9.9 KiB
SPC Convective Outlook API Implementation Plan
Summary
Implement the weatherapi SPC convective outlook API described in docs/roadmap/outlook.md.
This plan is execution-oriented. Implement stages in order. Keep weatherapi read-only: it must read weatherfeeder Postgres tables, reconstruct canonical model payloads, apply API filters, and present results over HTTP. Do not add ingestion, upstream SPC calls, table creation, migrations, or historical browsing.
Target routes:
GET /outlooks/convectiveGET /outlooks/convective/activeGET /outlooks/convective/location
All routes return the existing data envelope, support JSON/XML/text, and serve latest-run views only.
Stage 1: Application Use Case And Filtering
Implement the core use case before touching HTTP or SQL.
Required changes:
- Extend
internal/app.Repositorywith:LatestConvectiveOutlookRun(context.Context) (*model.WeatherOutlookRun, error)
- Add
internal/app.OutlookFilterwith:Day *intOutlookType stringContainsLocation *boolActiveAt *time.Time
- Add
LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error)tointernal/app.Service. - Implement
LatestConvectiveOutlookby callingrepo.LatestConvectiveOutlookRun(ctx), returningnil, nilfor no run, cloning the run, and filtering the clonedOutlooksslice. - Filtering rules:
Day: keep outlooks withoutlook.Day == *Day.OutlookType: keep outlooks with exact normalized value matchingoutlook.OutlookType.ContainsLocation: keep outlooks withoutlook.ContainsLocation == *ContainsLocation.ActiveAt: keep outlooks where!ActiveAt.Before(outlook.ValidFrom)andActiveAt.Before(outlook.ValidTo).
- Preserve original outlook order.
- Do not mutate the repository-returned run, slice, geometry, or pointer fields.
Tests:
- Add fake repository fields/methods in
internal/app/service_test.go. - Test repository delegation and no-data behavior.
- Test each filter independently.
- Test combined filters.
- Test active boundary behavior: includes exact
validFrom, excludes exactvalidTo. - Test filtered result keeps run metadata and can return
outlooks: []. - Test filtering does not mutate the original run or outlook slice.
Run:
go test ./internal/app
Stage 2: Postgres Outlook Read Adapter
Add latest-run reconstruction from weatherfeeder outlook tables.
Required changes:
- Add
outlooks_queries.go,outlooks_rows.go,outlooks_mapper.go, andoutlooks_read.gounderinternal/adapters/outbound/postgres. - Query latest parent from
outlook_runs:- selected columns:
event_id,location_id,location_name,latitude,longitude,as_of,issued_at - order:
as_of DESC, event_emitted_at DESC - limit:
1
- selected columns:
- Query child rows from
outlooksbyrun_event_id = $1, ordered byoutlook_index ASC. - Select and map all canonical child columns:
outlook_index,outlook_id,provider,product,day,outlook_type,label,label_text,severity_rank,valid_from,valid_to,issued_at,expires_at,forecaster,headline,summary,discussion,source_url,image_url,contains_location,geometry_json
- Map parent row into
model.WeatherOutlookRunwith UTC-normalizedAsOfandIssuedAtpointer. - Map child row into
model.WeatherOutlookwith UTC-normalized timestamps. - Convert
geometry_jsonintojson.RawMessagewithout parsing or reserializing. - Return
nil, nilwhen the latest parent query returnssql.ErrNoRows. - Wrap query, scan, iteration, and geometry decode/conversion failures with operation context.
- Confirm
var _ app.Repository = (*Repository)(nil)still compiles.
Tests:
- Extend
internal/adapters/outbound/postgres/repository_test.goor add outlook-specific mapper tests in the same package. - Test parent nullable mapping and UTC normalization.
- Test child mapping for every canonical field, including
outlook_id,provider,contains_location, andgeometry_json. - Test nullable fields map to nil/zero omitted model values consistently with existing helpers.
- Test geometry bytes are preserved exactly.
- Add read-path tests following existing repository test style if the package already has DB-backed query tests available; otherwise keep mapper-focused tests and rely on compile-time interface satisfaction.
Run:
go test ./internal/adapters/outbound/postgres
Stage 3: HTTP Route Definitions And Query Binding
Add route wiring and strict query validation.
Required changes:
- Extend
internal/adapters/inbound/httpapi.Servicewith:LatestConvectiveOutlook(context.Context, app.OutlookFilter) (*model.WeatherOutlookRun, error)
- Update HTTP test fakes to implement the new method.
- Add
outlooks_endpoint.gounderinternal/adapters/inbound/httpapi. - Register outlook definitions from
Definitions(svc)after weather stories or before discussion; route order should not affect behavior. - Add an outlook query binder that accepts common
format,units,tz,TZ, plus outlook-specific filters. - Reuse existing timezone parsing and common query normalization patterns.
- Validate:
daymust be integer1,2, or3.outlookTypemust be one ofcategorical,tornado,hail,wind; normalize case/space like other common values.containsLocationmust parse as boolean when allowed.precisionand unknown params are rejected.containsLocationis rejected on/outlooks/convective/location.
- Add package-level
outlookNow = time.Nowfor active/location filters. - Route filter construction:
/outlooks/convective: use user filters only./outlooks/convective/active: addActiveAt=outlookNow().UTC()plus user filters./outlooks/convective/location: addActiveAt=outlookNow().UTC()andContainsLocation=true; reject explicitcontainsLocation.
- Return
response.Envelope{Data: presenter.OutlookRunPayload(run, req.Units, req.Timezone)}.
Tests:
- Route registration for all three paths.
- JSON success for each route.
- No repository data returns
data: null. - Filtered no-match run returns
outlooks: []. day,outlookType, andcontainsLocationquery params construct expected filters.- Active and location endpoints use deterministic
outlookNowin tests. precision, unknown params, invalid day/type/bool, invalid timezone, conflictingtz/TZ, andcontainsLocationon/locationreturn400 Bad Request.
Run:
go test ./internal/adapters/inbound/httpapi
Stage 4: Presenter And Text Template
Add outlook response presentation while preserving canonical shape.
Required changes:
- Add
presenter/outlook.go. - Implement
OutlookRunPayload(run *model.WeatherOutlookRun, _ Units, tz *time.Location) any. - Return nil for nil input.
- Copy run fields and outlook entries before timestamp conversion.
- Convert run
AsOf, runIssuedAt, outlookValidFrom,ValidTo,IssuedAt, andExpiresAtwith existing timezone helper behavior. - Preserve
Latitude,Longitude,SeverityRank, andGeometrywithout mutating source pointers or slices. - Keep
unitsaccepted but ignored. - Add
templates/outlooks_convective.txt.tmpl. - Template should render a clear no-data message when
.Datais nil and concise outlook summaries when data exists.
Tests:
- Presenter nil input returns nil.
- Timezone conversion affects all run/outlook timestamps and preserves instant.
- Source model is not mutated.
- Geometry bytes are preserved exactly.
- Endpoint text response uses the new template.
- XML response renders without handler error.
Run:
go test ./internal/adapters/inbound/httpapi/presenter
go test ./internal/adapters/inbound/httpapi
Stage 5: Current-Behavior Documentation
Update docs only after the endpoint behavior is implemented in code.
Required changes:
- Update
README.mdendpoint list with:GET /outlooks/convectiveGET /outlooks/convective/activeGET /outlooks/convective/location
- Update
README.mdquery parameter summary to mention outlook filters:day,outlookType, andcontainsLocation. - Update
docs/api.mdwith an Outlook endpoints section covering:- route behavior
- supported query params and rejected params
- active filtering semantics
containsLocationsemantics- response fields and nullability
- GeoJSON geometry coordinate order
- examples for full and location-filtered responses
- text format note
- Update
docs/internal/http-adapter.md,docs/internal/presenters.md,docs/internal/postgres-repository.md, anddocs/integrations/weatherfeeder-postgres.mdonly if the implementation adds details not already covered by existing internal/integration docs. - Do not move implemented behavior into roadmap docs; roadmap docs should remain future/planning context until retired.
Tests:
- No documentation-specific tests are currently required unless existing tests check docs.
- Run full test suite after docs updates to catch template or route registration regressions.
Run:
go test ./...
Stage 6: Final Verification And Cleanup
Before committing the implementation:
- Run focused tests:
go test ./internal/app
go test ./internal/adapters/outbound/postgres
go test ./internal/adapters/inbound/httpapi
go test ./internal/adapters/inbound/httpapi/presenter
- Run full tests:
go test ./...
- Check status and diff:
git status --short
git diff --stat
- Confirm no unrelated files are included.
- Confirm no weatherfeeder ingestion behavior, schema creation, migration logic, or upstream SPC calls were added.
- Confirm current-behavior docs no longer describe outlook endpoints as future work once implementation is complete, or leave roadmap docs clearly marked as historical/planning if they remain.
Open Questions
None. The feature roadmap defines the route names, latest-run semantics, filters, adapter ownership, documentation timing, and validation behavior needed for implementation.