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
Empty file removed Dockerfile.dev
Empty file.
1 change: 0 additions & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ services:
- persistent-app-volume:/app
ports:
- "8080:8080"
- "6060:6060"
env_file:
- .env.dev

Expand Down
15 changes: 15 additions & 0 deletions debug.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# syntax=docker/dockerfile:1

FROM golang:1.22.4 as build
ARG VERSION
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
RUN go install github.com/swaggo/swag/cmd/swag@latest
COPY . .
RUN swag init -d ./internal/api/routers,./ -g main_router.go
# Debug mode
RUN CGO_ENABLED=0 go install -ldflags "-s -w -extldflags '-static'" github.com/go-delve/delve/cmd/dlv@latest
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/chabo-api -gcflags "all=-N -l" -ldflags="-X github.com/vareversat/chabo-api/internal/api/routers.version=$VERSION"

CMD [ "/go/bin/dlv", "--listen=:4000", "--headless=true", "--log=true", "--accept-multiclient", "--api-version=2", "exec", "/app/chabo-api" ]
26 changes: 26 additions & 0 deletions debug.compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
services:
app:
container_name: chabo-api
build:
context: .
dockerfile: debug.Dockerfile
args:
- VERSION=v0.0.0+dev
volumes:
- persistent-app-volume:/app
ports:
- "8080:8080"
- "4000:4000"
env_file:
- .env.dev

mongo:
container_name: mongo-server
image: mongo:6.0.16
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: my_password
MONGO_INITDB_DATABASE: chabo-api

volumes:
persistent-app-volume:
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
module github.com/vareversat/chabo-api

go 1.22

toolchain go1.22.0
go 1.22.0

require (
github.com/getsentry/sentry-go v0.28.1
github.com/gin-gonic/gin v1.10.0
github.com/stretchr/testify v1.9.0
github.com/swaggo/files v1.0.1
github.com/vareversat/gics v0.2.1
go.mongodb.org/mongo-driver v1.16.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vareversat/gics v0.2.1 h1:LuKK83Kdx2t8pbowY147AuFcEU2KMff6+QjdtngdrHY=
github.com/vareversat/gics v0.2.1/go.mod h1:6WZtZqEvvT1CyeGPAXVclCppimbAKX5MQu7oIoxPzs0=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
Expand Down
54 changes: 44 additions & 10 deletions internal/api/controllers/forecast_controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"bytes"
"fmt"
"net/http"
"time"
Expand All @@ -16,6 +17,11 @@ type ForecastController struct {
ForecastUseCase domains.ForecastUseCase
}

const (
jsonFormat = "json"
webcalFormat = "webcal"
)

// GetAllForecasts godoc
//
// @Summary Get all forecasts
Expand All @@ -27,8 +33,9 @@ type ForecastController struct {
// @Failure 400 {object} domains.APIErrorResponse{} "Some params are missing and/or not properly formatted from the requests"
// @Failure 500 {object} domains.APIErrorResponse{} "An error occurred on the server side"
// @Param from query string false "The date to filter from (RFC3339)" Format(date-time)
// @Param limit query int true "Set the limit of the queried results" Format(int) default(10)
// @Param offset query int true "Set the offset of the queried results" Format(int) default(0)
// @Param limit query int true "Set the limit of the queried results" Format(int) default(10)
// @Param offset query int true "Set the offset of the queried results" Format(int) default(0)
// @Param format query string true "json or webcal output" Enums(json, webcal) default(json)
// @Param reason query string false "The closing reason" Enums(boat, maintenance, wine_festival_boats, special_event)
// @Param boat query string false "The boat name of the event"
// @Param maneuver query string false "The boat maneuver of the event" Enums(leaving_bordeaux, entering_in_bordeaux)
Expand All @@ -47,12 +54,14 @@ func (fC *ForecastController) GetAllForecasts() gin.HandlerFunc {
location, locationErr := utils.GetTimezoneFromHeader(c)
limit, limitErr := utils.GetIntParams(c, "limit")
offset, offsetErr := utils.GetIntParams(c, "offset")
reason := utils.GetStringParams(c, "reason")
boat := utils.GetStringParams(c, "boat")
maneuver := utils.GetStringParams(c, "maneuver")
parsedTime, timeErr := time.Parse(time.RFC3339, utils.GetStringParams(c, "from"))

if timeErr != nil && utils.GetStringParams(c, "from") != "" {
from, _ := utils.GetStringParams(c, "from", false)
reason, _ := utils.GetStringParams(c, "reason", false)
boat, _ := utils.GetStringParams(c, "boat", false)
maneuver, _ := utils.GetStringParams(c, "maneuver", false)
parsedTime, timeErr := time.Parse(time.RFC3339, from)
outputFormat := c.DefaultQuery("format", jsonFormat)

if timeErr != nil && from != "" {
c.JSON(
http.StatusBadRequest,
domains.APIErrorResponse{
Expand Down Expand Up @@ -126,9 +135,34 @@ func (fC *ForecastController) GetAllForecasts() gin.HandlerFunc {
Timezone: location.String(),
}

c.JSON(http.StatusOK, response)
switch outputFormat {
case jsonFormat:
c.JSON(http.StatusOK, response)
case webcalFormat:
cal, err := utils.ComputeCalendar(forecasts, location.String())
if err != nil {
c.JSON(http.StatusInternalServerError, domains.APIErrorResponse{Error: err.Error()})
sentry.CaptureException(err)
return
} else {
output := bytes.Buffer{}
cal.SerializeToICSFormat(&output)
c.String(http.StatusOK, output.String())
}
default:
c.JSON(
http.StatusBadRequest,
domains.APIErrorResponse{
Error: fmt.Sprintf(
"You must use '%s' or '%s' values for format param",
jsonFormat,
webcalFormat,
),
},
)
sentry.CaptureException(locationErr)
}
}

return gin.HandlerFunc(fn)
}

Expand Down
32 changes: 32 additions & 0 deletions internal/domains/forecast_domain.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package domains

import (
"bytes"
"context"
"fmt"
"os"
"reflect"
"time"
Expand Down Expand Up @@ -89,6 +91,36 @@ func (forecasts *Forecasts) AreEqual(other Forecasts) bool {
return true
}

func (f *Forecast) GetSummary() string {
switch f.ClosingReason {
case BoatReason:
var summary bytes.Buffer
summary.WriteString(`Le pont Chaban sera fermé en raison des manoeuvres suivantes :\n`)
for _, boat := range f.Boats {
if boat.Maneuver == Leaving {
summary.WriteString(fmt.Sprintf(`\n • %s`, "Départ du "))
} else {
summary.WriteString(fmt.Sprintf(`\n • %s`, "Arrivée du "))
}
summary.WriteString(fmt.Sprintf(`%s`, boat.Name))
summary.WriteString(
fmt.Sprintf(
` (passage approx. prévu aux alentours de %s)`,
boat.CrossingDateApproximation.Format("15:04:05"),
),
)
}
return summary.String()
case Maintenance:
return fmt.Sprintf("Le pont Chanban sera fermé pour maintenance")
case WineFestivalBoats:
return fmt.Sprintf(
"Le pont Chanban sera fermé pour l'arrivée des bateaux de la fête du vin",
)
}
return ""
}

func (f *Forecast) ChangeLocation(location *time.Location) {
f.CirculationClosingDate = f.CirculationClosingDate.In(location)
f.CirculationReopeningDate = f.CirculationReopeningDate.In(location)
Expand Down
10 changes: 7 additions & 3 deletions internal/utils/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ func GetIntParams(c *gin.Context, paramName string) (int, error) {

// GetStringParams Get the string param paramName passed into the request.
// Return empty string if not specified or empty, the value either
func GetStringParams(c *gin.Context, paramName string) string {
func GetStringParams(c *gin.Context, paramName string, mandatory bool) (string, error) {

paramValue, _ := c.GetQuery(paramName)
paramValue, exists := c.GetQuery(paramName)

return paramValue
if !exists && mandatory {
return "", fmt.Errorf("you have to specify the %s in requests params", paramName)
}

return paramValue, nil

}

Expand Down
49 changes: 49 additions & 0 deletions internal/utils/webcal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package utils

import (
"time"

"github.com/vareversat/chabo-api/internal/domains"
"github.com/vareversat/gics"
"github.com/vareversat/gics/components"
"github.com/vareversat/gics/parameters"
"github.com/vareversat/gics/properties"
"github.com/vareversat/gics/types"
)

// ComputeCalendar take all the fetched forecasts and the requested timezone
// Return a gics.Calendar (webcal)
func ComputeCalendar(forecasts domains.Forecasts, timezone string) (gics.Calendar, error) {
calendarComponents := components.CalendarComponents{}
for _, forecast := range forecasts {
calendarComponents = append(calendarComponents, components.NewEventCalendarComponent(
properties.NewUidProperty(
forecast.ID,
),
properties.NewDateTimeStampProperty(time.Now().UTC()),
[]components.AlarmCalendarComponent{},
properties.NewDateTimeStartProperty(
forecast.CirculationClosingDate,
types.WithLocalTime,
parameters.NewTimeZoneIdentifierParam(timezone),
),
properties.NewDateTimeEndProperty(
forecast.CirculationReopeningDate,
types.WithLocalTime,
parameters.NewTimeZoneIdentifierParam(timezone),
),
properties.NewDescriptionProperty(forecast.GetSummary()),
properties.NewGeographicPositionProperty(44.858339101606994, -0.551626089048817),
properties.NewSummaryProperty("Fermeture du pont Chaban"),
properties.NewLocationProperty("Pont Jacques Chaban Delmas - Bordeaux"),
))
}

return gics.NewCalendar(
calendarComponents,
"-//Valentin REVERSAT//https://github.com/vareversat/gics//FR",
"PUBLISH",
"2.0",
)

}