diff --git a/docs/internal/http-adapter.md b/docs/internal/http-adapter.md new file mode 100644 index 0000000..4b8ee2f --- /dev/null +++ b/docs/internal/http-adapter.md @@ -0,0 +1,151 @@ +# HTTP Adapter + +This document describes the internal HTTP adapter under +`internal/adapters/inbound/httpapi`. The public endpoint contract belongs in +[`docs/api.md`](../api.md). + +## Purpose + +The HTTP adapter turns feedapi route definitions into calls on the application +service boundary. It owns route registration, query binding, request validation, +forecast day-slice filtering, response envelopes, and template names. + +## Inputs and Outputs + +Inputs: + +- feedapi HTTP requests; +- query parameters; +- an implementation of the adapter-local `Service` interface. + +Outputs: + +- `endpoint.Definition` values registered by runtime composition; +- `response.Envelope{Data: ...}` values for successful requests; +- typed feedapi errors for invalid query parameters; +- endpoint-specific template names for text rendering. + +## Boundaries + +The adapter may: + +- define routes and supported response formats; +- bind and validate query parameters; +- call the `Service` interface; +- choose the presenter function for an endpoint; +- filter forecast copies for `/today` and `/tomorrow`. + +The adapter must not: + +- execute SQL; +- open databases; +- mutate repository-returned models in place; +- duplicate public API reference text; +- move unit conversion or timezone presentation out of presenters. + +## Config Fields Used + +The HTTP adapter does not read config directly. Feedapi applies server and +renderer configuration before requests reach these handlers. Template names are +declared in endpoint definitions, but `templates.base_dir` is loaded by feedapi. + +## External Adapters Used + +- `feedapi/endpoint` for route definitions. +- `feedapi/render` for declared output formats. +- `feedapi/response` for success envelopes. +- `feedapi/bind` and `feedapi/errors` for query validation failures. +- `presenter` package for payload shaping. + +## State + +The adapter has no durable state. `forecastNow` is package-level state only to +make forecast day filtering testable. Do not add request caches or cross-request +mutable state here. + +## Route Registry + +`Definitions(svc)` returns all implemented route definitions. It includes: + +- observations; +- active alerts; +- current conditions; +- weather stories; +- forecast discussions; +- hourly and narrative forecasts. + +Each route uses `endpoint.GET`, declares JSON/XML/text production, and names a +text template. Feedapi owns routing, format negotiation, response rendering, +middleware, and error normalization after definitions are registered. + +## Query Binding + +There are three binder shapes: + +- `bindQuery`: `format` and `units`; +- `bindPrecisionQuery`: `format`, `units`, and `precision`; +- `bindForecastPrecisionQuery`: `format`, `units`, `precision`, and timezone; +- `bindTimezoneQuery`: `format`, `units`, and timezone. + +All binders use feedapi binding helpers with `RejectUnknown: true`. Supported +common query values are lowercased and trimmed before binding where applicable. + +`precision` defaults to `0` and must be between `0` and `2`. Timezone parsing is +available only through binders used by forecast, discussion, and weather story +routes. + +## Timezone Parsing + +Timezone parsing accepts: + +- IANA names through `time.LoadLocation`; +- configured aliases such as `Chicago` and `Stl`; +- common US abbreviations handled as fixed zones; +- signed UTC offsets. + +If both `tz` and `TZ` are provided, they must match case-insensitively. +Invalid timezone input is converted to a feedapi invalid-parameter error. + +## Forecast Day Slices + +Forecast base routes return the latest run unchanged except for presentation. +`/today` and `/tomorrow` routes call `filterForecastRunByDaySlice`. + +Filtering behavior: + +- nil runs remain nil; +- missing timezone means UTC; +- day selection is based on `forecastNow()` in the resolved timezone; +- periods are included when `period.StartTime` falls on the target local date; +- the run is shallow-copied and `Periods` is replaced with the filtered slice. + +The package variable `forecastNow` exists so endpoint tests can make day-slice +behavior deterministic. + +## Failure Behavior + +Binder failures become feedapi invalid-parameter responses. Handler service +errors are returned unchanged to feedapi for runtime normalization. Nil service +payloads are presented as nil data, so response rendering can produce +`data: null`. + +## Templates + +Endpoint definitions bind template names only. Template loading and rendering +belong to feedapi. Template files live under `templates/` and are operator +configuration through `templates.base_dir`. + +## Tests to Inspect Before Changing + +- `internal/adapters/inbound/httpapi/endpoints_test.go` +- `internal/adapters/inbound/httpapi/presenter/payload_test.go` when changing + presenter interaction +- `docs/api.md` when query behavior, route behavior, or payload behavior changes + +## Invariants + +- Preserve strict unknown-query rejection. +- Keep route-specific query policy in binder functions. +- Keep endpoint handlers thin: bind, call service, present, envelope. +- Keep day-slice filtering in the HTTP adapter, not in the repository. +- Preserve top-level `data` envelopes and nil-data behavior. diff --git a/docs/internal/postgres-repository.md b/docs/internal/postgres-repository.md new file mode 100644 index 0000000..783afdb --- /dev/null +++ b/docs/internal/postgres-repository.md @@ -0,0 +1,157 @@ +# Postgres Repository + +This document describes the outbound read adapter under +`internal/adapters/outbound/postgres`. It documents repository behavior and +storage assumptions without duplicating full schema documentation. + +## Purpose + +The Postgres repository implements `internal/app.Repository` against +weatherfeeder-owned tables. It reconstructs latest weather resources from SQL +rows and returns canonical weatherfeeder model values or application read +models. + +## Inputs and Outputs + +Inputs: + +- a primary `*sql.DB` selected by runtime composition; +- query contexts from application service calls; +- weatherfeeder-populated Postgres rows. + +Outputs: + +- latest observation, forecast, discussion, weather story, alert, and current + conditions read models; +- nil data with nil error when the latest resource does not exist; +- contextual errors for query, scan, iteration, and JSON decode failures. + +## Boundaries + +The repository may: + +- own SQL text and query ordering; +- scan rows into adapter-local row structs; +- map SQL nulls to pointers or omitted zero values; +- normalize timestamps to UTC; +- reconstruct child slices in database order. + +The repository must not: + +- bind HTTP query parameters; +- perform unit conversion or response rounding; +- convert timestamps to requested presentation timezones; +- render templates; +- create or migrate weatherfeeder tables. + +## Config Fields Used + +The repository does not read config directly. Runtime composition supplies the +primary `*sql.DB` selected from the first configured database entry. + +## External Adapters Used + +- `database/sql` for query execution and nullable scan types. +- `github.com/lib/pq` through runtime driver registration. +- `weatherfeeder/model` for canonical return values. +- `internal/app` for the repository interface and current-conditions model. + +## State + +The repository stores only a database handle. It owns no durable data, no +migrations, no polling state, and no retry queue. + +## Repository Contract + +`Repository` stores a `*sql.DB` and satisfies `app.Repository`. + +Every public method first checks that the repository and database are +configured. A nil repository or nil database returns `postgres repository is not +configured`. + +Missing parent rows map to `nil, nil`. This is how HTTP endpoints can return +successful responses with `data: null`. + +## Route-to-Query Mapping + +- `LatestObservation`: latest row from `observations`, then present-weather + rows from `observation_present_weather`. +- `CurrentConditions`: aggregates recent rows from `observations` using the + application-provided observation window. +- `LatestAlertRun`: latest row from `alert_runs`, then child `alerts` and + `alert_references`. +- `LatestHourlyForecast`: latest `forecasts` row where `product = 'hourly'`, + then child `forecast_periods`. +- `LatestNarrativeForecast`: latest `forecasts` row where + `product = 'narrative'`, then child `forecast_periods`. +- `LatestForecastDiscussion`: latest row from `forecast_discussions`, then + child `forecast_discussion_key_messages`. +- `LatestWeatherStoryRun`: latest row from `weather_story_runs`, then child + `weather_stories`. +- `LatestWeatherStory`: latest individual row from `weather_stories`. + +Latest parent rows are selected by descending weather timestamp and +`event_emitted_at` where that tie-breaker is available in the query. + +## Child Loading and Ordering + +Child queries preserve stored order: + +- observation present weather by `weather_index`; +- alerts by `alert_index`; +- alert references by `alert_index`, then `reference_index`; +- forecast periods by `period_index`; +- discussion key messages by `message_index`; +- weather stories by `story_index`. + +Alert references are attached after both alert and reference rows are loaded. +References are grouped by alert index and attached to their corresponding alert. + +## Null and Timestamp Policy + +Row structs use `sql.Null*` types for nullable columns. Mapper helpers convert: + +- invalid strings to empty strings; +- invalid booleans, floats, times, and WMO codes to nil pointers; +- valid times to UTC. + +Required parent timestamps are normalized to UTC directly in mappers. Optional +times go through `timePtr`, which also normalizes to UTC. + +Current conditions return nil when the aggregate sample count is zero. + +## JSON Decode Behavior + +Observation present-weather rows store raw JSON text. Empty or null text maps +to an empty present-weather value. Invalid JSON returns a contextual decode +error with the weather index. + +## Failure Behavior + +Repository methods wrap failures with operation context, for example: + +- `query latest observation` +- `query forecast periods` +- `scan forecast period row` +- `iterate weather story rows` +- `decode observation present weather row` + +This context should be preserved when adding new reads so operator logs and +tests identify the failing operation. + +## Tests to Inspect Before Changing + +- `internal/adapters/outbound/postgres/repository_test.go` +- `internal/app/service_test.go` for repository port expectations +- endpoint tests when no-data behavior or returned model shape affects HTTP + responses + +## Invariants + +- Keep SQL and row structs in the Postgres adapter. +- Keep weatherfeeder schema ownership outside `weatherapi`. +- Preserve `nil, nil` no-data behavior for missing latest parent rows. +- Preserve UTC normalization at the repository boundary. +- Preserve child ordering from stored index columns. +- Preserve contextual error wrapping for query, scan, iteration, and decode + failures. diff --git a/docs/internal/presenters.md b/docs/internal/presenters.md new file mode 100644 index 0000000..b9c832e --- /dev/null +++ b/docs/internal/presenters.md @@ -0,0 +1,143 @@ +# Presenters and Templates + +This document describes payload shaping in +`internal/adapters/inbound/httpapi/presenter` and its relationship to text +templates. Public response fields are documented in [`docs/api.md`](../api.md). + +## Purpose + +Presenters translate repository/application read models into response-ready +payloads. They own unit conversion, numeric rounding, timezone conversion, +optional field preservation, and copy semantics. + +## Inputs and Outputs + +Inputs: + +- weatherfeeder model values returned by the repository; +- `app.CurrentConditions` values returned by the application service; +- requested unit mode; +- requested precision; +- optional timezone location. + +Outputs: + +- JSON/XML/text-ready payload structs or canonical model copies; +- nil payloads for nil inputs. + +## Boundaries + +Presenters may: + +- convert metric values to US-customary response fields; +- round numeric values that are presentation values; +- convert timestamps to a requested timezone; +- copy slices and pointers before changing presentation values; +- add template-only helper fields when they are not serialized. + +Presenters must not: + +- execute SQL; +- call services or repositories; +- parse HTTP query parameters; +- load templates; +- change canonical repository models in place. + +## Config Fields Used + +Presenters do not read config directly. They receive unit, precision, and +timezone selections from HTTP binders. Text output depends indirectly on +`templates.base_dir` because feedapi loads templates from that configured +directory. + +## External Adapters Used + +- `weatherfeeder/model` for canonical weather payloads. +- `weatherfeeder/standards` for current-condition text from WMO codes. +- `internal/app` for the current-conditions read model. +- Feedapi templates indirectly consume presenter output during text rendering. + +## State + +Presenters are stateless. Helper functions allocate copied values and return +new payloads for each request. + +## Unit Conversion + +Metric mode generally returns weatherfeeder canonical model shapes or +metric-named fields. US mode uses explicit US response structs for observations +and forecasts and US-specific fields in current conditions. + +Conversion constants live in `constants.go`: + +- Celsius to Fahrenheit; +- kilometers per hour to miles per hour; +- meters to miles or feet; +- pascals to inches of mercury; +- millimeters to inches. + +Non-unit fields such as percentages, directions, text, IDs, and ordering values +keep their existing values. + +## Precision + +`roundedPtr` and `roundFloat` round numeric presentation values. Precision `0` +rounds to whole numbers. Positive precision uses powers of ten. Nil numeric +pointers remain nil. + +Latitude and longitude are copied but not rounded by forecast presenters. + +## Timezone Conversion + +`inLocationTime` and `inLocationTimePtr` convert timestamps only when a +timezone is supplied. Without a timezone, timestamps are preserved as returned +by the repository. + +Timezone conversion is applied by forecast, discussion, and weather story +presenters. Observations and current conditions do not currently receive +timezone input from their routes. + +## Optional Fields and Copy Semantics + +Presenter helpers copy pointer values before changing them. Slices are copied +before inclusion where needed. This preserves nil/omitempty behavior and avoids +mutating repository-returned values. + +Nil input payloads return nil. Endpoint handlers wrap those nil payloads in a +response envelope so renderers can produce `data: null`. + +## Endpoint Families + +- Observations: metric copy or US response shape, including present-weather + slice copy. +- Current conditions: single response shape with either metric or US unit + fields populated, plus a template-only day/night text helper. +- Alerts: pass-through of canonical alert runs, with nil preserved. +- Forecasts: metric copy or US response shape, period copy, unit conversion, + precision, and timezone conversion. +- Discussions: full or focused payload shapes, section copy, key-message copy, + timezone conversion. +- Weather stories: run/story copy and timezone conversion. + +## Templates + +Text templates consume the same envelope data produced by presenters. Template +files live under `templates/` and are loaded by feedapi from `templates.base_dir`. + +Presenter changes can break text output even when JSON and XML still compile. +Check template field references before renaming or removing presenter fields. + +## Tests to Inspect Before Changing + +- `internal/adapters/inbound/httpapi/presenter/payload_test.go` +- `internal/adapters/inbound/httpapi/endpoints_test.go` for rendered text and + envelope behavior +- affected template files under `templates/` + +## Invariants + +- Keep unit conversion and rounding out of handlers and repositories. +- Preserve nil pointer and optional field behavior. +- Copy before converting, rounding, or timezone-shifting values. +- Keep text-template helper fields out of JSON/XML when they are not API fields. +- Update `docs/api.md` and templates when payload shape changes. diff --git a/docs/internal/runtime.md b/docs/internal/runtime.md new file mode 100644 index 0000000..2c152e7 --- /dev/null +++ b/docs/internal/runtime.md @@ -0,0 +1,105 @@ +# Runtime Composition + +This document describes how the `weatherapi` executable wires configuration, +database handles, application services, HTTP endpoints, renderers, and shutdown. +It is development-facing; operator commands belong in +[`docs/operations.md`](../operations.md), and configuration fields belong in +[`docs/config.md`](../config.md). + +## Purpose + +`cmd/weatherapi/main.go` is the composition root. It should stay thin and only +connect already-implemented packages. Endpoint logic, SQL, presentation logic, +and business read behavior belong outside `cmd`. + +## Inputs and Outputs + +Inputs: + +- config path from `-config`, `WEATHERAPI_CONFIG`, or `config.yml`; +- feedapi YAML config containing `server`, `databases`, and `templates`; +- OS cancellation signals; +- database handles opened by feedapi. + +Outputs: + +- a configured feedapi HTTP server; +- registered `weatherapi` endpoint definitions; +- process logs for fatal startup errors and database close errors. + +## Composition Flow + +The executable: + +1. sets standard logger flags with microsecond precision; +2. resolves the config path; +3. creates a context canceled by `os.Interrupt` or `SIGTERM`; +4. loads config with `feedapi/config.Load`; +5. requires at least one configured database; +6. opens all configured databases with `feedapi/db.OpenAll`; +7. selects the first configured database name as the primary store; +8. constructs `postgres.Repository` with the primary `*sql.DB`; +9. constructs `app.Service` over the repository; +10. builds HTTP endpoint definitions with `httpapi.Definitions`; +11. constructs a feedapi app with the DB registry and endpoints; +12. starts feedapi with the signal-aware context. + +## Config Fields Used + +Runtime composition uses: + +- `server`: consumed by feedapi for HTTP runtime settings and default format; +- `databases`: opened by feedapi, with the first entry selected as primary; +- `templates`: consumed by feedapi for text-template loading. + +Do not duplicate the config field reference here. Keep it in +[`docs/config.md`](../config.md). + +## External Adapters Used + +- `feedapi/config`: YAML loading. +- `feedapi/db`: database registry and lifecycle. +- `feedapi/app`: HTTP runtime construction and startup. +- `internal/adapters/outbound/postgres`: weather read repository. +- `internal/adapters/inbound/httpapi`: endpoint definition registry. +- `github.com/lib/pq`: Postgres driver registration through blank import. + +## State and Lifecycle + +`weatherapi` owns no durable weather state. Runtime state is limited to loaded +configuration, database pools, endpoint definitions, renderer/template +registries managed by feedapi, and the running HTTP server. + +Database handles are closed with a deferred registry close. Close errors are +logged but do not change response behavior because they occur during shutdown. + +## Failure Behavior + +`run` wraps startup errors with operation context: + +- `load config` +- `config.databases requires at least one entry` +- `open databases` +- `select primary database` +- `build app` + +Errors returned by `a.Start(ctx)` are returned to `main`, which logs a fatal +`weatherapi failed: ...` message. Feedapi owns graceful HTTP shutdown after the +context is canceled. + +## Tests to Inspect Before Changing + +- `internal/app/service_test.go` for service wiring expectations. +- `internal/adapters/inbound/httpapi/endpoints_test.go` for endpoint registry + and runtime adapter expectations. +- Full `go test ./...` when runtime wiring, config behavior, or feedapi + integration changes. + +## Invariants + +- Keep `cmd/weatherapi` as composition code only. +- Preserve config path precedence: `-config`, `WEATHERAPI_CONFIG`, `config.yml`. +- Preserve first configured database as the primary weather store. +- Keep generic HTTP runtime behavior in feedapi. +- Keep endpoint definitions in the HTTP adapter. +- Keep SQL and row mapping in the Postgres adapter.