Add convective outlook HTTP routes
This commit is contained in:
@@ -11,6 +11,7 @@ func Definitions(svc Service) []endpoint.Definition {
|
|||||||
conditionsDefinition(svc),
|
conditionsDefinition(svc),
|
||||||
}
|
}
|
||||||
defs = append(defs, weatherStoriesDefinitions(svc)...)
|
defs = append(defs, weatherStoriesDefinitions(svc)...)
|
||||||
|
defs = append(defs, outlookDefinitions(svc)...)
|
||||||
defs = append(defs, discussionDefinitions(svc)...)
|
defs = append(defs, discussionDefinitions(svc)...)
|
||||||
defs = append(defs, forecastDefinitions(svc)...)
|
defs = append(defs, forecastDefinitions(svc)...)
|
||||||
return defs
|
return defs
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ type fakeService struct {
|
|||||||
weatherStoryRun *model.WeatherStoryRun
|
weatherStoryRun *model.WeatherStoryRun
|
||||||
weatherStory *model.WeatherStory
|
weatherStory *model.WeatherStory
|
||||||
alerts *model.WeatherAlertRun
|
alerts *model.WeatherAlertRun
|
||||||
|
outlookRun *model.WeatherOutlookRun
|
||||||
|
outlookFilters []app.OutlookFilter
|
||||||
conditions *app.CurrentConditions
|
conditions *app.CurrentConditions
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,11 @@ func (s *fakeService) LatestAlertRun(context.Context) (*model.WeatherAlertRun, e
|
|||||||
return s.alerts, s.err
|
return s.alerts, s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *fakeService) LatestConvectiveOutlook(_ context.Context, filter app.OutlookFilter) (*model.WeatherOutlookRun, error) {
|
||||||
|
s.outlookFilters = append(s.outlookFilters, filter)
|
||||||
|
return s.outlookRun, s.err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fakeService) CurrentConditions(context.Context) (*app.CurrentConditions, error) {
|
func (s *fakeService) CurrentConditions(context.Context) (*app.CurrentConditions, error) {
|
||||||
return s.conditions, s.err
|
return s.conditions, s.err
|
||||||
}
|
}
|
||||||
@@ -1272,6 +1279,208 @@ func TestAlertsRejectPrecisionQueryParameter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOutlookRoutesRegistered(t *testing.T) {
|
||||||
|
defs := Definitions(&fakeService{})
|
||||||
|
for _, path := range []string{
|
||||||
|
"/outlooks/convective",
|
||||||
|
"/outlooks/convective/active",
|
||||||
|
"/outlooks/convective/location",
|
||||||
|
} {
|
||||||
|
def := definitionForPath(t, defs, path)
|
||||||
|
if len(def.Methods) != 1 || def.Methods[0] != http.MethodGet {
|
||||||
|
t.Fatalf("%s: expected GET definition, got %+v", path, def.Methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookRoutesJSONSuccess(t *testing.T) {
|
||||||
|
setOutlookNowForTest(t, time.Date(2026, 6, 11, 15, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
for _, path := range []string{
|
||||||
|
"/outlooks/convective",
|
||||||
|
"/outlooks/convective/active",
|
||||||
|
"/outlooks/convective/location",
|
||||||
|
} {
|
||||||
|
t.Run(path, func(t *testing.T) {
|
||||||
|
h := newHandler(t, &fakeService{outlookRun: testOutlookRun()}, path)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Data struct {
|
||||||
|
LocationID string `json:"locationId"`
|
||||||
|
Outlooks []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"outlooks"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode outlook payload: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Data.LocationID != "stl" {
|
||||||
|
t.Fatalf("expected locationId stl, got %q", payload.Data.LocationID)
|
||||||
|
}
|
||||||
|
if len(payload.Data.Outlooks) != 1 || payload.Data.Outlooks[0].ID != "cat-1" {
|
||||||
|
t.Fatalf("unexpected outlooks payload: %+v", payload.Data.Outlooks)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookNoDataReturnsNullEnvelopeData(t *testing.T) {
|
||||||
|
h := newHandler(t, &fakeService{}, "/outlooks/convective")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/outlooks/convective", nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Data *json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode envelope: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Data != nil {
|
||||||
|
t.Fatalf("expected data null, got %s", string(*payload.Data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookFilteredNoMatchReturnsEmptyOutlooks(t *testing.T) {
|
||||||
|
run := testOutlookRun()
|
||||||
|
run.Outlooks = []model.WeatherOutlook{}
|
||||||
|
h := newHandler(t, &fakeService{outlookRun: run}, "/outlooks/convective")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/outlooks/convective?day=3", nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Data struct {
|
||||||
|
LocationID string `json:"locationId"`
|
||||||
|
Outlooks []model.WeatherOutlook `json:"outlooks"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode outlook payload: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Data.LocationID != "stl" {
|
||||||
|
t.Fatalf("expected metadata to remain populated, got %+v", payload.Data)
|
||||||
|
}
|
||||||
|
if payload.Data.Outlooks == nil || len(payload.Data.Outlooks) != 0 {
|
||||||
|
t.Fatalf("expected empty outlooks slice, got %+v", payload.Data.Outlooks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookQueryParamsConstructFilter(t *testing.T) {
|
||||||
|
svc := &fakeService{outlookRun: testOutlookRun()}
|
||||||
|
h := newHandler(t, svc, "/outlooks/convective")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/outlooks/convective?day=2&outlookType=Tornado&containsLocation=true&tz=CDT&units=US", nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
if len(svc.outlookFilters) != 1 {
|
||||||
|
t.Fatalf("expected one filter, got %d", len(svc.outlookFilters))
|
||||||
|
}
|
||||||
|
filter := svc.outlookFilters[0]
|
||||||
|
if filter.Day == nil || *filter.Day != 2 {
|
||||||
|
t.Fatalf("expected day filter 2, got %+v", filter.Day)
|
||||||
|
}
|
||||||
|
if filter.OutlookType != "tornado" {
|
||||||
|
t.Fatalf("expected outlookType tornado, got %q", filter.OutlookType)
|
||||||
|
}
|
||||||
|
if filter.ContainsLocation == nil || !*filter.ContainsLocation {
|
||||||
|
t.Fatalf("expected containsLocation true, got %+v", filter.ContainsLocation)
|
||||||
|
}
|
||||||
|
if filter.ActiveAt != nil {
|
||||||
|
t.Fatalf("expected no active filter, got %v", filter.ActiveAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookActiveAndLocationFiltersUseNow(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 11, 15, 30, 0, 0, time.FixedZone("CDT", -5*3600))
|
||||||
|
setOutlookNowForTest(t, now)
|
||||||
|
|
||||||
|
activeSvc := &fakeService{outlookRun: testOutlookRun()}
|
||||||
|
activeHandler := newHandler(t, activeSvc, "/outlooks/convective/active")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/outlooks/convective/active?day=1", nil)
|
||||||
|
activeHandler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected active 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
activeFilter := activeSvc.outlookFilters[0]
|
||||||
|
if activeFilter.ActiveAt == nil || !activeFilter.ActiveAt.Equal(now.UTC()) {
|
||||||
|
t.Fatalf("expected activeAt %s, got %v", now.UTC(), activeFilter.ActiveAt)
|
||||||
|
}
|
||||||
|
if activeFilter.ContainsLocation != nil {
|
||||||
|
t.Fatalf("expected active route not to force containsLocation, got %+v", activeFilter.ContainsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
locationSvc := &fakeService{outlookRun: testOutlookRun()}
|
||||||
|
locationHandler := newHandler(t, locationSvc, "/outlooks/convective/location")
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/outlooks/convective/location?outlookType=hail", nil)
|
||||||
|
locationHandler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected location 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
locationFilter := locationSvc.outlookFilters[0]
|
||||||
|
if locationFilter.ActiveAt == nil || !locationFilter.ActiveAt.Equal(now.UTC()) {
|
||||||
|
t.Fatalf("expected location activeAt %s, got %v", now.UTC(), locationFilter.ActiveAt)
|
||||||
|
}
|
||||||
|
if locationFilter.ContainsLocation == nil || !*locationFilter.ContainsLocation {
|
||||||
|
t.Fatalf("expected location route to force containsLocation true, got %+v", locationFilter.ContainsLocation)
|
||||||
|
}
|
||||||
|
if locationFilter.OutlookType != "hail" {
|
||||||
|
t.Fatalf("expected outlookType hail, got %q", locationFilter.OutlookType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutlookInvalidQueryParamsReturnBadRequest(t *testing.T) {
|
||||||
|
for _, rawURL := range []string{
|
||||||
|
"/outlooks/convective?precision=1",
|
||||||
|
"/outlooks/convective?bogus=1",
|
||||||
|
"/outlooks/convective?day=0",
|
||||||
|
"/outlooks/convective?day=4",
|
||||||
|
"/outlooks/convective?day=two",
|
||||||
|
"/outlooks/convective?outlookType=snow",
|
||||||
|
"/outlooks/convective?containsLocation=maybe",
|
||||||
|
"/outlooks/convective?tz=not-a-timezone",
|
||||||
|
"/outlooks/convective?tz=CDT&TZ=EST",
|
||||||
|
"/outlooks/convective/location?containsLocation=true",
|
||||||
|
} {
|
||||||
|
t.Run(rawURL, func(t *testing.T) {
|
||||||
|
h := newHandler(t, &fakeService{outlookRun: testOutlookRun()}, strings.Split(rawURL, "?")[0])
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, rawURL, nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDiscussionNoDataReturnsNullEnvelopeData(t *testing.T) {
|
func TestDiscussionNoDataReturnsNullEnvelopeData(t *testing.T) {
|
||||||
h := newHandler(t, &fakeService{}, "/discussion")
|
h := newHandler(t, &fakeService{}, "/discussion")
|
||||||
|
|
||||||
@@ -1965,6 +2174,7 @@ func testRenderers(t *testing.T) *render.Registry {
|
|||||||
"discussion_long_term.txt.tmpl": "Forecast Discussion Long Term",
|
"discussion_long_term.txt.tmpl": "Forecast Discussion Long Term",
|
||||||
"forecast_hourly.txt.tmpl": "Forecast text",
|
"forecast_hourly.txt.tmpl": "Forecast text",
|
||||||
"forecast_narrative.txt.tmpl": "Narrative Forecast",
|
"forecast_narrative.txt.tmpl": "Narrative Forecast",
|
||||||
|
"outlooks_convective.txt.tmpl": "Convective Outlook",
|
||||||
"weatherstories.txt.tmpl": "Weather Stories",
|
"weatherstories.txt.tmpl": "Weather Stories",
|
||||||
"weatherstories_latest.txt.tmpl": "Latest Weather Story",
|
"weatherstories_latest.txt.tmpl": "Latest Weather Story",
|
||||||
"alerts_active.txt.tmpl": "Alerts text",
|
"alerts_active.txt.tmpl": "Alerts text",
|
||||||
@@ -1994,6 +2204,38 @@ func wmoCodePtr(v model.WMOCode) *model.WMOCode {
|
|||||||
return &out
|
return &out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setOutlookNowForTest(t *testing.T, now time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
original := outlookNow
|
||||||
|
outlookNow = func() time.Time { return now }
|
||||||
|
t.Cleanup(func() { outlookNow = original })
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOutlookRun() *model.WeatherOutlookRun {
|
||||||
|
issuedAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC)
|
||||||
|
return &model.WeatherOutlookRun{
|
||||||
|
LocationID: "stl",
|
||||||
|
LocationName: "St. Louis",
|
||||||
|
AsOf: issuedAt,
|
||||||
|
IssuedAt: &issuedAt,
|
||||||
|
Outlooks: []model.WeatherOutlook{{
|
||||||
|
ID: "cat-1",
|
||||||
|
Provider: "spc",
|
||||||
|
Product: "convective",
|
||||||
|
Day: 1,
|
||||||
|
OutlookType: "categorical",
|
||||||
|
Label: "SLGT",
|
||||||
|
LabelText: "Slight Risk",
|
||||||
|
ValidFrom: issuedAt,
|
||||||
|
ValidTo: issuedAt.Add(6 * time.Hour),
|
||||||
|
IssuedAt: issuedAt,
|
||||||
|
ExpiresAt: issuedAt.Add(6 * time.Hour),
|
||||||
|
ContainsLocation: true,
|
||||||
|
Geometry: []byte(`{"type":"Point","coordinates":[-90.2,38.6]}`),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sampleWeatherStoryRun() *model.WeatherStoryRun {
|
func sampleWeatherStoryRun() *model.WeatherStoryRun {
|
||||||
return &model.WeatherStoryRun{
|
return &model.WeatherStoryRun{
|
||||||
OfficeID: "LSX",
|
OfficeID: "LSX",
|
||||||
|
|||||||
63
internal/adapters/inbound/httpapi/outlooks_endpoint.go
Normal file
63
internal/adapters/inbound/httpapi/outlooks_endpoint.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// outlooks_endpoint.go defines convective outlook endpoint behavior.
|
||||||
|
// Layer: adapters/inbound/httpapi outlook routes.
|
||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/ejr/feedapi/endpoint"
|
||||||
|
"gitea.maximumdirect.net/ejr/feedapi/render"
|
||||||
|
"gitea.maximumdirect.net/ejr/feedapi/response"
|
||||||
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type outlookFilterMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
outlookFilterUser outlookFilterMode = iota
|
||||||
|
outlookFilterActive
|
||||||
|
outlookFilterLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
var outlookNow = time.Now
|
||||||
|
|
||||||
|
func outlookDefinitions(svc Service) []endpoint.Definition {
|
||||||
|
return []endpoint.Definition{
|
||||||
|
outlookDefinition("/outlooks/convective", outlookFilterUser, bindOutlookQuery, svc),
|
||||||
|
outlookDefinition("/outlooks/convective/active", outlookFilterActive, bindOutlookQuery, svc),
|
||||||
|
outlookDefinition("/outlooks/convective/location", outlookFilterLocation, bindOutlookLocationQuery, svc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func outlookDefinition(
|
||||||
|
path string,
|
||||||
|
mode outlookFilterMode,
|
||||||
|
binder func(*http.Request) (outlookQueryRequest, error),
|
||||||
|
svc Service,
|
||||||
|
) endpoint.Definition {
|
||||||
|
return endpoint.GET(
|
||||||
|
path,
|
||||||
|
binder,
|
||||||
|
func(ctx context.Context, req outlookQueryRequest) (any, error) {
|
||||||
|
filter := req.Filter
|
||||||
|
if mode == outlookFilterActive || mode == outlookFilterLocation {
|
||||||
|
activeAt := outlookNow().UTC()
|
||||||
|
filter.ActiveAt = &activeAt
|
||||||
|
}
|
||||||
|
if mode == outlookFilterLocation {
|
||||||
|
containsLocation := true
|
||||||
|
filter.ContainsLocation = &containsLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
run, err := svc.LatestConvectiveOutlook(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response.Envelope{Data: presenter.OutlookRunPayload(run, req.Units, req.Timezone)}, nil
|
||||||
|
},
|
||||||
|
endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText),
|
||||||
|
endpoint.WithTemplate("outlooks_convective.txt.tmpl"),
|
||||||
|
)
|
||||||
|
}
|
||||||
16
internal/adapters/inbound/httpapi/presenter/outlook.go
Normal file
16
internal/adapters/inbound/httpapi/presenter/outlook.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// outlook.go presents convective outlook payloads.
|
||||||
|
// Layer: adapters/inbound/httpapi/presenter outlook payload mapping.
|
||||||
|
package presenter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OutlookRunPayload(run *model.WeatherOutlookRun, _ Units, _ *time.Location) any {
|
||||||
|
if run == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return run
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/ejr/feedapi/bind"
|
"gitea.maximumdirect.net/ejr/feedapi/bind"
|
||||||
|
apierrors "gitea.maximumdirect.net/ejr/feedapi/errors"
|
||||||
"gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter"
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter"
|
||||||
|
"gitea.maximumdirect.net/ejr/weatherapi/internal/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
type queryRequest struct {
|
type queryRequest struct {
|
||||||
@@ -26,6 +28,12 @@ type timezoneQueryRequest struct {
|
|||||||
Timezone *time.Location
|
Timezone *time.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type outlookQueryRequest struct {
|
||||||
|
Units presenter.Units
|
||||||
|
Timezone *time.Location
|
||||||
|
Filter app.OutlookFilter
|
||||||
|
}
|
||||||
|
|
||||||
func bindQuery(r *http.Request) (queryRequest, error) {
|
func bindQuery(r *http.Request) (queryRequest, error) {
|
||||||
normalizeCommonQueryValue(r, "units")
|
normalizeCommonQueryValue(r, "units")
|
||||||
normalizeCommonQueryValue(r, "format")
|
normalizeCommonQueryValue(r, "format")
|
||||||
@@ -135,3 +143,91 @@ func bindPrecisionQueryInternal(r *http.Request, allowTimezone bool) (precisionQ
|
|||||||
Timezone: tz,
|
Timezone: tz,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bindOutlookQuery(r *http.Request) (outlookQueryRequest, error) {
|
||||||
|
return bindOutlookQueryInternal(r, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindOutlookLocationQuery(r *http.Request) (outlookQueryRequest, error) {
|
||||||
|
return bindOutlookQueryInternal(r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindOutlookQueryInternal(r *http.Request, allowContainsLocation bool) (outlookQueryRequest, error) {
|
||||||
|
normalizeCommonQueryValue(r, "units")
|
||||||
|
normalizeCommonQueryValue(r, "format")
|
||||||
|
normalizeCommonQueryValue(r, "outlookType")
|
||||||
|
|
||||||
|
allowedExtra := []string{"tz", "TZ", "day", "outlookType"}
|
||||||
|
if allowContainsLocation {
|
||||||
|
allowedExtra = append(allowedExtra, "containsLocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
common, err := bind.CommonQueryParams(r, bind.QueryPolicy{
|
||||||
|
AllowUnits: true,
|
||||||
|
AllowFormat: true,
|
||||||
|
DefaultUnits: string(presenter.UnitsMetric),
|
||||||
|
RejectUnknown: true,
|
||||||
|
}, allowedExtra...)
|
||||||
|
if err != nil {
|
||||||
|
return outlookQueryRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units)))
|
||||||
|
if units == "" {
|
||||||
|
units = presenter.UnitsMetric
|
||||||
|
}
|
||||||
|
|
||||||
|
tz, err := parseTimezoneQuery(r)
|
||||||
|
if err != nil {
|
||||||
|
return outlookQueryRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := bindOutlookFilter(r, allowContainsLocation)
|
||||||
|
if err != nil {
|
||||||
|
return outlookQueryRequest{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outlookQueryRequest{
|
||||||
|
Units: units,
|
||||||
|
Timezone: tz,
|
||||||
|
Filter: filter,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindOutlookFilter(r *http.Request, allowContainsLocation bool) (app.OutlookFilter, error) {
|
||||||
|
var filter app.OutlookFilter
|
||||||
|
|
||||||
|
if strings.TrimSpace(r.URL.Query().Get("day")) != "" {
|
||||||
|
day, err := bind.OptionalInt(r, "day", 0)
|
||||||
|
if err != nil {
|
||||||
|
return app.OutlookFilter{}, err
|
||||||
|
}
|
||||||
|
if day < 1 || day > 3 {
|
||||||
|
return app.OutlookFilter{}, apierrors.InvalidParameter("day must be one of [1, 2, 3]")
|
||||||
|
}
|
||||||
|
filter.Day = &day
|
||||||
|
}
|
||||||
|
|
||||||
|
outlookType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("outlookType")))
|
||||||
|
if outlookType != "" {
|
||||||
|
switch outlookType {
|
||||||
|
case "categorical", "tornado", "hail", "wind":
|
||||||
|
filter.OutlookType = outlookType
|
||||||
|
default:
|
||||||
|
return app.OutlookFilter{}, apierrors.InvalidParameter("outlookType must be one of [categorical, tornado, hail, wind]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(r.URL.Query().Get("containsLocation")) != "" {
|
||||||
|
if !allowContainsLocation {
|
||||||
|
return app.OutlookFilter{}, apierrors.InvalidParameter("containsLocation is not allowed on this endpoint")
|
||||||
|
}
|
||||||
|
containsLocation, err := bind.OptionalBool(r, "containsLocation", false)
|
||||||
|
if err != nil {
|
||||||
|
return app.OutlookFilter{}, err
|
||||||
|
}
|
||||||
|
filter.ContainsLocation = &containsLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ type Service interface {
|
|||||||
LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error)
|
LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error)
|
||||||
LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error)
|
LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error)
|
||||||
LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
|
LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
|
||||||
|
LatestConvectiveOutlook(ctx context.Context, filter app.OutlookFilter) (*model.WeatherOutlookRun, error)
|
||||||
CurrentConditions(ctx context.Context) (*app.CurrentConditions, error)
|
CurrentConditions(ctx context.Context) (*app.CurrentConditions, error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user