package httpapi import ( "context" "net/http" "strings" "gitea.maximumdirect.net/ejr/feedapi/bind" "gitea.maximumdirect.net/ejr/feedapi/endpoint" "gitea.maximumdirect.net/ejr/feedapi/render" "gitea.maximumdirect.net/ejr/feedapi/response" "gitea.maximumdirect.net/ejr/weatherapi/internal/core" "gitea.maximumdirect.net/ejr/weatherfeeder/model" ) // Service describes the weather use-cases needed by the HTTP adapter. type Service interface { LatestObservation(ctx context.Context) (*model.WeatherObservation, error) LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error) LatestActiveAlerts(ctx context.Context) (*model.WeatherAlertRun, error) CurrentConditions(ctx context.Context) (*core.CurrentConditions, error) } type queryRequest struct { Units core.Units } func Definitions(svc Service) []endpoint.Definition { return []endpoint.Definition{ endpoint.GET( "/observations", bindQuery, func(ctx context.Context, req queryRequest) (any, error) { obs, err := svc.LatestObservation(ctx) if err != nil { return nil, err } return response.Envelope{Data: core.ObservationPayload(obs, req.Units)}, nil }, endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText), endpoint.WithTemplate("observations.txt.tmpl"), ), endpoint.GET( "/forecast/hourly", bindQuery, func(ctx context.Context, req queryRequest) (any, error) { run, err := svc.LatestHourlyForecast(ctx) if err != nil { return nil, err } return response.Envelope{Data: core.ForecastPayload(run, req.Units)}, nil }, endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText), endpoint.WithTemplate("forecast_hourly.txt.tmpl"), ), endpoint.GET( "/alerts/active", bindQuery, func(ctx context.Context, req queryRequest) (any, error) { run, err := svc.LatestActiveAlerts(ctx) if err != nil { return nil, err } return response.Envelope{Data: core.AlertsPayload(run, req.Units)}, nil }, endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText), endpoint.WithTemplate("alerts_active.txt.tmpl"), ), endpoint.GET( "/conditions/current", bindQuery, func(ctx context.Context, req queryRequest) (any, error) { conditions, err := svc.CurrentConditions(ctx) if err != nil { return nil, err } return response.Envelope{Data: core.CurrentConditionsPayload(conditions, req.Units)}, nil }, endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText), endpoint.WithTemplate("conditions_current.txt.tmpl"), ), } } func bindQuery(r *http.Request) (queryRequest, error) { normalizeCommonQueryValue(r, "units") normalizeCommonQueryValue(r, "format") common, err := bind.CommonQueryParams(r, bind.QueryPolicy{ AllowUnits: true, AllowFormat: true, DefaultUnits: string(core.UnitsMetric), RejectUnknown: true, }) if err != nil { return queryRequest{}, err } units := core.Units(strings.ToLower(strings.TrimSpace(common.Units))) if units == "" { units = core.UnitsMetric } return queryRequest{Units: units}, nil } func normalizeCommonQueryValue(r *http.Request, key string) { q := r.URL.Query() values, ok := q[key] if !ok || len(values) == 0 { return } normalized := strings.ToLower(strings.TrimSpace(values[0])) if normalized == values[0] { return } q.Set(key, normalized) r.URL.RawQuery = q.Encode() }