Files
weatherapi/internal/adapters/inbound/httpapi/endpoints.go
Eric Rakestraw 26a52f8c44
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
Add US unit support for weather observations and forecasts
2026-03-19 22:36:15 -05:00

106 lines
2.9 KiB
Go

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)
}
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"),
),
}
}
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()
}