feat: spec-level Postel's-Law tolerant readers + stable enum constants#25
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Terraform provider now decodes response-DTO multi-value enums as
plain
string. So when the API adds a new monitor type, alert channelkind, or incident status, the provider's
Read()keeps working —terraform refreshwon't surface the resource as drift just becausea downstream API value is unknown to this provider build.
Authoring stays strict:
terraform planstill rejects unknown valueson Create/Update (sourced from generated subtype-tag constants in
internal/api/enums.go).Implementation
Three pieces:
Spec-level relaxation —
scripts/preprocess.mjs(vendored copyof mono's
@devhelm/openapi-tools) runsrelaxResponseEnumsInSpecbefore
oapi-codegensees the spec. Response-DTO enum fields landin
internal/generated/types.goas plainstring.scripts/oapi-codegen.yaml(new) — pinsalways-prefix-enum-values: trueandresolve-type-name-collisions: trueso the constantsoapi-codegen emits stay stable across spec evolution. Without
these, oapi-codegen silently drops the
MonitorDtoTypeprefixwhen a single-value enum collapses, breaking compile in
downstream code.
internal/api/enums.go— input-sideOneOfslices aresourced 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.gois documented as the Go counterpart toZod
safeParse(sdk-js) and Pydanticmodel_validate(sdk-python):runtime response validation driven by the spec. The function now
skips multi-value-enum
Valid()checks on response DTOs (those arestringafter relaxation) and only fires on single-valuediscriminator 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.gorewritten to walk constants bytypeNameSuffix, so new spec values are auto-covered.assert acceptance (Postel). Input-side
stringvalidator.OneOfnegative tests stay strict.
internal/generated/types.go.Test plan
terraform-provider-devhelmacceptance tests pass againstthe test API stack.
internal/generated/types.go: response DTOs (e.g.MonitorDto.type) are typedstring; input DTOs (e.g.CreateMonitorRequest.type) keep typedMonitorType.Made with Cursor