Add US unit support for weather observations and forecasts
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-03-19 22:36:15 -05:00
parent 6e8adcc9cc
commit 26a52f8c44
5 changed files with 460 additions and 18 deletions

View File

@@ -3,11 +3,13 @@ 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"
)
@@ -18,45 +20,47 @@ type Service interface {
LatestActiveAlerts(ctx context.Context) (*model.WeatherAlertRun, error)
}
type emptyRequest struct{}
type queryRequest struct {
Units core.Units
}
func Definitions(svc Service) []endpoint.Definition {
return []endpoint.Definition{
endpoint.GET(
"/observations",
bindFormatOnly,
func(ctx context.Context, _ emptyRequest) (any, error) {
bindQuery,
func(ctx context.Context, req queryRequest) (any, error) {
obs, err := svc.LatestObservation(ctx)
if err != nil {
return nil, err
}
return response.Envelope{Data: obs}, nil
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",
bindFormatOnly,
func(ctx context.Context, _ emptyRequest) (any, error) {
bindQuery,
func(ctx context.Context, req queryRequest) (any, error) {
run, err := svc.LatestHourlyForecast(ctx)
if err != nil {
return nil, err
}
return response.Envelope{Data: run}, nil
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",
bindFormatOnly,
func(ctx context.Context, _ emptyRequest) (any, error) {
bindQuery,
func(ctx context.Context, req queryRequest) (any, error) {
run, err := svc.LatestActiveAlerts(ctx)
if err != nil {
return nil, err
}
return response.Envelope{Data: run}, nil
return response.Envelope{Data: core.AlertsPayload(run, req.Units)}, nil
},
endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText),
endpoint.WithTemplate("alerts_active.txt.tmpl"),
@@ -64,13 +68,38 @@ func Definitions(svc Service) []endpoint.Definition {
}
}
func bindFormatOnly(r *http.Request) (emptyRequest, error) {
_, err := bind.CommonQueryParams(r, bind.QueryPolicy{
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 emptyRequest{}, err
return queryRequest{}, err
}
return emptyRequest{}, nil
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()
}