Files
weatherapi/docs/policy/architecture.md
Eric Rakestraw 3ec0a9bd84
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
Refresh documentation cross-links
2026-06-11 14:25:42 +00:00

12 KiB

Architecture Policy

Purpose

This document defines weatherapi's development architecture and invariants for maintainers and LLM coding agents. It describes how the implemented system is built and how future changes should preserve its boundaries.

This is an inward-facing policy document. Consumer-facing HTTP contract details belong in docs/api.md, and proposed or unimplemented work belongs under docs/roadmap/.

Project Shape

weatherapi is a small Go HTTP API that serves weather data previously persisted by weatherfeeder. It does not poll upstream weather providers. It reads weatherfeeder-owned Postgres tables, maps rows back into weatherfeeder canonical model types or API-specific read models, and presents those resources through HTTP endpoints.

The implemented runtime flow is:

  1. cmd/weatherapi resolves the config path from -config, then WEATHERAPI_CONFIG, then config.yml.
  2. Feedapi loads YAML configuration for the HTTP server, database handles, and template directory.
  3. The command opens configured databases and selects the first database as the primary weather data store.
  4. The Postgres repository is constructed against the primary database.
  5. The application service is constructed over the repository read port.
  6. HTTP endpoint definitions are registered with feedapi.
  7. Feedapi handles routing, middleware, format negotiation, template rendering, and graceful shutdown.
  8. Each request binds query parameters, calls an application use case, presents the result, and returns a data response envelope.

Core application code lives in internal/app. Inbound HTTP adapters live under internal/adapters/inbound/httpapi. Outbound Postgres reads live under internal/adapters/outbound/postgres. Text response templates live in templates/.

Core Design Principles

  • Hexagonal boundaries: HTTP, configuration, database handles, SQL, rendering, and templates are adapters around application read use cases.
  • Read-only API posture: weatherapi reads persisted weather data; ingestion and table ownership belong to weatherfeeder.
  • Explicit ports: internal/app.Repository is the outbound read port, and internal/adapters/inbound/httpapi.Service is the inbound service contract used by handlers.
  • Thin composition root: cmd/weatherapi wires config, databases, repository, service, endpoints, and feedapi runtime; it should not contain endpoint, SQL, or presentation logic.
  • Adapter-local policy: query validation belongs at the HTTP boundary; SQL row shape and null handling belong in the Postgres adapter; unit and timezone presentation belongs in presenter helpers.
  • Current-behavior docs: outside roadmap files, document only implemented routes, config, storage assumptions, and runtime behavior.
  • Standard-library-first: use the Go standard library where reasonable, with narrow dependencies for framework/runtime integration and database access.

Architectural Boundaries

Core/application logic:

  • internal/app defines read use cases, repository ports, and API-specific read models such as current conditions.
  • Application code may depend on canonical weatherfeeder/model types because those are the persisted weather domain contract.
  • Application code should not import HTTP, feedapi endpoint/render packages, SQL packages, or template packages.

Inbound HTTP adapter:

  • internal/adapters/inbound/httpapi owns route definitions, query binding, request validation, day-slice filtering for forecast routes, and endpoint-specific handler wiring.
  • internal/adapters/inbound/httpapi/presenter owns response payload shaping, metric/US conversions, rounding, timestamp timezone conversion, and copy semantics.
  • Endpoint handlers should call the Service interface and return response.Envelope{Data: ...}. They should not execute SQL directly.

Outbound Postgres adapter:

  • internal/adapters/outbound/postgres owns SQL text, row structs, row scanning, null conversion, UTC normalization, and reconstruction of nested canonical payloads from weatherfeeder tables.
  • Repository methods return nil, nil for missing latest rows so HTTP responses can render data: null.
  • The adapter assumes the weatherfeeder Postgres schema exists. It does not create or migrate weatherfeeder tables.

Runtime composition:

  • cmd/weatherapi/main.go owns process wiring: config path selection, config loading, DB registry, primary DB selection, repository/service construction, endpoint registration, signal cancellation, and logging.
  • Feedapi owns the generic HTTP runtime: routing, renderers, middleware, database registry lifecycle, and graceful server shutdown.

Modules or Stages

The implemented request path has five main stages: route matching, query binding, service call, presentation, and rendering.

Query binding contract:

  • Normalize common format, units, and precision query values where supported.
  • Reject unknown query parameters on implemented endpoints.
  • Keep endpoint-specific query rules in endpoint binders.
  • Parse timezone only for route families that support tz / TZ.
  • Return typed feedapi errors so invalid input becomes a structured 400 Bad Request response.

Service contract:

  • Keep service methods narrow and resource-specific.
  • Delegate simple latest-resource reads directly to the repository.
  • Keep use-case defaults in internal/app; current conditions use ObservationWindowMinutesDefault.
  • Do not let transport or SQL details leak into service method signatures.

Presenter contract:

  • Treat repository-returned models as immutable input.
  • Copy payloads before unit conversion, rounding, or timezone conversion.
  • Preserve nil pointers and optional fields so JSON/XML omitempty behavior remains meaningful.
  • Keep metric and US field naming explicit and tested.
  • Keep text templates conditional so absent optional values do not render misleading zero values.

Repository contract:

  • Query the latest parent row for each resource, then load child rows in stored order where needed.
  • Map SQL nulls to nil pointers or zero-value omitted fields consistently.
  • Normalize database timestamps to UTC before returning models.
  • Wrap query, scan, iteration, and decode errors with operation context.

State, Inputs, and Outputs

Inputs are HTTP requests and Postgres rows written by weatherfeeder.

Outputs are HTTP responses in JSON, XML, or text format. All public endpoint handlers return a top-level data envelope; missing latest data is represented as data: null.

weatherapi owns no durable weather state. Its runtime state is limited to process memory, loaded configuration, HTTP server state, template registry, renderer registry, and database connection pools. Durable weather data and schema creation are external concerns owned by weatherfeeder and Postgres.

The API currently serves latest-resource views: latest observation, current conditions, latest active alerts run, latest hourly forecast, latest narrative forecast, latest forecast discussion, latest weather story run, and latest individual weather story. Forecast today and tomorrow routes derive filtered copies from the latest run.

Configuration and CLI Boundaries

The executable supports one CLI flag:

  • -config: path to the YAML config file.

If -config is not provided, the default comes from WEATHERAPI_CONFIG; if that environment variable is empty, the default is config.yml.

Configuration shape is provided by feedapi and includes:

  • server: listen address, default format, and HTTP timeouts;
  • databases: named database handles;
  • templates: template base directory.

cmd/weatherapi requires at least one configured database and uses the first database as the primary weather repository. Template loading is handled by feedapi; text output is available when the configured template directory is present and loaded.

Do not duplicate full CLI or config reference material here. If docs/cli.md or docs/config.md are added or updated, they should be the canonical user/operator references.

Errors, Logging, and Diagnostics

cmd/weatherapi uses the standard library log package with timestamps and microseconds.

Startup failures are fatal and include operation context such as config loading, database opening, primary database selection, app construction, or server startup. Database close errors during shutdown are logged.

Feedapi handles transport-level diagnostics:

  • bind errors become structured invalid-parameter responses;
  • unsupported formats become structured unsupported-format responses;
  • handler errors are normalized before response rendering;
  • recovery, request ID, and timing middleware are installed by default.

Repository methods should distinguish no data from errors. sql.ErrNoRows maps to nil, nil; query, scan, iteration, and JSON decode failures should return contextual errors.

The daemon handles os.Interrupt and SIGTERM through signal.NotifyContext. Feedapi starts http.Server.ListenAndServe and performs graceful shutdown when the context is canceled.

Testing Expectations

When changing behavior, inspect or add focused tests in the owning package.

Expected coverage by change type:

  • App service: repository delegation, use-case defaults, filtering or aggregation rules, and error propagation.
  • HTTP adapter: route registration, query validation, supported/rejected parameters, format negotiation, error envelopes, null data, day-slice behavior, and timezone behavior.
  • Presenter: metric/US conversion, rounding, timezone conversion, optional field omission, nil handling, copy semantics, and text-template-sensitive fields.
  • Postgres adapter: SQL row mapping, nullable handling, UTC normalization, child ordering, missing rows, and nested payload reconstruction.
  • Runtime/config: config path behavior, database selection, template loading, and endpoint registration when those surfaces change.

Use fakes, httptest, local renderers/templates, and row-mapping fixtures rather than live services. Full-package tests should remain fast and deterministic.

Dependency Policy

Prefer the Go standard library for HTTP helpers, time handling, logging, tests, conversions, and small utilities.

Accepted project-level dependencies are narrow and purposeful:

  • feedapi provides config loading, HTTP runtime, endpoint definitions, renderers, templates, middleware, and database registry behavior.
  • weatherfeeder provides canonical weather model and standards types that match the persisted data contract.
  • github.com/lib/pq provides the Postgres driver.

Do not add dependencies for small conveniences. Do not let dependency-specific types cross application boundaries unless that dependency is the explicit adapter contract.

Documentation Expectations

Documentation must follow docs/policy/documentation.md.

Rules for architecture-related docs:

  • Current-behavior docs must describe implemented behavior only.
  • Unimplemented work belongs only under docs/roadmap/ until implemented.
  • Prefer links to canonical docs over repeated reference material.
  • Update docs in the same change when modifying public routes, query behavior, config behavior, runtime behavior, presenter payloads, or database read assumptions.

Architectural Invariants

  • Keep cmd/weatherapi as composition code, not endpoint, SQL, or presentation logic.
  • Keep application read ports in internal/app independent of HTTP and SQL packages.
  • Keep HTTP route definitions and query binding in the inbound adapter.
  • Keep unit conversion, rounding, timezone presentation, and payload copy behavior in presenters.
  • Keep SQL text, row structs, and null handling in the Postgres adapter.
  • Preserve data envelope responses and data: null semantics for missing latest rows.
  • Preserve UTC normalization at the repository boundary and timezone conversion at the presentation boundary.
  • Preserve strict query-parameter rejection unless an endpoint explicitly allows a parameter.
  • Preserve text templates as adapter presentation assets, not business logic.
  • Do not make weatherapi responsible for ingesting upstream weather data or creating weatherfeeder tables.

Non-Goals

  • weatherapi is not a weather ingester and does not call NWS, Open-Meteo, OpenWeather, or SPC provider APIs.
  • weatherapi does not own weatherfeeder's database schema creation or migrations.
  • weatherapi does not provide historical browsing except where an implemented endpoint explicitly derives a filtered view from the latest run.
  • weatherapi is not a general-purpose API framework; generic HTTP runtime behavior belongs to feedapi.
  • Architecture policy is not a public endpoint, CLI, or config reference.