Bind and BindEmpty fill Req from path/query/json struct tags and validate once, extending the typed decode->validate->call->encode pipeline to routes with identifiers and filters. Conversion via builtins + encoding.TextUnmarshaler (uuid.UUID, time.Time); malformed value -> 400 naming the parameter; default: applies only when absent; repeated query -> slice; mis-tagged struct panics at wiring. Purely additive; existing adapters unchanged. Coordinated lockstep v1.6.0.
31 lines
3.1 KiB
Markdown
31 lines
3.1 KiB
Markdown
# ADR Index — web
|
||
|
||
Module-level architecture decisions for `code.nochebuena.dev/einherjar/web`.
|
||
|
||
For framework-wide decisions see the
|
||
[Einherjar docs repository](https://code.nochebuena.dev/einherjar/docs).
|
||
|
||
No module-level ADRs for v1.0.0 — all design decisions were consistent with
|
||
existing framework principles (ADR-001 through ADR-003 from `core`, framework
|
||
ADRs 001–004 from `docs`). No contested choices required a record.
|
||
|
||
Module ADRs:
|
||
|
||
| ADR | Title | Shipped |
|
||
|---|---|---|
|
||
| [ADR-001](ADR-001-request-binding.md) | `httputil` request binding — `Bind` / `BindEmpty` | v1.6.0 |
|
||
|
||
Decisions worth noting (not ADR-worthy individually):
|
||
|
||
| Decision | Outcome | Rationale |
|
||
|---|---|---|
|
||
| `RateLimiterStore` as interface (not concrete) | Interface only; `InMemoryRateLimiterStore` as default | Allows `cache-valkey` to satisfy it via duck typing without cross-module import |
|
||
| Fail-open rate limiting | Request allowed on store error | Availability preferred over hard enforcement during infrastructure degradation |
|
||
| `last_seen` excluded | Not in v1.0.0 | Application-domain concern; not transport-level middleware |
|
||
| UUID v7 for request IDs | v7 with v4 fallback | Time-ordered IDs sort chronologically in logs; fallback ensures generation never fails |
|
||
| `observability.Checkable` not redefined | Imported from contracts | Starters implement contracts directly; no web import needed by db/cache starters |
|
||
| Background goroutine for in-memory eviction | `time.Ticker` goroutine | Avoids `worker` module dependency; in-memory store is self-contained |
|
||
| `mw.RequestIDFrom` resolver sees the request (v1.4.0) | New entry point takes `func(*http.Request) string`; `RequestID` becomes a request-ignoring wrapper over it | An inbound correlation id must be able to survive this boundary, but acceptability is per-service — a typed audit column rejects what an opaque log accepts. The framework provides plumbing only (context + header, once per request); the app owns policy (which header, validation, generation fallback). An empty resolver result attaches nothing rather than a silently-empty value |
|
||
| `httputil` success status is configurable (v1.5.0) | `Handle`/`HandleNoBody`/`HandleEmpty` take `opts ...Option`; `WithStatus(code)` overrides the default (200 / 200 / 204) | 201 Created / 202 Accepted are common and were only reachable by hand-rolling the handler (losing decode+validate+error-mapping). Variadic options are non-breaking and future-extensible (headers, etc.). `WithStatus` is success-only (2xx) and panics at wiring on a non-2xx code — a wrong status is a routing mistake that should fail to boot, not surface at runtime; error status stays separate, resolved from the xerror by `Error` |
|
||
| `httputil` request binding (v1.6.0) | `Bind`/`BindEmpty` fill `Req` from `path:`/`query:`/`json:` tags and validate once; see [ADR-001](ADR-001-request-binding.md) | Path/query were only reachable via `HandlerFunc`, the one route to production with no validation (unvalidated bounds; client mistakes as 500s). `encoding.TextUnmarshaler` is the whole extensibility story (uuid/time, no new dep). Mis-tagged struct panics at wiring. Purely additive |
|