Skip to content

feat: spec-level Postel's-Law tolerant readers + stable enum constants#25

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

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

Conversation

@caballeto
Copy link
Copy Markdown
Member

Summary

The Terraform provider now decodes response-DTO multi-value enums as
plain string. So when the API adds a new monitor type, alert channel
kind, or incident status, the provider's Read() keeps working —
terraform refresh won't surface the resource as drift just because
a downstream API value is unknown to this provider build.

Authoring stays strict: terraform plan still rejects unknown values
on Create/Update (sourced from generated subtype-tag constants in
internal/api/enums.go).

Implementation

Three pieces:

  1. Spec-level relaxationscripts/preprocess.mjs (vendored copy
    of mono's @devhelm/openapi-tools) runs relaxResponseEnumsInSpec
    before oapi-codegen sees the spec. Response-DTO enum fields land
    in internal/generated/types.go as plain string.

  2. scripts/oapi-codegen.yaml (new) — pins
    always-prefix-enum-values: true and
    resolve-type-name-collisions: true so the constants
    oapi-codegen emits stay stable across spec evolution. Without
    these, oapi-codegen silently drops the MonitorDtoType prefix
    when a single-value enum collapses, breaking compile in
    downstream code.

  3. internal/api/enums.go — input-side OneOf slices are
    sourced from generated subtype-tag constants (e.g.
    MonitorAuthConfigBasicAuthConfigTypeBasic) walked at init time,
    so adding a new spec value auto-extends validation without
    hand edits.

internal/api/validate.go is documented as the Go counterpart to
Zod safeParse (sdk-js) and Pydantic model_validate (sdk-python):
runtime response validation driven by the spec. The function now
skips multi-value-enum Valid() checks on response DTOs (those are
string after relaxation) and only fires on single-value
discriminator tags — exactly the desired Postel behaviour.

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

Tests

  • go vet ./... clean.
  • go build ./... clean.
  • go test ./... — all packages green.
  • enums_coverage_test.go rewritten to walk constants by
    typeNameSuffix, so new spec values are auto-covered.
  • Negative tests around response-side enum rejection flipped to
    assert acceptance (Postel). Input-side stringvalidator.OneOf
    negative tests stay strict.
  • Codegen is deterministic: double-regen produces bit-identical
    internal/generated/types.go.

Test plan

  • CI green.
  • terraform-provider-devhelm acceptance tests pass against
    the test API stack.
  • Verify internal/generated/types.go: response DTOs (e.g.
    MonitorDto.type) are typed string; input DTOs (e.g.
    CreateMonitorRequest.type) keep typed MonitorType.

Made with Cursor

Vendored `scripts/preprocess.mjs` is in lockstep with mono's
`@devhelm/openapi-tools` and runs `relaxResponseEnumsInSpec` before
`oapi-codegen` consumes the spec. Response-DTO multi-value enum
fields lose their typed wrapper (`MonitorDtoType`, `IncidentDtoStatus`,
…) and land in `internal/generated/types.go` as plain `string`. So
when the API adds a new monitor type, alert channel kind, or incident
status, the provider's `Read()` keeps working — `terraform refresh`
won't surface the resource as drift just because a downstream API value
is unknown to this provider build.

Authoring stays strict: input validation on resource Create/Update
flows through `stringvalidator.OneOf(allMonitorTypes...)` slices in
`internal/api/enums.go`, which are sourced from generated subtype-tag
constants (e.g. `MonitorAuthConfigBasicAuthConfigTypeBasic`). So a
typo in `terraform plan` still fails plan-time, not at the API call.

`scripts/oapi-codegen.yaml` (new) pins `always-prefix-enum-values: true`
+ `resolve-type-name-collisions: true` so the constants oapi-codegen
emits stay stable across spec evolution. Without this, oapi-codegen
silently drops the `MonitorDtoType` prefix when a single-value enum
collapses, breaking compile.

`internal/api/validate.go` is documented as the Go equivalent of Zod
`safeParse` (sdk-js) and Pydantic `model_validate` (sdk-python) —
runtime response validation driven by the spec, with zero hand-written
per-DTO code. The function now skips multi-value-enum `Valid()` checks
on response DTOs (those are `string` after relaxation) and only fires
on single-value discriminator tags.

`enums_coverage_test.go` walks all generated constants by their
`typeNameSuffix` (e.g. `Type`, `Status`, `ChannelType`) so adding a
new spec value automatically extends coverage without test churn.

Negative tests around response-side enum rejection are flipped to
assert tolerance. Negative tests around input-side validation
(stringvalidator.OneOf) stay strict.

See `mini/runbooks/api-contract.md` § 3.3 for the cross-surface design.

Coverage: `go vet ./...`, `go build ./...`, `go test ./...` all green.
Co-authored-by: Cursor <cursoragent@cursor.com>
@caballeto caballeto merged commit ca94c6c into main May 11, 2026
6 checks passed
@caballeto caballeto deleted the feat/postel-tolerant-readers branch May 11, 2026 19:42
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