validation

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 3 Imported by: 0

README

validation

Package validation declares the SDK's value-checking port: Constraint[T], the ViolationValue it reports when a value is not acceptable, the ReportValue that collects them, and the path grammar (RootPath, JoinField, JoinIndex) that makes a violation say where — user.addresses[2].zip.

Constraint is a function port rather than an interface, so it cannot grow a method and break a downstream implementer (ADR 0039).

A violation is a value, not an error, and a report is not an error either: its zero value is a passing report, and ReportValue.Err() converts on demand, returning a genuine nil when nothing is wrong.

This package is the engine's contract; internal/core/config.Validator is the self-check contract a config struct implements using it. The concrete constraints, the combinators and the struct-tag front end live in internal/service/validation; facade: pkg/v1/validation. ADR 0046. See CLAUDE.md.

Documentation

Overview

Package validation — range 0.2.15.* (ADR 0046 core/validation block).

Package validation — declares the sentinel *errs.Error port outcomes. Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.

Package validation — defines the path grammar every ViolationValue speaks.

Package validation — hosts ReportValue, the ordered collection of everything one validation found wrong.

Package validation declares the SDK's value-checking port: the Constraint that inspects one value, the ViolationValue it reports when that value is not acceptable, and the ReportValue that collects them. A core sibling admitted by ADR 0046.

The port is a FUNCTION type rather than an interface — the shape internal/core/CLAUDE.md already admits for resilience.Operation and scheduler.Job/Schedule. ADR 0039's rule is that a published port must not grow a method, because pkg/v1 aliases publish the shape and Go interfaces are structural; a func type satisfies that rule structurally, since it cannot grow a method at all.

What this package is not

It is not internal/core/config.Validator. That port is a decoded config struct's SELF-check — `Validate() error`, one boolean answer, called once by config.Load. This package is the engine that answers it: a struct implements config.Validator by running its constraints and returning ReportValue.Err(). The two compose; neither replaces the other.

The concrete constraints (presence, bounds, length, set membership, pattern), the combinators that descend into fields and elements, and the struct-tag front end live in internal/service/validation. This package owns the contract, the two domain values, the path grammar, and the typed sentinels.

Package validation — hosts ViolationValue, the located statement that one value failed one rule.

Index

Constants

View Source
const CodeConstraintMisconfigured errs.Code = 0x00_02_0F_02 // 0.2.15.2

CodeConstraintMisconfigured identifies a constraint refused AT CONSTRUCTION because its configuration cannot be honoured — an inverted numeric range, a negative length bound, an empty allowed set, an uncompilable pattern. It is never a validation outcome: the value was never reached.

View Source
const CodeValidationFailed errs.Code = 0x00_02_0F_01 // 0.2.15.1

CodeValidationFailed identifies the aggregate error a non-empty ReportValue converts to. It is the code an error-typed contract sees; the individual rules keep their own codes inside the report.

View Source
const RootPath string = ""

RootPath is the path of the value handed to a validator, before any descent. A violation reported at RootPath is about the value as a whole — a cross-field rule with no single culpable member.

Variables

View Source
var (
	// ValidationFailed is what ReportValue.Err returns for a non-empty report.
	// Its fields carry the violation count, the first rule, and the list of
	// paths — never a message and never a value. The full report stays the
	// ReportValue; this error is the shape an error-typed contract can hold.
	ValidationFailed = errs.Define(CodeValidationFailed, "VALIDATION_FAILED",
		"One or more values did not satisfy their constraints",
		"core/validation: a ReportValue was converted to an error; the fields carry the violation count and the offending paths",
		errs.WithHTTPStatus(httpUnprocessable))

	// ConstraintMisconfigured is returned by a constraint CONSTRUCTOR whose
	// arguments it cannot honour: a minimum above its maximum, a negative
	// length, an empty allowed set, an uncompilable pattern.
	//
	// It is the ADR 0031 half that must not be confused with the other: a
	// validator with NO constraint is legitimate and passes, while a
	// constraint that could never be satisfied — or never be evaluated — is
	// refused before it can quietly reject, or quietly accept, everything.
	ConstraintMisconfigured = errs.Define(CodeConstraintMisconfigured, "CONSTRAINT_MISCONFIGURED",
		"The constraint cannot be built from the given configuration",
		"core/validation: a constraint constructor received bounds or arguments it cannot honour; the fields name the rule and the clause that failed",
		errs.WithExitCode(exitConfig))
)

Functions

func JoinField

func JoinField(base, name string) string

JoinField extends base with a struct member, producing the dotted form the grammar defines:

JoinField(RootPath, "user")        == "user"
JoinField("user", "address")       == "user.address"
JoinField("user.addresses[2]", "zip") == "user.addresses[2].zip"

name is whatever the calling front end names the member. The struct-tag front end passes the json tag's name when the field has one, else the Go field name (ADR 0046): the SDK's own config.Load decodes every format through a JSON round trip, so the json name is the key the operator actually wrote. The programmatic Field takes the name as an argument, so there the caller decides.

The grammar does not quote. A member name that itself contains '.', '[' or ']' — json:"log.level" is legal — yields a path indistinguishable from a nested or indexed one. The quoted segment that would disambiguate it is the same one map descent needs, and ADR 0046 §Deferred defers both together rather than growing the grammar for one of them.

func JoinIndex

func JoinIndex(base string, index int) string

JoinIndex extends base with an element position, producing the bracketed form the grammar defines:

JoinIndex("addresses", 2) == "addresses[2]"

The bracket binds tighter than the dot: "a.b[0].c" is element 0 of a.b, and there is never a dot before an opening bracket.

Types

type Constraint

type Constraint[T any] func(path string, value T) ReportValue

Constraint reports what is wrong with value, at the path by which value was reached. A value it accepts yields a nil ReportValue — the acceptance path therefore allocates nothing, which matters because it is the path taken on every field of every valid request.

path is the position of value inside the structure being validated, in the grammar JoinField / JoinIndex define; RootPath means "the value itself". A Constraint MUST propagate that path into every ViolationValue it reports, and MUST NOT invent a different grammar — the path is the whole reason a violation is actionable on a nested structure.

Implementations MUST be pure and safe for concurrent use: the same value at the same path always yields the same report, and one compiled Constraint is shared by every goroutine validating that type.

A Constraint MUST NOT copy the VALUE it rejected into the report. It names the rule and the bound; the value may be a password, a token or a national identifier, and a validation message is the one error message in a service that is designed to reach the end user. See ViolationValue.

type ReportValue

type ReportValue []ViolationValue

ReportValue is every violation one validation found, in the order the constraints reported them. It is a SLICE rather than a struct so that the zero value — nil — is a passing report: a validator with no constraint passes, which is ADR 0046's first invariant and ADR 0031's "an empty configuration is not the same thing as a broken one".

Composition is therefore plain append, and the accepting path allocates nothing at all.

ReportValue is a published concrete shape (pkg/v1/validation.Report aliases it), so ADR 0040 applies: it may still change while the module is v0, said out loud, and not after v1.

func (ReportValue) Err

func (r ReportValue) Err() error

Err converts the report to the SDK error model, so a validation result can satisfy an error-typed contract — principally core/config.Validator's Validate() error, which is how a decoded config struct self-checks.

It returns a genuine nil interface when the report is OK. That is why ReportValue does not implement error itself: a value type implementing error and returned by value makes `if err != nil` true for a PASSING validation, the typed-nil trap errs.Wrap has an explicit case for.

The error carries the violation COUNT and the list of paths, never the messages and never the values: the full report is the report. A caller that needs every message keeps the ReportValue; the error is the interop shape.

func (ReportValue) First

func (r ReportValue) First() (violation ViolationValue, ok bool)

First returns the earliest violation and whether there was one. Constraints report in the order they were composed, so "first" is the outermost rule on the earliest field — the one a stop-at-first caller wants.

func (ReportValue) OK

func (r ReportValue) OK() bool

OK reports whether the value satisfied every constraint. It is the question to ask; comparing len(report) to zero says the same thing less clearly.

func (ReportValue) Paths

func (r ReportValue) Paths() []string

Paths returns the located path of every violation, in report order. It is the shape an HTTP layer wants when it maps a report onto a form: the caller already knows its own field names, and needs the SDK to say which ones.

type ViolationValue

type ViolationValue struct {
	// Path locates the offending value inside the structure that was
	// validated, in the grammar of [JoinField] / [JoinIndex] —
	// "user.addresses[2].zip". [RootPath] (the empty string) means the value
	// as a whole, which is what a cross-field rule reports when no single
	// field is at fault.
	Path string
	// Rule names the constraint that refused, in lower case —
	// "required", "between", "length", "one_of", "pattern". It is the stable
	// identifier a caller switches on when the Code granularity is too coarse,
	// and the key an application uses to look up its own translated message.
	Rule string
	// Message explains the rule in one wire-safe line: no newline, no private
	// data, and — the part that is a security property rather than a style
	// choice — NEVER the rejected value itself. A validation message is the
	// one error message in a service designed to reach the end user, so a
	// message that echoed the value would exfiltrate whatever the caller was
	// unwise enough to validate: a password, a token, a card number. It names
	// the rule and the bound instead ("must be between 8 and 64 characters").
	Message string
	// Code is the dotted-quad identity of the rule that refused, so a caller
	// routes on it with errs.HasCode / errs.NewPrefixMatcher instead of
	// string-matching Rule. The built-in constraints own 0.3.47.*; a
	// consumer-written Constraint SHOULD carry a Code in the third-party
	// Major range (ADR 0019, 0x40-0x7F).
	Code errs.Code
}

ViolationValue is one located failure: WHERE it happened, WHICH rule it broke, a wire-safe explanation, and a routable errs.Code.

It is deliberately NOT an error. A validation normally produces several of them, and errors.Join of five would render five bracket headers, lose the ordering and bury the paths — while a single error would have to pick one violation to be about. The aggregate that IS convertible to an error is ReportValue; see its Err method.

The zero value is not meaningful; constraints build one per failure.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL