Chapter 02.04 — Invariants#

Ocarina’s sub-DSL for typed, composable invariants. Used everywhere: CLI, POMs, the custom_invariants/testing/ that check suite consistency before any test runs.

Plan#

#FileSubject
0101-validate-flow.mdThe full flow validate(value) → assert_that → execute → raise_if_invalid + the _ValidationChain class.
0202-assertions.mdCatalog of builtin assertions (is_str, is_email, is_positive, is_in, has_unique_elements, each, is_valid_filename, …).
0303-otherwise-any-of.md.otherwise(...) and the OR combination via _any_of.
0404-then-chain-of-validations.md.then(new_value) and chain_validations(...) for composition.
0505-business-vs-framework-validator.mdBusinessInvariantValidator vs FrameworkInvariantValidator
0606-invariant-errors.mdInvariantViolationError, DuplicatesError, AggregateInvariantViolationError.

Why the invariants DSL#

  1. Pre-execution guard. TestSuite.__init__ checks — before launching anything — that test names / IDs are unique, max_workers >= 1, etc.
  2. CLI validation. Every flag carries its validate=lambda chain: chain.assert_that(...) inside the CliStore. Errors aggregate into one message.
  3. POM-side validation. POM actions validate their parameters ("retries must be positive") via validate(retries, name="retries").assert_that(is_positive).execute().raise_if_invalid().

The Holy Book sums it up:

Usable in POMs, validate lets you express invariants as chains. Execution is deferred: you have to call .execute() explicitly.

The result exposes is_valid, errors, and validated_values. It is inert by default. .raise_if_invalid() raises the exception when needed.

Design principles#

PrincipleRealization
Deferred execution.execute() is mandatory. Before that, the chain is composable but inert.
Aggregated errors_ValidationChain runs every step and collects errors; no fail-fast.
Typed composition.then(U) changes the type; the T is preserved through the chain.
Logical OR.otherwise(pred) combines with the previous via _any_of.
Type safetyvalidate("lol").assert_that(is_positive) raises a mypy error (incompatible predicate).
ReusabilityBusinessInvariantValidator.create(...) or FrameworkInvariantValidator.create(...) to factor out an invariant.

The invariants sub-DSL follows the same grammar as ROP:

ROPInvariants
ActionStart[T]ActionFailure[T]ActionSuccess[T]ActionChain[T]ValidationStartBlock[T]ValidationAssertBlock[T]_ValidationResult
Flat composition via chain_actions(*)Flat composition via chain_validations(*)
Lazy evaluation (Thunk[ActionChain[T]])Lazy evaluation (.execute())
Result[T]_ValidationResult (with is_valid, errors, validated_values)
Error aggregated by FailError aggregated by AggregateInvariantViolationError

Same pattern, different domain. See ../03-railway/ for the original.