Skip to content

feat: spec-level Postel's-Law tolerant readers + codegen-stable enum aliases#29

Merged
caballeto merged 1 commit into
mainfrom
feat/postel-tolerant-readers
May 11, 2026
Merged

feat: spec-level Postel's-Law tolerant readers + codegen-stable enum aliases#29
caballeto merged 1 commit into
mainfrom
feat/postel-tolerant-readers

Conversation

@caballeto
Copy link
Copy Markdown
Contributor

Summary

devhelm SDK now decodes response-DTO multi-value enums as plain
strMonitorDto.type, IncidentDto.status, etc. accept future
API values added after this SDK version was built. Request DTOs keep
their StrEnum classes and Field(discriminator=…) wiring for strict
authoring-time validation.

Implementation

Two layers:

  1. Spec-level relaxationscripts/typegen.sh runs the shared
    @devhelm/openapi-tools preprocessor (relaxResponseEnumsInSpec)
    before datamodel-codegen sees the spec. Response-DTO enum fields
    land in _generated.py as plain str annotations.

  2. Stable public enum aliasesscripts/emit_response_enums.py
    (new) reads the un-relaxed spec and emits src/devhelm/_enums.py
    with one Literal[...] alias per (SchemaName, propertyName)
    pair. Names are stable across spec evolution because they don't
    depend on datamodel-codegen's suffixed names
    (Status1Status15, Type1Type6) which used to shift on
    every spec change.

types.py is rewritten to import every public alias
(IncidentStatus, MonitorType, CustomDomainStatus,
ConfirmationPolicyType, …) from _enums.py. The public API surface
is unchanged.

Customer impact: the public type names are unchanged but they are
now Literal[...] aliases rather than StrEnum classes. Type
narrowing behavior is equivalent for static checkers; runtime
comparisons like x == AlertSensitivity.ALL need to be rewritten
to plain string comparisons (x == \"ALL\").

Cross-surface design: mini/runbooks/api-contract.md § 3.2.

Tests

  • 742 / 742 tests pass.
  • ruff check clean, ruff format --check clean.
  • mypy src/ clean (26 source files).
  • Codegen is deterministic: double-regen produces bit-identical
    _generated.py and _enums.py.

Test plan

  • CI green.
  • Verify in src/devhelm/_generated.py: response DTOs (e.g.
    MonitorDto.type) are typed str; request DTOs (e.g.
    CreateMonitorRequest.type) keep their StrEnum field types.
  • Verify in src/devhelm/_enums.py: 164 aliases including
    MonitorDtoType, IncidentDtoStatus, ConfirmationPolicyType.

Made with Cursor

…aliases

Pipeline rewrite. `scripts/typegen.sh` now runs through the shared
`@devhelm/openapi-tools` preprocessor (which calls
`relaxResponseEnumsInSpec`) before `datamodel-codegen` sees the spec.
Response-DTO multi-value enum fields decode as plain `str` — so
`MonitorDto.type`, `IncidentDto.status`, etc. accept future API values
added after this SDK version was built. Request DTOs keep their
StrEnum classes and `Field(discriminator=…)` wiring for strict
authoring-time validation.

`scripts/emit_response_enums.py` (new) emits `_enums.py` from the
*un-relaxed* spec, with one `Literal[...]` alias per
`(SchemaName, propertyName)` pair (164 aliases). Names are stable
across spec evolution because they don't depend on
`datamodel-codegen`'s suffixed names (`Status1`…`Status15`,
`Type1`…`Type6`) which shifted on every spec change.

`types.py` now imports every public enum alias (`IncidentStatus`,
`MonitorType`, `CustomDomainStatus`, `ConfirmationPolicyType`, …)
from `_enums.py` so the public API stays stable while the runtime
stays Postel-tolerant. Hand-coded `ConfirmationPolicyType` literal
removed in favour of the auto-generated alias (caught a wrong value
in the process — the literal is `"multi_region"`, not
`"confirmation"`).

Negative tests around response-DTO enum rejection are flipped to
assert acceptance (tolerance). Request-DTO negative tests stay
strict.

See `mini/runbooks/api-contract.md` § 3.2 for the cross-surface
design and `_enums.py` rationale.

Coverage: 742 / 742 tests pass; ruff + mypy clean (26 source files).
Co-authored-by: Cursor <cursoragent@cursor.com>
@caballeto caballeto merged commit 47a238c into main May 11, 2026
4 checks passed
@caballeto caballeto deleted the feat/postel-tolerant-readers branch May 11, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant