From f0912e8c942466c78310485487c518ff38c1ad91 Mon Sep 17 00:00:00 2001 From: Valentin REVERSAT Date: Sat, 20 Jul 2024 01:17:49 +0200 Subject: [PATCH] feat(webcal): add the webcal format type return --- Dockerfile.dev | 0 compose.yml | 1 - debug.Dockerfile | 15 ++++++ debug.compose.yml | 26 +++++++++ go.mod | 5 +- go.sum | 2 + .../api/controllers/forecast_controller.go | 54 +++++++++++++++---- internal/domains/forecast_domain.go | 32 +++++++++++ internal/utils/controller.go | 10 ++-- internal/utils/webcal.go | 49 +++++++++++++++++ 10 files changed, 177 insertions(+), 17 deletions(-) delete mode 100644 Dockerfile.dev create mode 100644 debug.Dockerfile create mode 100644 debug.compose.yml create mode 100644 internal/utils/webcal.go diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index e69de29..0000000 diff --git a/compose.yml b/compose.yml index 8e85c61..ac8b04a 100644 --- a/compose.yml +++ b/compose.yml @@ -10,7 +10,6 @@ services: - persistent-app-volume:/app ports: - "8080:8080" - - "6060:6060" env_file: - .env.dev diff --git a/debug.Dockerfile b/debug.Dockerfile new file mode 100644 index 0000000..cd71e9a --- /dev/null +++ b/debug.Dockerfile @@ -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" ] diff --git a/debug.compose.yml b/debug.compose.yml new file mode 100644 index 0000000..73ecfb8 --- /dev/null +++ b/debug.compose.yml @@ -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: \ No newline at end of file diff --git a/go.mod b/go.mod index e9a1255..10394bb 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 0d753fa..0c69703 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/api/controllers/forecast_controller.go b/internal/api/controllers/forecast_controller.go index d90f35e..a7e1107 100644 --- a/internal/api/controllers/forecast_controller.go +++ b/internal/api/controllers/forecast_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "bytes" "fmt" "net/http" "time" @@ -16,6 +17,11 @@ type ForecastController struct { ForecastUseCase domains.ForecastUseCase } +const ( + jsonFormat = "json" + webcalFormat = "webcal" +) + // GetAllForecasts godoc // // @Summary Get all forecasts @@ -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) @@ -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{ @@ -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) } diff --git a/internal/domains/forecast_domain.go b/internal/domains/forecast_domain.go index 591c40e..470c019 100644 --- a/internal/domains/forecast_domain.go +++ b/internal/domains/forecast_domain.go @@ -1,7 +1,9 @@ package domains import ( + "bytes" "context" + "fmt" "os" "reflect" "time" @@ -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) diff --git a/internal/utils/controller.go b/internal/utils/controller.go index 5069b9d..9b76389 100644 --- a/internal/utils/controller.go +++ b/internal/utils/controller.go @@ -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 } diff --git a/internal/utils/webcal.go b/internal/utils/webcal.go new file mode 100644 index 0000000..a3d88d7 --- /dev/null +++ b/internal/utils/webcal.go @@ -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", + ) + +}