validation

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package validation defines Arc findings, typed validator invocation and command severity policy, explicit graph registries and portable model annotations. Runtime method discovery and reflective invocation are not provided.

Index

Examples

Constants

View Source
const AllowedSeverityHeader = "X-Allowed-Severity"

AllowedSeverityHeader is the command ingress allowance header.

Variables

View Source
var (
	// ErrRejected identifies an application rejection.
	ErrRejected = errors.New("validation rejected")
	// ErrValidatorFailed identifies an unexpected validator failure.
	ErrValidatorFailed = errors.New("validator failed")
	// ErrInvalidSeverity identifies unsupported numeric severities.
	ErrInvalidSeverity = errors.New("invalid validation severity")
	// ErrInvalidValidator identifies a nil callback or context.
	ErrInvalidValidator = errors.New("invalid validator")
)
View Source
var (
	// ErrDuplicate identifies multiple graph validators for one exact type.
	ErrDuplicate = errors.New("duplicate graph validator")
	// ErrFrozen identifies changes after a successful registry Build.
	ErrFrozen = errors.New("validation registry frozen")
	// ErrInvalidRegistration identifies invalid validator model/factory metadata.
	ErrInvalidRegistration = errors.New("invalid validation registration")
	// ErrGraphDepth identifies a graph exceeding the framework traversal budget.
	ErrGraphDepth = errors.New("validation graph depth exceeded")
)

Functions

func Register

func Register[T any](r *Registry, validator Validator[T]) error

Register borrows a shared, concurrently callable validator for exact T.

func RegisterConcept

func RegisterConcept[T any](r *Registry, validator Validator[T]) error

RegisterConcept registers T as an opaque concept leaf. Findings attach to the owning member, not to a concept's private representation.

func RegisterScoped

func RegisterScoped[T any](r *Registry, factory Factory[Validator[T]], keys ...di.Key) error

RegisterScoped registers a lazy validator factory. Optional exact dependency keys are copied and checked by pipeline Build without activating the factory. Omit keys for manually wired factories; invalid keys fail registration.

func RegisterScopedConcept

func RegisterScopedConcept[T any](r *Registry, factory Factory[Validator[T]], keys ...di.Key) error

RegisterScopedConcept registers a lazy concept validator factory with the same optional dependency manifest contract as RegisterScoped.

func Reject

func Reject(results ...Result) error

Reject copies nonempty findings into a Failure; no findings returns nil.

Types

type Factory

type Factory[T any] func(context.Context, *execution.Scope) (T, error)

Factory creates a borrowed validator during validation, never during Build. The non-closing scope expires on return from this node's validation callback.

type Failure

type Failure interface {
	error
	ValidationResults() []Result
}

Failure is a business rejection or redacted invocation failure. Findings are returned as independent copies; their application State remains borrowed.

type Graph

type Graph struct {
	// contains filtered or unexported fields
}

Graph holds immutable registrations and is concurrent-safe when its validators are. Zero (and nil) enables model methods and tags without registered rules. Maps are opaque: register an explicit validator to inspect their entries.

func (*Graph) CheckDependencies

func (g *Graph) CheckDependencies(catalog di.Catalog) error

CheckDependencies validates every registered scoped validator's declared keys against a borrowed catalog, without resolving dependencies or running rules. All registrations are checked because runtime interfaces may expose any model. Nil/zero graphs and graphs without keys need no catalog. A missing or typed-nil catalog fails when keys exist. Hidden closure dependencies cannot be checked.

func (*Graph) CheckType

func (g *Graph) CheckType(t reflect.Type) error

CheckType validates statically visible graph/tag shapes without invoking validators. Pipeline Build can use it for input types without registry rules. Interface-held runtime types remain subject to validation-time checks.

func (*Graph) Validate

func (g *Graph) Validate(ctx context.Context, scope *execution.Scope, model any) (results []Result, err error)

Validate runs each nonnull node's exact registry validator, then its model method, before descending readable fields/collection elements. Pointer identity stops cycles/shared instances; equal distinct instances still run. Collections keep the owning path without indices. Concept registrations are opaque leaves. Root required tags run separately after the graph. Results/error semantics match Invoke: Failure findings remain on the returned error, not duplicated in results. Factory failures are infrastructure errors, never validator failures. Scope is optional for direct validators; scoped factories require a valid scope.

Example
package main

import (
	"context"
	"fmt"

	"github.com/cratis/arc.go/validation"
)

func main() {
	type quantity int
	var registry validation.Registry
	err := validation.RegisterConcept(&registry, validation.ValidatorFunc[quantity](func(_ context.Context, value quantity) ([]validation.Result, error) {
		if value > 0 {
			return nil, nil
		}
		return []validation.Result{{Severity: validation.Error, Message: "Quantity must be positive.", Members: []string{"value"}}}, nil
	}))
	if err != nil {
		panic(err)
	}
	graph, err := registry.Build()
	if err != nil {
		panic(err)
	}
	findings, err := graph.Validate(context.Background(), nil, struct {
		Quantity quantity `json:"quantity"`
	}{})
	if err != nil {
		panic(err)
	}
	fmt.Println(findings[0].Members, findings[0].Message)
}
Output:
[quantity] Quantity must be positive.

type InvocationError

type InvocationError struct {
	// Cause is an inspectable unexpected callback error.
	Cause error
	// Panic is the recovered panic value, for local diagnostics only.
	Panic any
}

InvocationError exposes local diagnostics, never callback details in its text. It implements Failure with one safe Error finding.

func (*InvocationError) Error

func (e *InvocationError) Error() string

Error returns generic, client-safe text.

func (*InvocationError) Unwrap

func (e *InvocationError) Unwrap() []error

Unwrap preserves the failure category and original cause.

func (*InvocationError) ValidationResults

func (e *InvocationError) ValidationResults() []Result

ValidationResults returns a fresh, redacted finding with no State or members.

type ModelValidator

type ModelValidator interface {
	Validate(context.Context) ([]Result, error)
}

ModelValidator is the optional model-bound Validate convention. Go method sets apply exactly: value input never synthesizes a pointer receiver.

type Policy

type Policy struct {
	// contains filtered or unexported fields
}

Policy is immutable and concurrent-safe. Zero retains only Error findings.

func NewPolicy

func NewPolicy(options SeverityOptions) (Policy, error)

NewPolicy copies configuration, rejecting severities outside 0–3. An explicit floor cannot be weakened by Allowed, even a trusted Allowed=Error.

func (Policy) Blocks

func (p Policy) Blocks(severity Severity) bool

Blocks reports whether a finding stops execution; unsupported numbers block.

func (Policy) Filter

func (p Policy) Filter(results []Result) []Result

Filter returns independent copies of blocking findings, borrowing State. This does not make an authorization decision. Queries retain all findings.

type Portable

type Portable[T any] struct {
	// contains filtered or unexported fields
}

Portable is an immutable concurrently callable validator built from field tags. Handwritten validators and concept inference remain separate server contracts.

func NewPortable

func NewPortable[T any]() (*Portable[T], error)

NewPortable compiles the explicitly declared rules tags on T's wire fields. It does not run constructors, application validators or codecs. Nullable values skip non-presence rules, matching the pinned JavaScript rules.

func (*Portable[T]) Validate

func (p *Portable[T]) Validate(ctx context.Context, model T) ([]Result, error)

Validate evaluates compiled rules without mutating or retaining the model.

type Reason

type Reason string

Reason is an open wire string; unknown future reasons remain valid.

const (
	// Rule identifies application-authored validation.
	Rule Reason = "rule"
	// ConcurrencyViolation identifies a concurrency rejection.
	ConcurrencyViolation Reason = "concurrencyViolation"
	// ConstraintViolation identifies an event-store constraint rejection.
	ConstraintViolation Reason = "constraintViolation"
	// ValidatorFailed identifies a validator that could not run.
	ValidatorFailed Reason = "validatorFailed"
	// DependencyUnavailable identifies a missing required read model.
	DependencyUnavailable Reason = "dependencyUnavailable"
	// MalformedRequest identifies invalid client input.
	MalformedRequest Reason = "malformedRequest"
)

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a single-owner builder. Zero is usable. One validator (ordinary or concept) is allowed per exact type. Build freezes only after successful checks.

func (*Registry) Build

func (r *Registry) Build() (*Graph, error)

Build validates statically visible tag shapes without constructing validators or invoking rules. Interface-held runtime shapes are checked during Validate.

type Result

type Result struct {
	// Severity is the numeric importance of the finding.
	Severity Severity `json:"severity"`
	// Message is safe, client-visible feedback (never exception-redacted).
	Message string `json:"message"`
	// Members identifies affected wire members; nil means no member.
	Members []string `json:"members"`
	// State is optional application-owned data. Typed nil values are omitted.
	State any `json:"state,omitempty"`
	// Reason is the machine-readable open category; empty means Rule.
	Reason Reason `json:"reason"`
	// ReasonDetail optionally identifies a specific constraint or other cause.
	ReasonDetail *string `json:"reasonDetail,omitempty"`
}

Result is one validation finding. Members use wire names, including indexed paths. State is borrowed application data; callers must not mutate it during encoding. A zero Reason marshals as "rule". Nil Members marshals as [], not null.

func Invoke

func Invoke[T any](ctx context.Context, validator Validator[T], value T) (results []Result, err error)

Invoke copies findings (but borrows State), preserves wrapped Failure errors, and redacts unexpected errors/panics. Cancellation is not validation failure.

Example
package main

import (
	"context"
	"fmt"

	"github.com/cratis/arc.go/validation"
)

func main() {
	validator := validation.ValidatorFunc[int](func(_ context.Context, value int) ([]validation.Result, error) {
		if value < 0 {
			return []validation.Result{{Severity: validation.Error, Message: "Must be positive."}}, nil
		}
		return nil, nil
	})
	findings, err := validation.Invoke(context.Background(), validator, -1)
	fmt.Println(len(findings), err)
}
Output:
1 <nil>

func ValidateTags

func ValidateTags(model any) ([]Result, error)

ValidateTags runs only the root's portable required annotations. It neither invokes model validators nor descends nested models. Nil roots are skipped.

func (Result) Clone

func (r Result) Clone() Result

Clone copies a finding's members and reason detail. Application State is borrowed.

func (Result) MarshalJSON

func (r Result) MarshalJSON() ([]byte, error)

MarshalJSON implements the Arc validation envelope.

func (Result) MarshalJSONWith

func (r Result) MarshalJSONWith(encode func(any) ([]byte, error)) ([]byte, error)

MarshalJSONWith encodes nested State using the supplied Arc traversal. The callback is synchronous and is not retained; callers normally use MarshalJSON.

type RuleDescriptor

type RuleDescriptor struct {
	Property   string            `json:"property,omitempty"`
	Name       string            `json:"name"`
	Arguments  []json.RawMessage `json:"arguments,omitempty"`
	Message    string            `json:"message,omitempty"`
	Severity   *Severity         `json:"severity,omitempty"`
	Source     string            `json:"source,omitempty"`
	ServerOnly bool              `json:"serverOnly,omitempty"`
	Optional   bool              `json:"optional,omitempty"`
	Concept    bool              `json:"concept,omitempty"`
}

RuleDescriptor is the portable, serializable server/client rule contract. Arguments are JSON string/number scalars, validated against Name before use. Nil Severity means Error. ServerOnly rules must supply their own server validator; neither the portable executor nor generator silently skips them.

func ParseRules

func ParseRules(text, property, kind string) ([]RuleDescriptor, error)

ParseRules reads the rules field tag's JSON array without executing validators. kind is string, number, boolean, collection or object. Paths are wire names. Explicit messages are required so both projections preserve server messages. Unsupported rules/regex/severity are construction errors, not weaker checks.

type Severity

type Severity int32

Severity is Arc's numeric wire severity. Every retained finding makes a result invalid, regardless of severity; filtering belongs to the later pipeline.

const (
	// Unknown is severity zero.
	Unknown Severity = iota
	// Information is informational feedback.
	Information
	// Warning is warning feedback.
	Warning
	// Error is an error finding.
	Error
)

func AllowedFromHeader

func AllowedFromHeader(values []string) (Severity, bool)

AllowedFromHeader accepts exactly one trimmed signed decimal int32 in 0–3. Untrusted Error allowance is capped at Warning. Invalid input means default policy, not permission to ignore all findings.

type SeverityOptions

type SeverityOptions struct {
	// Allowed retains only severities strictly greater than this value.
	Allowed *Severity
	// BlockOn is inclusive and always blocks Unknown when present.
	BlockOn *Severity
}

SeverityOptions combines a caller allowance with an inclusive command floor.

type Tags

type Tags struct {
	// Required rejects nil or empty/whitespace strings in the top-level model.
	// Numeric zero, false and nonnil empty collections are supplied values.
	Required bool
	// SkipConcept suppresses only the immediate child's registered concept rule.
	SkipConcept bool
}

Tags is the portable validation annotation subset, separate from graph rules.

func ParseTags

func ParseTags(text string) (Tags, error)

ParseTags accepts required and skipConcept separated by commas. Values, duplicate options and unknown rules are errors, never ignored annotations.

type Validator

type Validator[T any] interface {
	Validate(context.Context, T) ([]Result, error)
}

Validator validates caller-owned input synchronously. Implementations must honor cancellation and support concurrent calls if shared across operations.

type ValidatorFunc

type ValidatorFunc[T any] func(context.Context, T) ([]Result, error)

ValidatorFunc adapts a validation callback.

func (ValidatorFunc[T]) Validate

func (f ValidatorFunc[T]) Validate(ctx context.Context, value T) ([]Result, error)

Validate invokes f without taking ownership of value.

Jump to

Keyboard shortcuts

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