Add internal architecture documentation
This commit is contained in:
151
docs/internal/http-adapter.md
Normal file
151
docs/internal/http-adapter.md
Normal 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.
|
||||
157
docs/internal/postgres-repository.md
Normal file
157
docs/internal/postgres-repository.md
Normal 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
143
docs/internal/presenters.md
Normal 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
105
docs/internal/runtime.md
Normal 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.
|
||||
Reference in New Issue
Block a user