Documentation
¶
Overview ¶
Package validation — the ordered-value bounds constraints.
Package validation — range 0.3.47.* (ADR 0046 service/validation block).
Package validation — the struct-tag compiler. Everything here runs once per type, at construction; nothing here runs during a validation.
Package validation — the descent combinators. These are what make a violation LOCATED on a nested structure, and they do it with an accessor function rather than reflection: the programmatic path never imports reflect, so a hot validator is a chain of direct field reads the compiler can inline.
Package validation — compiling the `dive` descent.
Descent is EXPLICIT. A nested struct is walked only when its field says dive, never automatically: automatic recursion would walk into time.Time, net.IP and every other struct that happens to be a field, and a rule engine that silently reaches places the author did not name is a rule engine whose report cannot be trusted.
Package validation — hosts elementPlan, the rule set one element of a dived slice or array is checked against.
Package validation — declares the sentinel *errs.Error values the tag compiler emits. Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.
The VIOLATION codes (0.3.47.1 - 0.3.47.5) deliberately have no sentinel here: a violation is a ViolationValue, not an error, and minting five unused *errs.Error values to mirror them would invite exactly the confusion ADR 0046 §Violations are not errors exists to prevent.
Package validation — one embedded struct encoding/json descends into.
Package validation — which field a JSON key actually decodes into.
Package validation — the regular-expression constraint.
Package validation — the compiled struct-tag plan and its per-type cache.
A plan is compiled ONCE per (type, mode) and reused by every validation of that type. Parsing a struct tag on every request would put string splitting and reflect.StructField lookups on the hot path of every field of every request — which is exactly the shape of the "we added validation and the p99 moved" post-mortem. See BENCH.md for what the cache is worth.
Package validation — hosts planKey, the identity of a compiled plan.
Package validation — the presence constraint.
Package validation — a field the plan puts rules on, located for the reach check.
Package validation — the refusal helpers. Every refusal names what was wrong in structured fields; the Public message stays a fixed literal.
Package validation — the closed set of built-in rule names. A rule name is part of the published contract: it is what a ViolationValue.Rule carries, what an application keys its own translated messages on, and what a validate struct tag spells. Adding one is a decision the SDK maintains forever.
Package validation — the tag dialect: what a rule item may say, and what it is refused for saying.
Package validation — the kind-bound halves of the tag dialect. Each builder resolves the field's kind ONCE, at compile time, and returns a closure that reads the value through the one accessor that kind supports. A validation therefore performs no type dispatch at all.
Package validation — the tag half of set membership.
Package validation — the set-membership constraint.
Package validation — the size constraints: how long a string is, and how many elements a slice holds. Two rules rather than one, because "at most 30 characters" and "at most 30 entries" are different requirements with different fixes, and Go's type system cannot spell len over both anyway.
Package validation — the struct-tag front end.
Package validation implements the SDK's constraint engine over the internal/core/validation port: a small, closed set of built-in constraints, the combinators that compose them and descend into nested structures, and a struct-tag front end that compiles a cached plan per type (ADR 0046).
Two entry shapes, one contract. The PROGRAMMATIC path is generic and uses no reflection at all — Field and Each take an accessor function, so every descent is a direct field read the compiler can inline. The TAG path trades that for ergonomics and is measured rather than assumed; see BENCH.md.
Both produce a core/validation.Constraint, so they compose with each other: a struct-tag validator and a hand-written cross-field rule can be handed to All and appear in one report.
Collecting EVERY violation is the default and First is the opt-in. A form that reports one error at a time makes the user submit it five times.
Package validation — the single place a ViolationValue is minted, so every built-in constraint reports the same shape.
Index ¶
- Constants
- Variables
- func All[T any](constraints ...corevalidation.Constraint[T]) corevalidation.Constraint[T]
- func AtLeast[T cmp.Ordered](lo T) corevalidation.Constraint[T]
- func AtMost[T cmp.Ordered](hi T) corevalidation.Constraint[T]
- func Between[T cmp.Ordered](lo, hi T) (constraint corevalidation.Constraint[T], err error)
- func Check[T any](value T, constraints ...corevalidation.Constraint[T]) error
- func Count[E any](minLen, maxLen int) (constraint corevalidation.Constraint[[]E], err error)
- func Each[T, E any](name string, get func(T) []E, constraints ...corevalidation.Constraint[E]) (constraint corevalidation.Constraint[T], err error)
- func Field[T, F any](name string, get func(T) F, constraints ...corevalidation.Constraint[F]) (constraint corevalidation.Constraint[T], err error)
- func First[T any](constraints ...corevalidation.Constraint[T]) corevalidation.Constraint[T]
- func Length(minRunes, maxRunes int) (constraint corevalidation.Constraint[string], err error)
- func Matches(pattern string) (constraint corevalidation.Constraint[string], err error)
- func Must[T any](constraint corevalidation.Constraint[T], err error) corevalidation.Constraint[T]
- func OneOf[T comparable](allowed ...T) (constraint corevalidation.Constraint[T], err error)
- func Required[T comparable]() corevalidation.Constraint[T]
- func Struct[T any](cfg StructConfig) (constraint corevalidation.Constraint[T], err error)
- type StructConfig
Constants ¶
const CodeInvalidRule errs.Code = 0x00_03_2F_06 // 0.3.47.6
CodeInvalidRule identifies a struct tag refused at COMPILE time: an unknown rule, a malformed argument, a rule applied to a kind it cannot check, or a construct this dialect refuses by name. It is an error, never a violation — the value was never reached.
const CodeLengthOutOfRange errs.Code = 0x00_03_2F_03 // 0.3.47.3
CodeLengthOutOfRange identifies a string, slice or array whose SIZE is outside its allowed bounds. It is distinct from CodeOutOfRange because "between 3 and 10" and "3 to 10 characters long" are different rules with different fixes, and a caller routing on the code should not have to guess which one fired.
const CodeNotInSet errs.Code = 0x00_03_2F_04 // 0.3.47.4
CodeNotInSet identifies a value absent from the constraint's allowed set.
const CodeOutOfRange errs.Code = 0x00_03_2F_02 // 0.3.47.2
CodeOutOfRange identifies an ordered value outside its allowed bounds.
const CodePatternMismatch errs.Code = 0x00_03_2F_05 // 0.3.47.5
CodePatternMismatch identifies a string that did not match its pattern.
const CodeRequired errs.Code = 0x00_03_2F_01 // 0.3.47.1
CodeRequired identifies a value that is absent where presence was required. It is a VIOLATION code — it travels on a ViolationValue, not on an error.
const CodeUnsupportedTarget errs.Code = 0x00_03_2F_07 // 0.3.47.7
CodeUnsupportedTarget identifies a Struct[T] whose T the tag engine cannot walk — anything that is not a struct type.
const Unbounded int = -1
Unbounded is the upper bound that means "no ceiling". It is spelled rather than implied by a negative number so a reader of Length(8, Unbounded) does not have to know the convention to understand the line.
Variables ¶
var ( // InvalidRule is returned by Struct when a validate tag cannot be // compiled. The fields name the field, the rule and the clause that // failed; the message never guesses at what was meant, it names what was // refused — an unsupported dialect construct is refused BY NAME so the fix // is one word, not an investigation. InvalidRule = errs.Define(CodeInvalidRule, "INVALID_RULE", "The validate struct tag could not be compiled", "service/validation: a validate tag names an unknown rule, a malformed argument, or a rule the field's kind cannot answer; the fields carry the field name, the rule and the clause", errs.WithExitCode(exitConfig)) // UnsupportedTarget is returned by Struct when T is not a struct type. // The tag engine walks fields; there is nothing to walk on a scalar, a // slice or a pointer, and inventing a meaning for those would be a // different feature wearing this one's name. UnsupportedTarget = errs.Define(CodeUnsupportedTarget, "UNSUPPORTED_TARGET", "Struct tag validation requires a struct type", "service/validation: Struct[T] was instantiated with a non-struct T; the field carries the kind that was seen", errs.WithExitCode(exitConfig)) )
Functions ¶
func All ¶
func All[T any](constraints ...corevalidation.Constraint[T]) corevalidation.Constraint[T]
All runs every constraint and returns everything they found, in composition order. It is the default shape of the engine: a caller who wanted one answer can take the first violation, but a caller who needed all of them cannot recover the ones a short-circuit never computed.
All with no constraint is legitimate and accepts everything — a validator without rules is not a broken validator (ADR 0031). What is refused is a constraint that could never be satisfied, and that is refused by its own constructor, before it can be composed.
func AtLeast ¶
func AtLeast[T cmp.Ordered](lo T) corevalidation.Constraint[T]
AtLeast refuses a value below lo. The bound is inclusive: lo itself passes. It is the programmatic spelling of the `min=` struct tag; the name avoids shadowing Go's `min` builtin at every call site that dot-imports nothing.
It cannot be misconfigured — every value of an ordered type is a legitimate floor — so it returns a Constraint directly.
func AtMost ¶
func AtMost[T cmp.Ordered](hi T) corevalidation.Constraint[T]
AtMost refuses a value above hi. The bound is inclusive: hi itself passes. It is the programmatic spelling of the `max=` struct tag.
It cannot be misconfigured, for the same reason as AtLeast.
func Between ¶
func Between[T cmp.Ordered](lo, hi T) (constraint corevalidation.Constraint[T], err error)
Between refuses a value outside the closed interval [lo, hi].
It REFUSES lo > hi at construction rather than validating in silence. An inverted interval is satisfied by nothing, so the alternative is a validator that rejects every value it is ever shown while reporting a plausible per-field message — the ADR 0031 failure mode, in its rejecting form. The caller who wrote Between(10, 1) meant Between(1, 10), and finds out now rather than from a support ticket.
lo == hi is NOT a misconfiguration: an interval of exactly one value is a coherent requirement, and refusing it would be the SDK second-guessing a legitimate intent.
func Check ¶
func Check[T any](value T, constraints ...corevalidation.Constraint[T]) error
Check runs constraints against value at the root path and converts the result to the SDK error model. It is the one-line bridge to an error-typed contract — principally core/config.Validator's Validate() error, which is how a decoded config struct self-checks after config.Load.
It returns a genuine nil interface when nothing was found wrong.
func Count ¶
func Count[E any](minLen, maxLen int) (constraint corevalidation.Constraint[[]E], err error)
Count refuses a slice whose ELEMENT count falls outside [min, max]. It is also how presence is expressed for a slice, which Required cannot cover because a slice is not comparable: Count(1, Unbounded).
func Each ¶
func Each[T, E any](name string, get func(T) []E, constraints ...corevalidation.Constraint[E]) (constraint corevalidation.Constraint[T], err error)
Each applies constraints to EVERY element of a slice member of T reached through get, reporting each element's violations at the parent path extended by name and the element's index — "addresses[2]".
Every element is visited, including those after the first failing one: an import that reports row 3 and stops makes the operator run the import six times. The elements are visited in slice order, so the report is stable across runs, which is what makes two reports diffable.
A nil slice contributes nothing — it holds no element to be wrong about. Requiring at least one element is Count, a different question asked at the member itself.
A nil get, or an empty name, is refused at construction for the reasons Field gives.
func Field ¶
func Field[T, F any](name string, get func(T) F, constraints ...corevalidation.Constraint[F]) (constraint corevalidation.Constraint[T], err error)
Field applies constraints to a member of T reached through get, reporting every violation at the parent path extended by name.
name is the member's name as the CALLER wants it to appear in a path; the SDK does not read it off the type, because the programmatic path never touches reflection. Struct derives it from the json tag instead.
A nil get is REFUSED at construction. It is not a missing rule — the rules are right there — it is a missing way to reach the value they are about, so carrying it would produce a validator that runs, reports nothing, and looks exactly like a passing one. That is the ADR 0031 trap in its accepting form, which is the more dangerous of the two. An empty name is refused for the neighbouring reason: a violation whose path silently points at the parent is a violation that is not located.
func First ¶
func First[T any](constraints ...corevalidation.Constraint[T]) corevalidation.Constraint[T]
First runs the constraints in order and stops at the one that refuses, returning only its violations. It is a REAL short-circuit — the constraints after the first failure are never evaluated — so it is honest to reach for when a later check is expensive or would be meaningless.
It is deliberately a combinator rather than a mode flag on a runner. A flag that truncated the report after the fact would claim a saving it did not make; a combinator that stops really stops, and the caller can see where.
func Length ¶
func Length(minRunes, maxRunes int) (constraint corevalidation.Constraint[string], err error)
Length refuses a string whose RUNE count falls outside [min, max].
It counts runes, not bytes: "at most 30 characters" is what a form says, and counting bytes would refuse a perfectly ordinary accented name at 16 characters. It does not count grapheme clusters — an emoji built from a zero-width-joiner sequence counts as several runes — because clustering needs a Unicode table the SDK does not carry, and claiming otherwise would be worse than saying so.
max may be Unbounded. A negative min, or min > max when max is bounded, is refused at construction (ADR 0031).
func Matches ¶
func Matches(pattern string) (constraint corevalidation.Constraint[string], err error)
Matches refuses a string that the pattern does not match anywhere. Anchor it with ^ and $ when the whole string must match — that is regexp's own convention and inventing a different one here would surprise everyone who already knows it.
The pattern is COMPILED AT CONSTRUCTION and an uncompilable one is refused (ADR 0031). A pattern compiled lazily on first use would turn a typo in a rarely-exercised rule into a failure at the worst possible moment; compiling now means a bad pattern cannot reach production at all.
Why this is safe against the input ¶
Go's regexp is RE2: no backtracking, linear time in the length of the input. A pattern cannot be turned into a denial of service by the value it is shown, which is what makes it acceptable to run one on untrusted input at all. It also means the SDK does NOT need — and does not impose — an input length cap here; cap the length with Length if the field has one, for its own reasons.
Matches is deliberately absent from the struct-tag dialect. See Struct.
func Must ¶
func Must[T any](constraint corevalidation.Constraint[T], err error) corevalidation.Constraint[T]
Must returns constraint, panicking when err is non-nil. It is the regexp.MustCompile / template.Must idiom, for the same situation: a constraint built from source literals at package initialisation, where the only possible error is a defect the binary must not start with.
Use it for a package-level var. Do NOT use it on a bound that comes from configuration — there the error is a real runtime condition and must be returned, which is why every fallible constructor hands one back.
func OneOf ¶
func OneOf[T comparable](allowed ...T) (constraint corevalidation.Constraint[T], err error)
OneOf refuses a value absent from allowed.
It REFUSES an empty allowed set at construction. An empty set is satisfied by nothing, so the alternative is a constraint that rejects every value it is ever shown while looking like a working rule — the ADR 0031 failure mode in its rejecting form. "No allowed values" is not a permissive rule, it is a forgotten argument.
Duplicates in allowed are accepted and collapse: repeating a value does not change the set it denotes, and refusing a duplicate would turn a harmless copy-paste in a generated list into a start-up failure.
func Required ¶
func Required[T comparable]() corevalidation.Constraint[T]
Required refuses the zero value of T.
What "absent" means here, and what it cannot mean ¶
A Go value type has no representation for "not supplied": the zero value is what a decoder leaves behind for a field the input never mentioned AND for a field the input explicitly set to 0, "" or false. Required therefore refuses BOTH, and it cannot tell them apart — no constraint operating on T can.
This is stated rather than hidden because it changes designs: a caller who must distinguish "the operator did not configure a timeout" from "the operator configured a timeout of zero" declares the field as a POINTER, and Required on *time.Duration then means exactly "was supplied". The SDK will not guess which of the two a bare zero meant.
Required needs no configuration and therefore cannot be misconfigured, which is why it returns a Constraint directly rather than a (Constraint, error).
It is constrained to comparable T because presence is "differs from the zero value", and a slice is not comparable. Presence of a slice is its element count: use Count(1, Unbounded).
func Struct ¶
func Struct[T any](cfg StructConfig) (constraint corevalidation.Constraint[T], err error)
Struct compiles the `validate` struct tags of T into a Constraint. The plan is compiled once per (type, mode) and cached, so calling Struct again for the same T is one map read — which is what makes it usable inside a core/config.Validator's Validate method:
func (c Conf) Validate() error {
rules, err := validation.Struct[Conf](validation.StructConfig{})
if err != nil {
return err
}
return validation.Check(c, rules)
}
The result IS a Constraint, so tags and hand-written rules compose into one report: All(tagRules, crossFieldRule).
A type with no validate tag is legitimate ¶
It compiles to an empty plan and accepts everything. "No rules" is a rule set, and refusing it would make it impossible to add validation to a type incrementally. What is refused is a tag that cannot be honoured — and it is refused HERE, at compile time, never at the first request (ADR 0031).
The dialect is a strict subset of the programmatic API, on purpose ¶
Accepted: required, min, max (numbers), minlen, maxlen (string runes), mincount, maxcount (slice/array elements), oneof=a|b|c, dive.
Refused BY NAME, each with a message that says what to do instead: pattern and regex (a regular expression cannot live in a comma-separated tag without inventing an escape dialect, and a SILENTLY TRUNCATED pattern is the exact class of failure this SDK refuses — compose Matches in code); email, url and uuid (see ADR 0046 §What is refused); dive on a map; any unknown rule.
Types ¶
type StructConfig ¶
type StructConfig struct {
// StopAtFirst compiles a plan that stops at the first violation instead of
// collecting every one. The zero value collects everything, because a form
// that reports one error at a time makes the user submit it five times.
//
// It is compiled INTO the plan, nested plans included, so a stop-at-first
// validator really does stop: the later fields are never read and the
// later rules never run. It is not a filter applied to a full report.
StopAtFirst bool
}
StructConfig tunes a compiled struct validator.