rules

package
v0.401.0 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

rules — GOBL Validation Framework

The rules package provides structured validation for GOBL types. Rules produce machine-readable fault codes (e.g. GOBL-HEAD-HEADER-02) alongside human-readable messages, making errors testable by stable code rather than fragile string matching and suitable for export as structured data.

Core concepts

For — define a rule set for a type
func myRules() *rules.Set {
    return rules.For(new(MyStruct),
        // ... Defs
    )
}

rules.For accepts either a struct pointer or a named value type (e.g. rules.For(MyCode(""), ...)). The prototype value is used for type inference and to validate field names at initialisation time.

Field — scope assertions to a field
rules.Field("name",
    rules.Assert("01", "name is required", is.Present),
)

The name must match the JSON tag of a field in the parent struct. It is validated at initialisation — an unknown name panics immediately. All assertions inside Field receive the extracted field value.

Assert — a single validation assertion
rules.Assert("01", "description", test1, test2, ...)

All tests must pass. The first failure short-circuits the assertion and emits a fault with the given description. Assertion codes are prefixed automatically by Register or NewSet to form globally unique codes like GOBL-ORG-EMAIL-01.

Use AssertIfPresent when the assertion should be skipped for nil or empty values:

rules.Field("code",
    rules.AssertIfPresent("01", "code format invalid",
        is.Func("valid", isValidCode),
    ),
)
Each — per-element assertions on a slice field

Each is a nameless Def used inside Field to apply assertions to each element of a slice. It does not take a field name — it operates on the slice already extracted by the enclosing Field:

rules.Field("lines",
    rules.Assert("01", "no duplicate line codes",
        is.Func("no duplicates", hasNoDuplicateLineCodes),
    ),
    rules.Each(
        rules.Field("code",
            rules.Assert("02", "line code is required", is.Present),
        ),
    ),
)

Faults carry indexed paths: lines[0].code, lines[1].code, etc. Each panics at initialisation time when used outside a slice field. If the element type has its own registered rule set, those rules are applied automatically during recursive validation — Each is only needed when adding assertions from the parent's perspective that don't belong on the element type itself.

When — conditional rule subsets
rules.When(conditionTest,
    rules.Field("code", rules.Assert("01", "code is required", is.Present)),
)

The subset is only evaluated when conditionTest.Check(obj) returns true. The condition receives the full parent struct. Use is.Expr(...), is.Func(...), or any Test implementation.

Object — group object-level assertions
rules.Object(
    rules.Assert("10", "cross-field constraint", is.Expr(`FieldA != "" || FieldB == nil`)),
)

Object is sugar for passing assertions directly to For or When. Use it for organisational clarity when mixing field and object-level assertions.

Register — add rules to the global registry

In the package init() function (typically mypkg.go):

func init() {
    schema.Register(schema.GOBL.Add("mypkg"), MyStruct{})
    rules.Register(
        "mypkg",
        rules.GOBL.Add("MYPKG"),
        myStructRules(),
        anotherStructRules(),
    )
}

The first argument is the package name (used for generation), and the second is the namespace code prepended to all assertion IDs. Rules registered here are automatically applied by rules.Validate(obj) to any matching object type anywhere in the object graph.

NewSet — standalone namespace sets

When using rule sets outside the GOBL global registry (e.g. in a separate application for validating request bodies), use NewSet to create a namespace set that can be validated directly:

const MyApp rules.Code = "MYAPP"

personSet := rules.For(new(Person),
    rules.Assert("01", "name required", is.Present),
)
emailSet := rules.For(new(Email),
    rules.Field("addr",
        rules.Assert("01", "email required", is.Present),
    ),
)

validator := rules.NewSet(MyApp, personSet, emailSet)
faults := validator.Validate(person)
// Codes: MYAPP-PERSON-01, MYAPP-EMAIL-01

Unlike Register, the returned set is NOT added to the global registry and will not be applied by rules.Validate(obj). Like Register, the namespace code is prepended to all assertion and set IDs during construction.

Input sets are cloned internally, so the same output of For can safely be reused across multiple NewSet or Register calls.

Set.Validate also accepts optional WithContext values to inject context for context-aware guards:

faults := validator.Validate(obj, func(rc *rules.Context) {
    rc.Set("country", "ES")
})

Available tests

All tests live in the github.com/invopop/gobl/rules/is package. Import it alongside rules:

import (
    "github.com/invopop/gobl/rules"
    "github.com/invopop/gobl/rules/is"
)
Test Notes
is.Present Fails if nil, zero, or empty
is.NilOrNotEmpty Passes if nil pointer or non-empty
is.Empty Passes if nil or empty; fails if a value is present
is.Nil Passes only for a nil pointer; fails for any non-nil value, even empty
is.In(vals...) Skips nil; works with named types
is.NotIn(vals...) Skips nil; works with named types
is.Matches(pattern) Skips nil/empty strings
is.Length(min, max) max=0 means no upper bound
is.RuneLength(min, max) Unicode-aware
is.Min(v) / is.Max(v) int, uint, float, time
is.Expr(expr) CEL-like expression; fields accessed by Go field name
is.Func(desc, func(any) bool) Custom boolean function
is.StringFunc(desc, func(string) bool) Convenience for string-typed fields
is.FuncError(desc, func(any) error) Error message is discarded; use desc
is.FuncContext(desc, func(rules.Context, any) bool) Context-aware custom function
is.Or(tests...) Passes if any test passes
is.InContext(test) Passes when any context value satisfies the inner test

The rules/is package also re-exports all format tests from github.com/invopop/validation/is (e.g. is.URL, is.EmailFormat, is.Alphanumeric).

Common patterns

Required field with format check

Split presence and format into separate assertions so callers can distinguish a missing value from a malformed one:

rules.Field("addr",
    rules.Assert("01", "email address is required", is.Present),
    rules.Assert("02", "email address must be valid", is.EmailFormat),
)
Allowed-values check
rules.Field("category",
    rules.Assert("02", "category is not valid", is.In("a", "b", "c")),
)

is.In normalises named string/int types so In("a", "b") matches both string("a") and MyType("a"). For non-pointer named types like cbc.Key where In cannot distinguish absent from invalid, use AssertIfPresent with a custom validator instead.

Custom validation logic

Extract logic into named private functions and use is.Func, is.StringFunc, or is.FuncError. Prefer private named functions over inline anonymous functions — they are easier to test in isolation, appear in stack traces, and keep the rule set readable at a glance.

func myCodeChecksumValid(code string) bool {
    return isValidChecksum(code)
}

rules.Field("code",
    rules.Assert("03", "code checksum mismatch",
        is.StringFunc("checksum", myCodeChecksumValid),
    ),
)
Object-level (cross-field) assertions

Without Field, an assertion receives the full object. Prefer is.Expr for simple comparisons; use is.Func when the logic is more involved or when you want a named, testable function:

// Simple cross-field check
rules.Assert("10", "digest must be nil when MIME type is not provided",
    is.Expr(`MIME != "" || Digest == nil`),
)

// More complex logic
func digestHasMIME(val any) bool {
    obj, ok := val.(*MyStruct)
    if !ok || obj == nil {
        return false
    }
    return obj.MIME != "" || obj.Digest == nil
}

rules.Assert("10", "digest must be nil when MIME type is not provided",
    is.Func("no digest without MIME", digestHasMIME),
)

Note on receiver shape: rules.Validate(obj) may pass either *T or T to an object-level Func depending on how the object was reached. Always handle both in object-level helpers. Expr handles this automatically.

Conditional validation
func envelopeNotSigned(val any) bool {
    e, ok := val.(*Envelope)
    return ok && len(e.Signatures) == 0
}

rules.When(is.Func("not signed", envelopeNotSigned),
    rules.Field("stamps",
        rules.Assert("12", "stamps not allowed before signing", is.Length(0, 0)),
    ),
)

Rules have no access to context.Context. Conditions that depend on runtime context (e.g. "is signed?") must be derived from the object's own state.

Nested struct fields

Define rules for each type independently and register them all (or group them with NewSet). Both rules.Validate and Set.Validate recurse into every exported field automatically — no wiring is needed between parent and child.

When you need to add constraints on a nested type from the parent's perspective (e.g. regime-specific rules that don't belong on the child type), nest rules.Field calls to drill down the path:

func invoiceRules() *rules.Set {
    return rules.For(new(Invoice),
        rules.When(is.InContext(tax.RegimeIn("XX")),
            rules.Field("supplier",
                rules.Assert("01", "supplier is required", is.Present),
                rules.Field("tax_id",
                    rules.Assert("02", "supplier tax ID is required", is.Present),
                    rules.Field("code",
                        rules.Assert("03", "supplier tax ID must have a code", is.Present),
                    ),
                ),
            ),
        ),
    )
}

Each rules.Field in the chain constrains the context for its children, so assertions inside rules.Field("tax_id", ...) operate on the TaxIdentity struct, not the outer Invoice.

Named value types (e.g. cbc.Code, tax.Rate)

rules.For works with named non-struct types:

func myCodeRules() *rules.Set {
    return rules.For(MyCode(""),
        rules.Assert("01", "code must not be empty", is.Present),
        rules.Assert("02", "code must be alphanumeric", is.Alphanumeric),
    )
}

Inside Expr, the value is exposed as this:

rules.Assert("02", "code must not exceed 10 characters",
    is.Expr(`len(this) <= 10`),
)

Testing

Call rules.Validate(obj) (global registry) or set.Validate(obj) (standalone) and assert on the returned rules.Faults value:

import "github.com/invopop/gobl/rules"

// Global registry validation:
err := rules.Validate(obj)
assert.NoError(t, err)

faults := rules.Validate(obj)
require.NotNil(t, faults)
assert.True(t, faults.HasPath("field"))
assert.True(t, faults.HasCode("GOBL-PKG-STRUCT-01"))
assert.Equal(t, "assertion description", faults.First().Message())

// Standalone set validation:
faults = mySet.Validate(obj)
require.NotNil(t, faults)
assert.True(t, faults.HasCode("MYAPP-STRUCT-01"))

rules.Faults implements error. A nil return means no faults. The full error string format is:

[GOBL-PKG-STRUCT-01] field: assertion description

Assertion code conventions

Codes within a set are short local identifiers (e.g. "01", "02"). They are prefixed by Register or NewSet to form globally unique codes. Follow this convention:

  • 0109: field-level assertions, in the order fields appear in the struct
  • 1019: object-level (cross-field) assertions
  • 20+: reserved for When conditional subsets if needed

The fully-qualified code is constructed as: {NAMESPACE}-{STRUCT}-{LOCAL_CODE}

For example, a "03" assertion on head.Header registered under GOBL-HEAD becomes GOBL-HEAD-HEADER-03.

Assertion message conventions

Write messages so they are self-explanatory without inspecting the fault path or source code:

  1. Include the parent context for nested fields — write "supplier tax ID is required", not "tax ID is required".
  2. Include extension keys in single quotes using fmt.Sprintf — write fmt.Sprintf("tax requires '%s' extension", ExtKeyModel), not "tax requires a model extension".
  3. Include extension values when the code alone is ambiguous — write fmt.Sprintf("NF-e does not support '%s' for '%s'", PresenceDelivery, ExtKeyPresence).
  4. Preserve business rule codes (e.g. BR-FR-30) in messages when the original validation spec includes them — they are the primary reference for compliance.

Documentation

Overview

Package rules provides a framework for defining and applying validation rules to data structures in order to provide consistent error codes and messages from GOBL.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Indirect

func Indirect(value interface{}) (interface{}, bool)

Indirect returns the value that the given interface or pointer references to. If the value implements driver.Valuer, it will deal with the value returned by the Value() method instead. A boolean value is also returned to indicate if the value is nil or not (only applicable to interface, pointer, map, and slice). If the value is neither an interface nor a pointer, it will be returned back.

func IsEmpty

func IsEmpty(value interface{}) bool

IsEmpty checks if a value is empty or not. A value is considered empty if - integer, float: zero - bool: false - string, array: len() == 0 - slice, map: nil or len() == 0 - interface, pointer: nil or the referenced value is empty

func LengthOfValue

func LengthOfValue(value interface{}) (int, error)

LengthOfValue returns the length of a value that is a string, slice, map, or array. An error is returned for all other types.

func Register

func Register(pkg string, code Code, sets ...*Set)

Register is used to register a set of rules for a given namespace.

func RegisterWithGuard

func RegisterWithGuard(pkg string, code Code, guard Test, sets ...*Set)

RegisterWithGuard is used to register a set of rules for a given namespace with an optional guard condition that determines when the rules should be applied.

func StringOrBytes

func StringOrBytes(value interface{}) (isString bool, str string, isBytes bool, bs []byte)

StringOrBytes typecasts a value into a string or byte slice. Boolean flags are returned to indicate if the typecasting succeeds or not.

Types

type Assertion

type Assertion struct {
	// ID defines a globally unique code for this assertion.
	ID Code `json:"id"`
	// Desc is the human-readable message to include in faults when this assertion fails.
	Desc string `json:"desc,omitempty"`
	// Tests is a list of tests to evaluate for this assertion. A false result indicates a failure.
	Tests []Test `json:"tests"`
}

Assertion represents a single validation rule definition.

func (Assertion) MarshalJSON

func (a Assertion) MarshalJSON() ([]byte, error)

MarshalJSON serializes Assertion to JSON, converting Tests to a comma-joined string.

type Code

type Code string

Code defines a unique code to use for rules.

const GOBL Code = "GOBL"

GOBL for GOBL rules.

func (Code) Add

func (c Code) Add(code Code) Code

Add allows us to create a new code by appending a suffix to the existing code.

type Context

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

Context holds key-value pairs accumulated during a rules.Validate call and is passed to ByContext test functions. Use Set to store values and Value to retrieve them.

func (Context) Each

func (c Context) Each(fn func(value any) bool) bool

Each iterates over all values in the context, calling fn for each. Returns true as soon as fn returns true (short-circuit), false otherwise.

func (Context) Keys

func (c Context) Keys() []ContextKey

Keys returns the set of distinct keys present in the context.

func (*Context) Set

func (c *Context) Set(key ContextKey, value any)

Set appends a key-value pair to the validation context, preserving insertion order.

func (Context) Value

func (c Context) Value(key ContextKey) any

Value returns the stored value for key, or nil if absent. Callers do a type assertion: v, ok := ctx.Value(key).(MyType)

type ContextAdder

type ContextAdder interface {
	RulesContext() WithContext
}

ContextAdder is implemented by objects that want to automatically inject values into the validation context when encountered by the rules engine.

type ContextKey

type ContextKey string

ContextKey is the key type for Context entries.

type ContextKeyable

type ContextKeyable interface {
	ContextKeys() []ContextKey
}

ContextKeyable is optionally implemented by guard tests that can report which context keys they depend on. This enables the engine to skip guard evaluation entirely when none of the required keys are present in the validation context.

type ContextualTest

type ContextualTest interface {
	CheckWithContext(rc *Context, val any) bool
}

ContextualTest is implemented by tests that need access to the validation context. The engine checks for this interface before falling back to the standard Test.Check method.

type Def

type Def func(s *Set)

Def is a function that modifies a Set during construction. Assert, Field, Each, Object, and When all return Def values that compose as arguments to For.

func Assert

func Assert(id Code, desc string, tests ...Test) Def

Assert returns a Def that adds a single assertion to the parent set. The assertion is evaluated against the parent object (or extracted field value when used inside Field or Each).

func AssertIfPresent

func AssertIfPresent(id Code, desc string, tests ...Test) Def

AssertIfPresent returns a Def that adds an assertion that is only evaluated when the current scoped value is non-nil and non-empty. Use this for optional fields that have format or content constraints.

func Each

func Each(defs ...Def) Def

Each returns a Def that iterates over the elements of the current context, which must be a slice or array. It is intended to be used inside a Field that targets a slice field. All assertions and subsets inside Each are applied to each element individually.

Usage:

rules.Field("lines",
    rules.Assert("01", "no duplicates", checkNoDups),  // whole-slice assertion
    rules.Each(
        rules.Assert("02", "line required", is.Present),  // per-element
    ),
)

Each panics at initialisation time if the parent context is not a slice or array.

func Field

func Field(name string, defs ...Def) Def

Field returns a Def that creates a field-scoped subset. name must be the JSON tag name of a field in the parent struct. All assertions and subsets inside Field receive the extracted field value when validating.

func Object

func Object(defs ...Def) Def

Object returns a Def that groups assertions evaluated against the whole object. It is equivalent to passing the assertions directly to For or When, and exists for organisational clarity.

func When

func When(guard Test, defs ...Def) Def

When returns a Def that conditionally applies its sub-definitions only when test evaluates to true. The test expression is compiled by the parent For call.

type Embeddable

type Embeddable interface {
	Embedded() any
}

Embeddable is implemented by types that wrap a private payload whose rules should be validated as if the payload were at the same JSON level as the wrapper. When the rules traversal encounters a struct that implements Embeddable, it calls Embedded() and recurses into the result without adding any path prefix.

type Fault

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

Fault represents a single rule assertion failure identified by a code and one or more paths. When multiple paths share the same code and message, they are merged into a single Fault. Fault is *not* designed to be instantiated directly, and will be created as part of the validation processes from defined rules.

func (*Fault) Code

func (f *Fault) Code() Code

Code returns the assertion code that produced this fault.

func (*Fault) Error

func (f *Fault) Error() string

Error implements the error interface.

func (*Fault) MarshalJSON

func (f *Fault) MarshalJSON() ([]byte, error)

MarshalJSON encodes the fault as a JSON object with paths, code, and message fields.

func (*Fault) Message

func (f *Fault) Message() string

Message returns the human-readable message associated with this fault.

func (*Fault) Paths

func (f *Fault) Paths() []string

Paths returns the JSON Path (RFC 6901) locations where this fault occurred.

type Faults

type Faults interface {
	error
	HasPath(path string) bool
	HasCode(code Code) bool
	// Len returns the number of faults in the collection.
	Len() int
	// First returns the first fault in the collection.
	First() *Fault
	// Last returns the last fault in the collection.
	Last() *Fault
	// At returns the fault at position i.
	At(i int) *Fault
	// List returns the underlying slice of faults.
	List() []*Fault
}

Faults is the interface for a collection of validation faults. A nil value indicates no faults were found.

func Validate

func Validate(obj any, opts ...WithContext) Faults

Validate uses the global registry of rule sets to validate the provided object. Each registered namespace set is applied in order; the Set.Validate method is responsible for matching the object type, evaluating guard conditions, running assertions, and recursively iterating exported struct fields. Returns nil when no faults are found.

Optional WithContext values inject additional context into the validation session. Context is also collected automatically from the root object's exported fields that implement ContextAdder (e.g. tax.Regime, tax.Addons).

type Set

type Set struct {
	// ID is the namespace for this set of rules, typically a package-level code like "GOBL" or "GOBL-ORG".
	ID Code `json:"id,omitempty"`
	// Package is the short package name used by Register to identify the rule set for generation purposes. It is only set by Register and RegisterWithGuard.
	Package string `json:"package,omitempty"`
	// Object is the fully-qualified Go type name (e.g. "bill.Invoice") that this set of rules applies to. It is set by For and is used for informational purposes.
	Object string `json:"object,omitempty"`
	// FieldName is the JSON tag name of the field this subset is scoped to. When non-empty, Validate extracts this field from the parent object and delegates to it.
	FieldName string `json:"field,omitempty"`
	// Each when true causes Validate to iterate over the slice elements of the field named by FieldName.
	Each bool `json:"each,omitempty"`
	// Guard is an optional expression that determines when this set of rules should be applied. If provided, the set will only be applied when the expression evaluates to true. The expression can reference any exported field from the struct associated with this set of rules.
	Guard Test `json:"guard,omitempty"`
	// Assert is a list of assertions to evaluate directly on the struct associated with this set of rules.
	Assert []*Assertion `json:"assert,omitempty"`
	// Subsets are additional sets of rules to apply recursively to the struct associated with this set of rules. They will be applied in order, and their assertions will be evaluated after the assertions in this set. Subsets can also have their own Test conditions, which will be evaluated independently.
	Subsets []*Set `json:"subsets,omitempty"`
	// contains filtered or unexported fields
}

Set represents a collection of rules grouped by a namespace an associated with a specific struct.

func AllSets

func AllSets() []*Set

AllSets returns all rule sets registered in the global registry.

func For

func For(obj any, defs ...Def) *Set

For creates a new set of rules for the provided object (struct or value type). Each Def is applied in order to build up the set's assertions and subsets. Assert, Field, Each, Object, and When all return Def values that can be passed here.

We let the compiler know that this function should not be "inlined" so that the package the caller is in can be detected reliably at runtime.

func NewSet

func NewSet(ns Code, sets ...*Set) *Set

NewSet creates a standalone namespace set from the given type-bound subsets. It prepends the namespace code to all assertion and set IDs and builds the type index for efficient lookup during validation.

Unlike Register, the returned set is NOT added to the global registry. Use Set.Validate to validate objects against it directly.

The input sets are cloned internally, so the same output of For can safely be passed to multiple NewSet or Register calls.

func Registry

func Registry() []*Set

Registry returns the global registry of rule sets.

func (Set) MarshalJSON

func (s Set) MarshalJSON() ([]byte, error)

MarshalJSON serializes Set to JSON, converting the Test field to its string representation.

func (*Set) Validate

func (s *Set) Validate(obj any, opts ...WithContext) Faults

Validate validates an object against the set's rules. If the set has a test condition (from When), it is evaluated first and the set is skipped when false. Returns nil when no faults are found.

Optional WithContext values inject additional context into the validation session. Context is also collected automatically from the root object's exported fields that implement ContextAdder (e.g. tax.Regime, tax.Addons).

type Test

type Test interface {
	Check(val any) bool
	String() string
}

Test defines an interface expected for a test condition.

type WithContext

type WithContext func(*Context)

WithContext is a functional option for rules.Validate that injects values into the validation context before validation begins.

Directories

Path Synopsis
Package is provides common tests for using inside rule assertions.
Package is provides common tests for using inside rule assertions.

Jump to

Keyboard shortcuts

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