Add internal architecture documentation

This commit is contained in:
2026-06-11 14:19:51 +00:00
parent ec027e34cc
commit 9fe480d3ab
4 changed files with 556 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

143
docs/internal/presenters.md Normal file
View File

@@ -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.

105
docs/internal/runtime.md Normal file
View File

@@ -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.