docs: spec-follows-runtime naming decision + casing wart (#198)
- api-conventions.md §5a: runtime Newtonsoft casing vs generated spec, the schema transformer that mirrors it, and the contract test guarding it. - decisions.md: append the "wire format is source of truth; spec follows via the real contract resolver" decision. - spa-conventions.md §4: trust the generated key casing; note the removed troubleshooting escape hatch and runtime-cased test mocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,31 @@ gets this wrong. The SPA works around it with a manual override type; see `web/s
|
||||
`PlayoutAlternateSchedule`, `PlayoutTemplate`, and their request types). Copy this pattern for any
|
||||
new DTO with a `DayOfWeek` (or `DayOfWeek[]`) member — don't trust the generated numeric type.
|
||||
|
||||
### 5a. Runtime JSON casing vs the generated spec (the `ffmpegProfileId` wart)
|
||||
|
||||
Runtime `/api/*` JSON is serialized by **Newtonsoft** (`AddNewtonsoftJson` in `Startup.cs`), using
|
||||
`ErsatzTV/Serialization/CustomContractResolver.cs` → `CustomNamingStrategy` (camelCase **plus** a
|
||||
special case mapping any `FFmpegProfileId` member to `"ffmpegProfileId"`, and honoring any
|
||||
`[JsonProperty("...")]` attribute, e.g. `ChannelResponseModel.FFmpegProfile` →
|
||||
`[JsonProperty("ffmpegProfile")]`). The OpenAPI document, however, is generated from
|
||||
**System.Text.Json** metadata, whose camelCase can differ (it emitted `fFmpegProfileId` /
|
||||
`fFmpegProfile`). That drift silently gave the SPA the wrong key to read (issue #198).
|
||||
|
||||
Fix (do not remove): `ErsatzTV/Serialization/NewtonsoftSchemaNamingTransformer.cs` is an OpenAPI
|
||||
**schema transformer** registered on all three documents (`options.AddSchemaTransformer(...)` in
|
||||
`Startup.cs`). For each object schema it resolves the CLR type's Newtonsoft `JsonObjectContract`
|
||||
through the *same* `CustomContractResolver` the runtime uses and renames `schema.Properties` (and
|
||||
`schema.Required`) keys to the exact names Newtonsoft would emit. This mirrors the wire format **by
|
||||
construction**, so future naming-strategy special cases or `[JsonProperty]` renames can't drift.
|
||||
|
||||
Guard: `ErsatzTV.Tests/Controllers/OpenApiSerializerContractTests.cs` serializes fully-populated
|
||||
DTOs (ChannelViewModel, FFmpegSettingsResponseModel, WatermarkViewModel,
|
||||
MediaItemInfoResponseModel) through the runtime Newtonsoft settings and asserts the emitted top-level
|
||||
keys equal the corresponding `v1.json` schema's property set. It fails if spec generation ever drifts
|
||||
from the MVC serializer again. Note: only exact-match `FFmpegProfileId` gets the special case —
|
||||
`DefaultFFmpegProfileId` stays `defaultFFmpegProfileId` under both serializers, and non-acronym or
|
||||
single-leading-cap names (`fFmpegPath`, `fFprobePath`, `zIndex`, `rFrameRate`) already agree.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- **Controller tests**: `ErsatzTV.Tests/Controllers/<Domain>ControllerTests.cs`. NUnit + Shouldly +
|
||||
|
||||
@@ -127,3 +127,19 @@ Sample** (alongside Download Results) while a troubleshooting session is startin
|
||||
only gated Download Results. Both downloads compete with the live transcode for I/O and the sample
|
||||
archiver reads the same media file, so gating both during a session is strictly safer and costs
|
||||
nothing (sessions are short).
|
||||
|
||||
## 2026-07-09 — OpenAPI spec mirrors the runtime Newtonsoft serializer (#198)
|
||||
|
||||
The generated OpenAPI document is made to follow the **runtime** JSON contract, not the reverse. Runtime
|
||||
`/api/*` responses are serialized by Newtonsoft via `CustomContractResolver`/`CustomNamingStrategy`
|
||||
(camelCase + a `FFmpegProfileId`→`ffmpegProfileId` special case + `[JsonProperty]` overrides such as
|
||||
`ChannelResponseModel.FFmpegProfile`→`ffmpegProfile`), while `Microsoft.AspNetCore.OpenApi` generates the
|
||||
spec from System.Text.Json metadata, whose camelCase drifted (`fFmpegProfileId`, `fFmpegProfile`). That
|
||||
drift fed the SPA the wrong key. Rather than hand-patch the spec or change the wire format (breaking clients),
|
||||
we added `NewtonsoftSchemaNamingTransformer` — an OpenAPI schema transformer registered on all three
|
||||
documents that renames each schema property through the *same* Newtonsoft contract resolver the runtime uses,
|
||||
so the spec matches the wire format by construction. A contract test
|
||||
(`OpenApiSerializerContractTests`) serializes representative DTOs through the real runtime settings and pins
|
||||
the spec property sets to them. Decision: **the wire format is the source of truth; the spec follows it via the
|
||||
real contract resolver.** This also fixed a latent SPA bug (the channel-list "FFmpeg profile" column read
|
||||
`fFmpegProfile` and always showed "Unassigned"). Issue #198.
|
||||
|
||||
@@ -76,6 +76,15 @@ One file per domain in `web/src/api/`, e.g. `logs.ts`, `blocks.ts`, `playouts.ts
|
||||
- `web/src/api/index.ts` re-exports everything so screens import from `'../api'`, not from the
|
||||
individual domain file directly.
|
||||
|
||||
**Trust the generated key casing — it mirrors the runtime.** Since #198 the OpenAPI spec is generated
|
||||
to match the runtime Newtonsoft serializer exactly (a schema transformer runs the same contract
|
||||
resolver; see `api-conventions.md` §5a), so the generated types carry the real wire keys — including
|
||||
Newtonsoft's acronym quirks like `ffmpegProfileId` (channel FFmpeg-profile id) and `ffmpegProfile`
|
||||
(channel FFmpeg-profile display name). **Do not** hand-cast responses to "fix" a key or dual-read a
|
||||
spec-cased vs runtime-cased key (the old `PlaybackTroubleshootingScreen` `#198` escape hatch that read
|
||||
`data.channel.fFmpegProfileId` has been removed — read `ffmpegProfileId` straight off the typed
|
||||
response). When you mock an API response in a test, use the generated (runtime) casing.
|
||||
|
||||
## 5. Artwork rendering
|
||||
|
||||
Render `item.artwork` / `item.poster` (or whatever the DTO field is named) **directly as an `<img
|
||||
|
||||
Reference in New Issue
Block a user