Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# Running Instruction
After clone, run this command:

```bash
cd api-gateway
docker-compose up
```

Application will listen on
```bash
http://localhost:3000
```

Documentation (Swagger) can be accessed :
```bash
http://localhost:3000/swagger/index.html
```

There is scheduler container which is hit this endpoint will be trigger every hour to fetch the data and insert it in the PostgreSQL database

```bash
POST http://localhost:3000/api/v1/indego-data-fetch-and-store-it-db
```

## Golang Backend Challenge

[Indego](https://www.rideindego.com) is Philadelphia's bike-sharing program, with many bike stations in the city.
Expand Down
8 changes: 8 additions & 0 deletions api-gateway/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
DB_HOST=postgresdb
DB_DRIVER=postgres
DB_USER=postgres
DB_PASSWORD=3p1phyte-corp.com
DB_NAME=postgres
DB_PORT=5432

OPENWEATHER_APIKEY=598eb70eacf1d53a1eec6ef3e6da25c2
1 change: 1 addition & 0 deletions api-gateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
14 changes: 14 additions & 0 deletions api-gateway/Dockerfile-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM golang:1.22.5-alpine3.19 AS builder

WORKDIR /build
COPY . .
RUN go mod download
RUN go get -u github.com/swaggo/swag
RUN go build -o ./api-gateway ./cmd/api-gateway/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /build/api-gateway ./api-gateway
COPY --from=builder /build/config ./config

CMD ["./api-gateway"]
8 changes: 8 additions & 0 deletions api-gateway/Dockerfile-cron
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.11-slim-buster

WORKDIR /build

RUN pip install requests schedule
COPY cron_job.py .
# RUN chmod +x cron_job.py
CMD ["python", "cron_job.py"]
10 changes: 10 additions & 0 deletions api-gateway/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
build:
go build -o api-gateway ./cmd/api-gateway

swag:
swag init -g ./cmd/api-gateway/main.go --markdownFiles swagger-markdown --parseDependency true

run:
go run cmd/api-gateway/main.go

.PHONY: swag run
210 changes: 210 additions & 0 deletions api-gateway/api/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package handlers

import (
"net/http"
"strconv"
"sync"
"time"

swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"

"github.com/arthben/BackendGolang/api-gateway/api/middlewares"
"github.com/arthben/BackendGolang/api-gateway/api/openweather"
"github.com/arthben/BackendGolang/api-gateway/api/rideindego"
"github.com/arthben/BackendGolang/api-gateway/docs"
"github.com/arthben/BackendGolang/api-gateway/internal/config"
"github.com/arthben/BackendGolang/api-gateway/internal/database"
"github.com/gin-gonic/gin"
)

type fetchResult struct {
HTTPCode int
Error error
}

type Handlers struct {
router *gin.Engine
rideindego *rideindego.Service
openweather *openweather.Service
}

func Barusaja() {

}

func NewHandler(db database.DBService, cfg *config.EnvParams) *Handlers {

return &Handlers{
router: gin.New(),
rideindego: rideindego.NewService(db),
openweather: openweather.NewService(db, cfg),
}
}

func (h *Handlers) BuildHandler() (http.Handler, error) {
h.setupMiddlewares()
h.setupSwagger()

apiv1 := h.router.Group("/api/v1")
{
apiv1.POST("/indego-data-fetch-and-store-it-db", h.FetchAndStoreIndego)
apiv1.GET("/stations", h.FindSpecifTime)
apiv1.GET("/stations/:kioskId", h.FindKioskWithTime)
}

return http.Handler(h.router), nil
}

func (h *Handlers) setupMiddlewares() {
h.router.Use(gin.Recovery())
h.router.Use(middlewares.AuthMiddleware())
h.router.Use(middlewares.CorsMiddleware())
}

func (h *Handlers) setupSwagger() {
docs.SwaggerInfo.Version = "1.0"
h.router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}

func (h *Handlers) fetchIndego() fetchResult {
httCode, err := h.rideindego.FetchAndStore()
return fetchResult{HTTPCode: httCode, Error: err}
}

func (h *Handlers) fetchWeather() fetchResult {
httpCode, err := h.openweather.FetchAndStore()
return fetchResult{HTTPCode: httpCode, Error: err}
}

// FetchAndStoreIndego godoc
// @Summary Store data from Indego
// @Description.markdown data-fetch
// @Tags API
// @Produce json
// @Param Authorization header string true "Bearer secret_token_static"
// @Router /api/v1/indego-data-fetch-and-store-it-db [post]
func (h *Handlers) FetchAndStoreIndego(c *gin.Context) {
var (
waitGroup sync.WaitGroup
chIndego = make(chan fetchResult)
chWeather = make(chan fetchResult)
)

waitGroup.Add(3)

go func() {
waitGroup.Wait()
close(chIndego)
close(chWeather)
}()

go func() {
defer waitGroup.Done()
result := h.fetchIndego()
chIndego <- result
}()

go func() {
defer waitGroup.Done()
result := h.fetchWeather()
chWeather <- result
}()

_ = <-chWeather
resIndego := <-chIndego

if resIndego.Error != nil {
c.AbortWithStatusJSON(resIndego.HTTPCode, gin.H{"error": resIndego.Error})
return
}

c.JSON(http.StatusOK, gin.H{"status": "Fetch and store success"})
}

// FindKioskWithTime godoc
// @Summary Snapshot of all stations at a specified time
// @Description.markdown stationsKioskId
// @Tags API
// @Produce json
// @Param Authorization header string true "Bearer secret_token_static"
// @Param at query string true "ex: 2019-09-01T10:00:00Z"
// @Param kioskId path string true "ex: 3005"
// @Router /api/v1/stations/{kioskId} [get]
func (h *Handlers) FindKioskWithTime(c *gin.Context) {
kioskId := c.Param("kioskId")
if _, err := strconv.Atoi(kioskId); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid kioskId format"})
return
}

q := c.Query("at")
if len(q) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request Parameter"})
return
}

at, err := time.Parse(time.RFC3339, q)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid timestamp format"})
return
}

jsonIndego, httpCode, err := h.rideindego.Search(at, kioskId)
if err != nil {
c.AbortWithStatusJSON(httpCode, gin.H{"error": err.Error()})
return
}

jsonWeather, httpCode, err := h.openweather.Search(at)
if err != nil {
c.AbortWithStatusJSON(httpCode, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{
"at": jsonIndego.LastUpdated.UTC(),
"stations": jsonIndego,
"weather": jsonWeather,
})
}

// FindSpecifTime godoc
// @Summary Snapshot of one station at a specific time
// @Description.markdown stations
// @Tags API
// @Produce json
// @Param Authorization header string true "Bearer secret_token_static"
// @Param at query string true "ex: 2019-09-01T10:00:00Z"
// @Router /api/v1/stations [get]
func (h *Handlers) FindSpecifTime(c *gin.Context) {
q := c.Query("at")
if len(q) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request Parameter"})
return
}

at, err := time.Parse(time.RFC3339, q)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid timestamp format"})
return
}

jsonIndego, httpCode, err := h.rideindego.Search(at, "")
if err != nil {
c.AbortWithStatusJSON(httpCode, gin.H{"error": err.Error()})
return
}

jsonWeather, httpCode, err := h.openweather.Search(at)
if err != nil {
c.AbortWithStatusJSON(httpCode, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{
"at": q,
"stations": jsonIndego,
"weather": jsonWeather,
})
}
Loading