feat: enforce the document's declared bounds at plan time - #94
Merged
Conversation
An API that silently clamps or truncates an out-of-range value leaves state and config disagreeing with no error to explain it. The bounds the document declares now become validators, so the configuration fails at plan time instead. Length is measured in characters: JSON Schema counts maxLength in characters and the framework's LengthBetween counts bytes, which would refuse a valid value the moment it stopped being ASCII. A pattern is compiled at generation time and dropped when Go cannot take it. OpenAPI patterns are ECMA-262 and Go's regexp is RE2, which has no lookahead; an expression RE2 rejects would panic the generated provider inside MustCompile at package initialisation, where go build cannot see it. A fractional bound on an integer attribute is dropped rather than truncated, which would silently move the boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
mapping.md row 9. An API that accepts an out-of-range value and silently stores
it clamped or truncated leaves state and config disagreeing with no error to
explain it. The bounds the document already declares — carried into the IR by
#91 — now become validators, so the configuration fails at plan time instead of
diverging at apply time.
Before this,
Validators:appeared 1,766 times across the pilots and wasentirely
stringvalidator.OneOf.What is emitted
minLength/maxLengthstringvalidator.UTF8LengthBetween/AtLeast/AtMostpatternstringvalidator.RegexMatchesminimum/maximumint64validator.Between/AtLeast/AtMostminimum/maximumfloat64validator.…minItems/maxItemslistvalidator.SizeBetween/…minItems/maxItemsmapvalidator.SizeBetween/…Each is a
code.CustomValidatorcarrying its own import, so it cannot reach afile whose import block forgot it —
TestUnit_ConstraintValidators_DeclareTheirOwnImportsholds that.
Three decisions worth reviewing
Length is counted in characters, not bytes. JSON Schema defines
maxLengthin characters. The framework offers both:
LengthBetweenuseslen()andUTF8LengthBetweenusesutf8.RuneCountInString. Using the byte-counting onewould refuse a valid value the moment it stopped being ASCII, so this emits the
UTF8Length*family.A pattern is compiled at generation time and dropped when Go cannot take it.
OpenAPI patterns are ECMA-262; Go's
regexpis RE2, which has no lookahead andno backreferences. An expression RE2 rejects would panic the generated provider
inside
regexp.MustCompileat package initialisation — before any plan ortest runs, and somewhere
postcheck'sgo buildcannot see it. So the patternis compiled here first and yields no validator if it fails, while the length
bound beside it still stands.
On these three documents nothing was actually dropped: all 25 pattern-carrying
attributes compiled. The guard is defensive, and
TestUnit_ConstraintValidators_SkipAPatternRE2CannotCompileproves it with^(?=.*[A-Z]).{8,}$.A fractional bound on an integer attribute is dropped, not truncated.
minimum: 1.5on an integer describes a value the attribute cannot hold;truncating to 1 would silently move the boundary. The integral bound beside it
survives.
A length or range declared on an object is ignored — it describes something
the attribute does not hold. A size bound on a nested list still applies,
because the list is the thing being sized.
Measured effect
Refusals unchanged on all three pilots — 817 / 290 / 343,
provider verifyreports no drift,
postcheck(go mod tidy,go build,go vet) passes. Thisadds validators to attributes that already generated.
448 constraint validators where there were none:
handoff.md:98recordsLengthBetween 0, RegexMatches 0, int64validator.Between 0; all three are now non-zero.mapvalidatorstays at 0 because no pilotdeclares
minItemson a map.Gates
gofmt -l internal cmd— emptyscripts/repo_hygiene_gate.sh— passgolangci-lint run— 0 issuesgo build ./... && go vet ./...— passgo test ./...— pass, coverage 91.4% totalprovider generate+provider verifyon all three — no driftNew file
internal/emit/render_constraints.gowith 5 tests inrender_constraints_test.go.Unrelated, and noted rather than fixed
The github corpus pin is stale and will keep drifting: its
upstreamUrlpoints at
github/rest-api-description/main/, a moving branch ref(pinned
80850db2…, served81c0ff2b…). No test reads that document today —specmodelandintermediate_representationboth fetch onlythousandeyes—so CI is green, but the first test that reads it fails immediately.
corpus.Describeris also still never installed, so the nextRewriteLockrunwrites
"unparsed", 0, 0. Both want their own PR.🤖 Generated with Claude Code