Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into docs/rules-swift/ as a read-only reference alongside the repo's tailored docs/rules/. PROVENANCE.md records the source, commit, and what was included or excluded; the upstream repo's own VCS, CI, and lint tooling is omitted as machinery rather than rule content. Exclude the vendored tree from the style gate (scripts/check-style.sh) and the pre-commit hook: the canon's enforcement docs quote the forbidden attribution phrases to define them, the same reason the repo's own enforcement files are already excluded.
442 lines
26 KiB
Markdown
442 lines
26 KiB
Markdown
# Validation Rules (MANDATORY)
|
|
|
|
When validating any parsed model (a config, a document, a manifest, a feature
|
|
model, any structured value), you **MUST** follow the **OpenAPIKit validation
|
|
idiom** by [Matt Polzin](https://github.com/mattpolzin/OpenAPIKit). This is the
|
|
companion to `parsing-rules.md`: parse first, validate second, and write the
|
|
validation layer the way OpenAPIKit does. This is not a suggestion. Validations
|
|
are composable values, never imperative if-trees.
|
|
|
|
## Reference repository
|
|
|
|
**https://github.com/mattpolzin/OpenAPIKit** (local mirror:
|
|
`PureYAMLResearch/References/OpenAPIKit`). Study `Sources/OpenAPIKit/Validator/`:
|
|
`Validation.swift`, `Validator.swift`, `Validator+Convenience.swift`,
|
|
`Validation+Builtins.swift`, `ReferenceValidations.swift`, and the tests under
|
|
`Tests/OpenAPIKitTests/Validator/`. Last re-analysed against upstream main
|
|
`1d42ea6477` (2026-06-11); when in doubt, the live source wins over this
|
|
summary.
|
|
|
|
## The philosophy (the eleven principles you must honor)
|
|
|
|
1. **Parse, then validate, as two passes.** The types make many illegal states
|
|
unrepresentable; the validator catches only what the type system cannot or
|
|
should not enforce. Validation runs over an already parsed, well typed value.
|
|
2. **Validations are composable values, not if-trees.** A validation is a plain
|
|
struct of closures (a description, a check, an apply-predicate). You combine
|
|
them with an algebra (`&&`, `||`, `all`, `lift`, `unwrap`, `lookup`). You
|
|
declare what correct looks like; you do not hand write traversal or
|
|
accumulation loops.
|
|
3. **The description states the CORRECT state, never the failure.** Phrase every
|
|
description positively ("Operations contain at least one response"). The
|
|
framework derives the negative message by prefixing `"Failed to satisfy: "`.
|
|
One source of truth, two readings.
|
|
4. **Errors carry a context path.** An error is a reason plus a location (the
|
|
coding path). The path is filled in by the traversal and rendered
|
|
consistently ("at root of document", "at path: .a.b['c']"). Failures are
|
|
locatable without manual bookkeeping.
|
|
5. **Application is predicate gated.** Every validation has a `when:` predicate
|
|
(default true) that runs before the check, with the full context, so a rule
|
|
scopes itself to a subset of values, a coding-path position, or a document
|
|
version without conditionals inside the check body.
|
|
6. **Dispatch is type directed.** A `Validation<Subject>` applies only to values
|
|
of type `Subject`, anywhere they appear in the tree, because the erased
|
|
wrapper filters by runtime type. You pick the subject type; the framework
|
|
finds every instance.
|
|
7. **Default set plus custom extension.** Ship a curated default validator and a
|
|
blank one. Builders are fluent and chainable (`.validating(...)`,
|
|
`.withoutValidating(...)`), returning Self. Users compose policy from defaults
|
|
plus or minus their own rules. Builtins live as `public static var` members of
|
|
a dedicated caseless namespace enum (`BuiltinValidation`), referenceable by
|
|
KeyPath rooted at that namespace so they can be added or removed by identity,
|
|
and the identity key IS the description string (`withoutValidating` removes by
|
|
matching descriptions across every list), so descriptions must be unique and
|
|
stable. Where strictness comes in grades, model the grades as whole-set
|
|
replacement methods on the validator (OpenAPIKit's lenient reference checks
|
|
by default, `validatingAllReferencesFoundInComponents()` swapping in the
|
|
strict set, `skippingReferenceValidations()` clearing it), not as flags
|
|
threaded through individual rules.
|
|
8. **Single-error and multi-error forms.** The Bool form (frontload the
|
|
description, return false if invalid) is for a rule that fails one way and auto
|
|
formats the message. The array-of-errors form is for a rule that can fail in
|
|
several places at once (one error per missing variable, per undefined
|
|
reference), each with its own reason and path. Choose by whether a single value
|
|
can fail in more than one way.
|
|
9. **Two error channels with a strict switch.** Validation errors always throw;
|
|
parse warnings are separate and elevated to errors only under `strict: true`
|
|
(the default), otherwise returned for inspection. Keep "spec violation" and
|
|
"lossy but parseable" distinct.
|
|
10. **One framework, parameterized over the document.** When more than one layer
|
|
validates (an authoring linter over a hand-written manifest, a semantic
|
|
validator over a resolved model), do NOT copy the machinery per layer.
|
|
OpenAPIKit fixes the document type to `OpenAPI.Document`; you generalize it.
|
|
Factor the error type, `Validation`, the erased wrapper, the `Validator`, and
|
|
the combinators into one framework in the lowest layer, parameterized over both
|
|
the `Subject` and the `Document` (the cross-cutting context: a library, a scope,
|
|
or `Void` when a rule needs nothing beyond its subject). Each layer then defines
|
|
only its own document, its rules, and its traversal. Two copies of
|
|
`ValidationError` is the same duplication this rule exists to prevent, one level
|
|
up.
|
|
|
|
11. **Each rule lives in exactly one authoritative layer; lower layers do not
|
|
re-implement it.** A rule belongs to the layer that holds the data it judges. A
|
|
*semantic* rule, one that needs a resolved model (referential closure, scope,
|
|
inheritance, readiness), lives in the semantic layer and nowhere else. A lower
|
|
layer, a syntax or source-structural validator, checks only what is meaningful at
|
|
its own level (an empty identifier, a duplicate authored name) and MUST NOT copy
|
|
the semantic rules down, even when it could approximate them. The reason is the
|
|
dependency arrow: if the semantic layer depends on the syntax layer (parse then
|
|
resolve), the syntax layer cannot call back into the semantic one without a cycle,
|
|
so the temptation is to re-derive the semantic rule in the lower layer, and that
|
|
is the duplication to refuse. The correct direction is the other way: the
|
|
authoritative (higher) layer *consumes* the lower one (it parses, then validates),
|
|
so the full "source to semantic verdict" path lives at the top, not split into two
|
|
drifting rule sets. When you find the same check in two layers, delete the copy in
|
|
the layer that does not own the data, and document the boundary on both sides.
|
|
|
|
## The core types (build these, with these exact roles)
|
|
|
|
- **`ValidationContext<Subject>`**: the read-only bundle every check receives.
|
|
Carries `document` (the whole structure, for cross-cutting checks), `subject`
|
|
(the current value of the specialized type), and `codingPath` (where in the
|
|
tree). Adapt `document` to your root model name.
|
|
- **`Validation<Subject>`**: the atomic value. Stores `description: String`, a
|
|
multi-error `validate: (ValidationContext<Subject>) -> [ValidationError]`, and a
|
|
`predicate: (ValidationContext<Subject>) -> Bool`. Has `apply(to:at:in:)` that
|
|
gates on the predicate then runs validate. Two inits:
|
|
- multi-error: `init(description: String? = nil, check: @escaping (Context) -> [ValidationError], when: @escaping (Context) -> Bool = { _ in true })`.
|
|
- single-error Bool: `init(description: String, check: @escaping (Context) -> Bool, when: ... = { _ in true })`, where false yields one error `"Failed to satisfy: \(description)"`. Description is REQUIRED here because it is the message.
|
|
- **`ValidationError`**: `reason: String` plus `codingPath: [CodingKey]`. Its
|
|
`description` strips a trailing period from the reason, renders an empty path as
|
|
`"<reason> at root of document"` and a non-empty path as
|
|
`"<reason> at path: <path>"`. Match this formatting.
|
|
- **`ValidationErrorCollection`**: the one value thrown at the end, with `.values:
|
|
[ValidationError]`. Tests inspect `.values`.
|
|
- **`Validatable`**: a pure marker protocol constraining `Subject`. Conform your
|
|
model types (and the primitives/containers you traverse) to it.
|
|
- **`AnyValidation`** (type erasure, internal): wraps `Validation<T>` so a
|
|
heterogeneous list lives together, and filters by runtime type. It MUST guard
|
|
against an optional matching its wrapped type (a `T?` must not satisfy a
|
|
`Validation<T>`): cast with `as? T`, then also check `type(of: subject) == type(of: input)`.
|
|
- **`Validator`**: a `final class` (reference semantics; the fluent builders
|
|
mutate self and return it, `@discardableResult`). Holds the default rules in
|
|
TWO tiers (`nonReferenceDefaultValidations` + `referenceDefaultValidations`)
|
|
plus `customValidations`; the effective order is fixed and tested:
|
|
non-reference defaults, then reference defaults, then custom. Exposes `init()`
|
|
(defaults on), a `blank` with none, fluent `validating(...)` /
|
|
`withoutValidating(...)`, and a `validationDescriptions` accessor listing the
|
|
active rules. The builtin add/remove overloads take VARIADIC KeyPaths via
|
|
parameter packs (`validating<each T: Encodable>(_ validations: repeat
|
|
KeyPath<BuiltinValidation.Type, Validation<each T>>)`), so one call can mix
|
|
builtins specialised on different subject types.
|
|
- **`BuiltinValidation`**: the caseless namespace enum carrying every shipped
|
|
rule as a `public static var ...: Validation<Subject>`. Rule FAMILIES that
|
|
differ only by type are generated by an internal factory (OpenAPIKit's
|
|
`References.referencesAreValid(ofType:named:mustBeInternal:mustPointToComponents:)`
|
|
computes graded descriptions and checks per component type) rather than
|
|
copy-pasted per type.
|
|
|
|
## How validation runs
|
|
|
|
Traverse the parsed model and offer every value to every validation; the type
|
|
filter in the erased wrapper makes each `Validation<T>` fire only on `T` values,
|
|
at every path where one occurs. OpenAPIKit does this by walking an `Encodable`
|
|
through a fake `Encoder` that, at each node, applies validations and extends the
|
|
coding path by the key or index. Dictionary keys are themselves validated: the
|
|
keyed container's child encoder recovers the original key value (`AnyCodingKey
|
|
.originalValue as? Validatable`) and offers it to the validations too. You may
|
|
reuse that Encoder trick when your model is `Encodable`, or write an explicit
|
|
recursive walk that builds the path and dispatches by subject type. Either way:
|
|
predicate gates before check, errors accumulate with their path, and the whole
|
|
run throws a single `ValidationErrorCollection` if non-empty.
|
|
|
|
The warning channel rides the same traversal: types declare their parse-time
|
|
warnings via small protocols (`HasWarnings`, and `HasConditionalWarnings` for
|
|
warnings that depend on the whole document), the walker collects them at each
|
|
node and contextualises each with the coding path where it was found. The
|
|
entrypoint is `validate(using:strict:) -> [Warning]` (`@discardableResult`):
|
|
under `strict: true` (the default) warnings are converted into
|
|
`ValidationError`s and merged into the thrown collection; under `strict: false`
|
|
they are returned for inspection instead.
|
|
|
|
## Authoring a validation (every surface)
|
|
|
|
- **Multi-error init**: return your own `[ValidationError]`, one per problem, each
|
|
with a custom reason and `context.codingPath`. Use when a value can fail several
|
|
ways at once.
|
|
- **Single-error Bool init**: pass a positive `description` and a Bool check;
|
|
failure auto-produces `"Failed to satisfy: <description>"`. The dominant form.
|
|
- **`.validating(...)` on the Validator**: add a rule inline without constructing
|
|
a `Validation` (closure form, Bool form, prebuilt form, and a KeyPath form that
|
|
adds named builtins). Mirror these overloads.
|
|
- **`lift(\.child, into: validationsOnChild...)`**: run child-typed validations
|
|
against a lifted value while keeping the parent's path and document. The key
|
|
compositional move.
|
|
|
|
Real exemplar (single-error, KeyPath check):
|
|
|
|
```swift
|
|
public static var operationsContainResponses: Validation<Operation> {
|
|
.init(
|
|
description: "Operations contain at least one response",
|
|
check: \.responses.count > 0
|
|
)
|
|
}
|
|
```
|
|
|
|
Real exemplar (multi-error, one error per offender):
|
|
|
|
```swift
|
|
public static var serverVariablesAreDefined: Validation<Server> {
|
|
.init(
|
|
description: "All server template variables are defined",
|
|
check: { context in
|
|
context.subject.urlTemplate.variables
|
|
.filter { !context.subject.variables.contains(key: $0) }
|
|
.map { name in
|
|
ValidationError(
|
|
reason: "Server Object does not define the variable '\(name)' ...",
|
|
at: context.codingPath
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
```
|
|
|
|
## The combinator algebra (the declarative core)
|
|
|
|
Provide these as free functions returning closures, so they compose by value:
|
|
|
|
- **`&&` / `||`** on error-array checks (`&&` concatenates both error lists; `||`
|
|
short-circuits, errors only if both fail) and on Bool predicates (the obvious
|
|
way).
|
|
- **`==`, `!=`, `>`, `>=`, `<`, `<=`** lifting a `KeyPath<ValidationContext<T>, U>`
|
|
or a `KeyPath<T, U>` (rooted through `context.subject`) and a literal into a
|
|
predicate. The dual rooting lets an author write `\.subject.foo`, `\.document.bar`,
|
|
or `\SomeType.foo` interchangeably and thereby tell the type system which type the
|
|
validation specializes on.
|
|
- **`take(\.path) { value in Bool }`**: dig to a value and run arbitrary logic
|
|
beyond equality.
|
|
- **`lift(\.child, into:)`**: as above.
|
|
- **`unwrap(\.optional, into:description:)`**: unwrap an optional, error if nil,
|
|
else run child validations.
|
|
- **`lookup` / `unwrapAndLookup`**: resolve a reference against the document's
|
|
component store, error if not found, else validate the resolved value.
|
|
- **`all(validations...)`**: apply many `Validation<T>` to the same context
|
|
(equivalent to `lift` with `\.self`).
|
|
|
|
Usage reads declaratively: `check: \.responseOutcomes.count >= 1 && { $0.subject.responseOutcomes.allSatisfy { $0.status == 200 } }`,
|
|
or `check: take(\.tags) { tags in tags.map(\.name).count == tags?.count }`.
|
|
|
|
One type-inference contract to honour between the two clauses: the subject type
|
|
must be pinned in at least one of `check:` / `when:`. When one clause only
|
|
touches `\.document` or `\.codingPath` (which say nothing about the subject),
|
|
the other clause must name the subject type, for example via a fully rooted
|
|
KeyPath (`take(\OpenAPI.Document.servers) { ... }`), so the compiler knows what
|
|
the validation specialises on. Both `take` and every comparison operator come
|
|
in two rootings for exactly this reason (`KeyPath<Subject, U>` and
|
|
`KeyPath<ValidationContext<Subject>, U>`).
|
|
|
|
## The test style (MANDATORY, copy it exactly)
|
|
|
|
Tests mirror the philosophy. The recipe:
|
|
|
|
1. **Build the subject** (the document / model), seeding the values under test.
|
|
2. **Build a validator**: `Validator.blank.validating(...)` to test ONE rule in
|
|
isolation, `Validator()` to test the default set, or `Validator().validating(...)`
|
|
for a custom rule on top of defaults.
|
|
3. **Run**: `try model.validate(using: validator)` (success tests simply run; a
|
|
throw fails the test).
|
|
4. **Assert failures** with `XCTAssertThrowsError`, cast to
|
|
`ValidationErrorCollection`, then check `.values.count`, each
|
|
`.values[i].reason` (including the `"Failed to satisfy: "` prefix), and
|
|
`.codingPath.map { $0.stringValue }`. For message rendering, assert
|
|
`String(describing: error)` equals `"<reason> at path: <path>"`.
|
|
5. **Isolate the combinators**: test each operator/`lift`/`take` as a pure
|
|
function against a hand-built dummy context, asserting `.isEmpty` / `.count` of
|
|
the resulting closure, with no full traversal. Test the erased wrapper directly
|
|
(`apply(to:at:in:)`), including that an optional and a wrong type both yield no
|
|
errors, and that the same value of the same type twice yields two errors.
|
|
|
|
Representative failure test to copy:
|
|
|
|
```swift
|
|
func test_unconditionalServerCountCheckFails() {
|
|
let document = ... // seed two server arrays
|
|
let validator = Validator.blank
|
|
.validating("All server arrays have more than 1 server", check: \[Server].count > 1)
|
|
XCTAssertThrowsError(try document.validate(using: validator)) { error in
|
|
let error = error as? ValidationErrorCollection
|
|
XCTAssertEqual(error?.values.count, 1)
|
|
XCTAssertEqual(error?.values.first?.reason, "Failed to satisfy: All server arrays have more than 1 server")
|
|
XCTAssertEqual(error?.values.first?.codingPath.map { $0.stringValue }, ["paths", "/hello/world", "get", "servers"])
|
|
}
|
|
}
|
|
```
|
|
|
|
(Use Swift Testing `#expect` where the project uses Swift Testing, but keep the
|
|
same shape: run the validator, assert the collection's count, each reason, each
|
|
path.)
|
|
|
|
## Exhaustive testing (MANDATORY, the upstream bar)
|
|
|
|
The recipe above applied to a SAMPLE of rules does not satisfy this rule. The
|
|
upstream suite is exhaustive in five specific ways, and a conforming repo
|
|
reproduces all five:
|
|
|
|
1. **Every rule ships a failing AND a succeeding test**, the upstream
|
|
`test_X_fails` / `test_X_succeeds` pairing (BuiltinValidationTests covers
|
|
every builtin both ways). The succeeding fixture is a NEAR MISS, the boundary
|
|
the rule almost trips (the value at the exact limit, the predicate one step
|
|
from firing), not merely an unrelated clean document. A rule with only its
|
|
failing direction tested is untested: nothing proves it stays quiet on valid
|
|
input.
|
|
2. **A configuration-pin test** asserts the EXACT ordered
|
|
`validationDescriptions` list (and count) of every validator configuration:
|
|
`blank` is empty, each fluent variant lists precisely its rules, the full
|
|
default set is pinned description by description (upstream
|
|
`test_variousConfigurationsHaveExpectedValidationCounts`). Adding, removing,
|
|
or rewording a rule MUST fail a test before the change is deliberate.
|
|
3. **The machinery has its own negative tests** (upstream ValidationTests):
|
|
the erased wrapper applied to an Optional subject yields no errors; applied
|
|
to a wrong type yields no errors; a false `when` predicate yields no errors;
|
|
a conditional rule gets a dedicated does-not-run test
|
|
(`test_validationNeverRunsAndSucceeds`); and the same value of the same type
|
|
occurring twice yields two errors (the positive control proving the
|
|
negatives are not vacuous).
|
|
4. **Many-error documents assert the COMPLETE error list**: exact
|
|
`.values.count`, then every reason and every coding path, in traversal order
|
|
(upstream asserts all nine reference errors with all nine paths in one
|
|
test). `contains` on a single element hides regressions in every other
|
|
error and in the ordering.
|
|
5. **Coverage is itself a tested law.** Keep a registry of every finding the
|
|
default set can produce (stable codes, or the descriptions list where codes
|
|
do not exist) and a meta-test that one failing fixture and one near-miss
|
|
fixture exist per entry, asserting set equality and naming any gap in the
|
|
failure message. Plant a fake entry once to prove the meta-test fails, then
|
|
remove it. Upstream pins this via the exact descriptions list; a registry
|
|
plus meta-test is the equivalent for code-bearing findings.
|
|
|
|
## Full validation coverage (MANDATORY)
|
|
|
|
The exhaustive-testing section proves every *rule* is tested. This proves the
|
|
rule *set* is complete over the input model: no field goes unexamined. The
|
|
OpenAPIKit idiom validates the whole document structure, not a chosen subset; a
|
|
conforming repo does the same.
|
|
|
|
1. **Every field is modeled or reported, never silently ignored.** For each
|
|
field or construct the input can carry, the parsed model either handles it (it
|
|
has a typed home and the validations that constrain it) or the validator emits
|
|
an explicit "unsupported" / "ignored, with reason" finding. A field that is
|
|
neither handled nor reported is a silent gap and a violation, even when the
|
|
output looks right. This is the `…FieldsAreModeledOrReported` discipline: layer,
|
|
transform, shape, style, mask, matte, and silent-risk fields each get a rule
|
|
that accounts for every key.
|
|
2. **Unknown and unexpected fields are detected, not dropped.** A rule asserts
|
|
that every key present is either known (modeled) or explicitly permitted
|
|
metadata, and reports an unrecognized key with its path. The parser must not
|
|
`try?`-swallow or otherwise discard a field the validator should have seen
|
|
(see `core/no-shortcuts-first-principles.md`: silent drop is forbidden).
|
|
3. **Behavior-affecting fields get priority and a silent-risk rule.** Any field
|
|
that can change output but is easy to miss (a compositing mode, a matte source,
|
|
a time remap) carries a dedicated rule so its presence is never accepted
|
|
unhandled.
|
|
4. **Coverage over the model is a tested law.** Keep a registry of every field or
|
|
construct the input model defines, and a meta-test asserting each maps to a
|
|
validation or an explicit unsupported/ignored classification. Adding a field to
|
|
the model without classifying it MUST fail that meta-test. This is the
|
|
input-side twin of the finding-coverage meta-test above: that one proves every
|
|
rule is exercised; this one proves every field has a rule.
|
|
|
|
A validator can pass every test it ships and still be incomplete if a field of
|
|
the input was never given a rule. Full coverage closes that gap: the validation
|
|
set is complete with respect to the input model, and incompleteness is caught
|
|
mechanically, not by review.
|
|
|
|
## Every public type is validated or excluded (MANDATORY)
|
|
|
|
This is the part it is most important to stick to. Validation is not applied to a
|
|
chosen subset of types. It is applied to EVERY public type. A package's public
|
|
surface is the set of values other code can construct and hand in, and each one
|
|
is a place malformed or untrusted state can enter, so each public type is exactly
|
|
one of two things:
|
|
|
|
- **validated**: it has at least one `Validation<Subject>` that constrains it, or
|
|
- **excluded**: it is listed in a checked-in exclusions file with a one-line
|
|
reason (a pure data-transfer type with no invariants, or a type whose
|
|
invariants are already made unrepresentable by its initializer).
|
|
|
|
There is no third option. A public type that is neither validated nor excluded
|
|
with a reason is a silent gap, and a silent gap is a violation even when every
|
|
shipped test passes.
|
|
|
|
**Enforce it mechanically, per repo, not by review.** Ship a coverage gate (a
|
|
script run by the pre-commit hook and in CI) that: (1) enumerates every public
|
|
type in the package, (2) checks each against the validator's subject types and
|
|
the exclusions file, and (3) fails the build, naming the type, if any public type
|
|
is neither covered nor excluded. Adding a new public type without a validation or
|
|
an exclusion entry MUST fail the gate. That is what "stick to it" means: the
|
|
discipline is enforced by the build, every time, for every public type.
|
|
|
|
**Prefer make-unrepresentable over validate.** The strongest coverage is deleting
|
|
the rule. If a type can carry its own invariant (a non-empty collection type, a
|
|
smart constructor that rejects the bad value at init), the bad state cannot be
|
|
constructed and needs no runtime validation. Excluding such a type because its
|
|
invariants are unrepresentable is the win, not a loophole. Reach for a
|
|
`Validation` only when the type system cannot or should not carry the invariant.
|
|
|
|
## DO
|
|
|
|
- Separate parsing from validation; validate an already typed value.
|
|
- Write each rule as a `Validation<Subject>` value with a positive description.
|
|
- Build errors as reason plus context path; let the traversal fill the path.
|
|
- Use the single-error Bool form by default; the multi-error form when a value can
|
|
fail several ways at once.
|
|
- Gate scope with the `when:` predicate, not with `if` inside the check.
|
|
- Compose with the operator algebra (`&&`, `lift`, `take`, `unwrap`, `lookup`,
|
|
`all`) instead of nesting conditionals.
|
|
- Ship a default validator plus a blank one; make rules addable and removable by
|
|
identity.
|
|
- In tests, isolate one rule with `.blank.validating(...)`, assert the error
|
|
collection's count, reasons, and coding paths, and unit test the combinators as
|
|
pure functions.
|
|
- When a second layer needs validation, reuse one framework parameterized over the
|
|
document (principle 10); never copy the error type, `Validation`, or `Validator`.
|
|
|
|
## DON'T
|
|
|
|
- Do not fold validation into parsing.
|
|
- Do not write a monolithic `validate()` that appends to a mutable error array
|
|
through a tree of `if` statements. That is the anti-pattern this rule exists to
|
|
replace.
|
|
- Do not phrase descriptions as failures; phrase the correct state.
|
|
- Do not drop the location; every error carries its path.
|
|
- Do not let an optional satisfy a non-optional validation (guard the erasure).
|
|
|
|
## Acceptance check
|
|
|
|
A repo whose validation conforms to this rule satisfies all of: (1) validations
|
|
are declared as `Validation<Subject>` values (grep finds the type, not a single
|
|
imperative `func validate()` accumulating into a `var errors`); (2) descriptions
|
|
read as the correct state, are unique (they are the removal identity), and
|
|
failures render `"Failed to satisfy: ..."`; (3) errors carry a coding path and
|
|
render `"... at path: ..."`; (4) a `Validator` with a default set and a `blank`,
|
|
plus fluent `validating` / `withoutValidating`; (5) tests build the subject, run
|
|
`validate(using:)`, and assert the thrown collection's `.values` count, reasons,
|
|
and coding paths, with the combinators unit tested as pure functions; (6) every
|
|
public entry point that consumes untrusted input gates on the validator before
|
|
doing work, and a non-throwing recovery accessor (a `validationOutcome` style
|
|
API returning valid-or-errors) exists beside the throwing gate for callers that
|
|
inspect rather than abort; (7) the test suite is exhaustive per the section
|
|
above: a failing plus near-miss succeeding pair for every rule, a
|
|
configuration-pin test over exact validation descriptions, machinery negatives
|
|
(optional, wrong type, false predicate, does-not-run), complete error-list
|
|
assertions on many-error documents, and a coverage meta-test (or descriptions
|
|
pin) that names any rule shipped without its fixtures; (8) the validation set is
|
|
complete over the input model per the full-coverage section: every field is
|
|
modeled or explicitly reported, unknown keys are detected, and a coverage
|
|
meta-test fails if any field of the model lacks a rule. A validator that is one
|
|
big if-tree fails this rule even if it produces correct results, and so does a
|
|
validator that quietly ignores fields the input can carry.
|