Files
weatherapi/docs/roadmap/implementation.md

10 KiB

SPC Convective Outlook API Implementation Plan

Status: implemented. This file is retained as planning history; current behavior is documented in docs/api.md and the internal docs under docs/internal/.

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.

The implementation target is gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0, which provides the canonical outlook model and Postgres table contract this feature consumes.

Target routes:

  • GET /outlooks/convective
  • GET /outlooks/convective/active
  • GET /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.Repository with:
    • LatestConvectiveOutlookRun(context.Context) (*model.WeatherOutlookRun, error)
  • Add internal/app.OutlookFilter with:
    • Day *int
    • OutlookType string
    • ContainsLocation *bool
    • ActiveAt *time.Time
  • Add LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error) to internal/app.Service.
  • Implement LatestConvectiveOutlook by calling repo.LatestConvectiveOutlookRun(ctx), returning nil, nil for no run, cloning the run, and filtering the cloned Outlooks slice.
  • Filtering rules:
    • Day: keep outlooks with outlook.Day == *Day.
    • OutlookType: keep outlooks with exact normalized value matching outlook.OutlookType.
    • ContainsLocation: keep outlooks with outlook.ContainsLocation == *ContainsLocation.
    • ActiveAt: keep outlooks where !ActiveAt.Before(outlook.ValidFrom) and ActiveAt.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 exact validTo.
  • 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, and outlooks_read.go under internal/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
  • Query child rows from outlooks by run_event_id = $1, ordered by outlook_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.WeatherOutlookRun with UTC-normalized AsOf and IssuedAt pointer.
  • Map child row into model.WeatherOutlook with UTC-normalized timestamps.
  • Convert geometry_json into json.RawMessage without parsing or reserializing.
  • Return nil, nil when the latest parent query returns sql.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.go or 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, and geometry_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.Service with:
    • LatestConvectiveOutlook(context.Context, app.OutlookFilter) (*model.WeatherOutlookRun, error)
  • Update HTTP test fakes to implement the new method.
  • Add outlooks_endpoint.go under internal/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:
    • day must be integer 1, 2, or 3.
    • outlookType must be one of categorical, tornado, hail, wind; normalize case/space like other common values.
    • containsLocation must parse as boolean when allowed.
    • precision and unknown params are rejected.
    • containsLocation is rejected on /outlooks/convective/location.
  • Add package-level outlookNow = time.Now for active/location filters.
  • Route filter construction:
    • /outlooks/convective: use user filters only.
    • /outlooks/convective/active: add ActiveAt=outlookNow().UTC() plus user filters.
    • /outlooks/convective/location: add ActiveAt=outlookNow().UTC() and ContainsLocation=true; reject explicit containsLocation.
  • 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, and containsLocation query params construct expected filters.
  • Active and location endpoints use deterministic outlookNow in tests.
  • precision, unknown params, invalid day/type/bool, invalid timezone, conflicting tz/TZ, and containsLocation on /location return 400 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, run IssuedAt, outlook ValidFrom, ValidTo, IssuedAt, and ExpiresAt with existing timezone helper behavior.
  • Preserve Latitude, Longitude, SeverityRank, and Geometry without mutating source pointers or slices.
  • Keep units accepted but ignored.
  • Add templates/outlooks_convective.txt.tmpl.
  • Template should render a clear no-data message when .Data is 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.md endpoint list with:
    • GET /outlooks/convective
    • GET /outlooks/convective/active
    • GET /outlooks/convective/location
  • Update README.md query parameter summary to mention outlook filters: day, outlookType, and containsLocation.
  • Update docs/api.md with an Outlook endpoints section covering:
    • route behavior
    • supported query params and rejected params
    • active filtering semantics
    • containsLocation semantics
    • 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, and docs/integrations/weatherfeeder-postgres.md only 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 go.mod still requires gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0.
  • 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.