Files
2026-05-27 23:29:53 +01:00

8.9 KiB

validation

Purpose

Self-contained runtime schema validation for the Suaveplan monorepo with a Zod-compatible API surface — parse, safeParse, parseAsync, safeParseAsync, refine, transform, optional, nullable — implemented independently of Zod. Provides exhaustive schema types (string, number, boolean, date, array, object with strip/strict/passthrough modes, record, tuple, union, intersection, literal, enum), async refinements, lifecycle hooks, and structured ValidationError codes under the VALIDATION.* namespace.

Requirements

VL-01 — Self-contained schema engine

@suaveplan/validation is its own schema parser — it does NOT depend on Zod. It exposes a Zod-compatible API surface (parse, safeParse, parseAsync, safeParseAsync, refine, transform, optional, nullable) but is implemented independently.

Verified by: tests/dep-graph.test.ts AND src/core/schema/schema.spec-conformance.test.ts::"string().parse returns string and accepts unknown input" AND src/core/schema/schema.spec-conformance.test.ts::"refine, transform, optional, nullable are present".

VL-02 — Never-throw contract

.parse() and .parseAsync() MUST throw ValidationError on failure (never raw errors). .safeParse() and .safeParseAsync() MUST return ParseResult<T> and NEVER throw.

Verified by: src/core/schema/schema.test.ts::"throws ValidationError for invalid input" AND src/core/schema/schema.test.ts::"returns failure for invalid input" AND src/core/schema/schema.spec-conformance.test.ts::"safeParse never throws on bizarre input" AND src/core/schema/schema.spec-conformance.test.ts::"safeParseAsync never throws on bizarre input".

VL-03 — ValidationError shape

ValidationError MUST extend SuaveplanError and carry an issues: ValidationIssue[] array. .flatten() MUST return FlattenedErrors with formErrors (top-level issues) and fieldErrors (keyed by path). .format() MUST return a nested error tree. Error codes under VALIDATION.* namespace.

Verified by: src/errors/errors.test.ts (full suite — extends SuaveplanError, flatten / format, code registry).

VL-04 — String schema constraints

StringSchema MUST enforce: min, max, length, regex, email, url, uuid, startsWith, endsWith, includes. Transform helpers trim, toLowerCase, toUpperCase MUST mutate the output value, not fail.

Verified by: src/schemas/string/string.test.ts (full suite — min / max / length / regex / email / url / uuid / startsWith / endsWith / includes + trim / toLowerCase / toUpperCase).

VL-05 — Number schema constraints

NumberSchema MUST enforce: min, max, int, positive, negative, multipleOf, finite.

Verified by: src/schemas/number/number.test.ts (full suite — min / max / int / positive / negative / multipleOf / finite).

VL-06 — Object schema modes

ObjectSchema MUST support three unknown-key modes: strip (default, drops unknown keys), strict (fails on unknown keys), passthrough (includes unknown keys). .pick(), .omit(), .partial(), .required(), .extend(), .merge() MUST each return a new ObjectSchema without mutating the original.

Verified by: src/schemas/object/object.test.ts (full suite — strip / strict / passthrough + pick / omit / partial / required / extend / merge).

VL-07 — Union short-circuit

UnionSchema MUST attempt each arm in order and return the result of the first successful parse. If no arm matches, it MUST return ParseFailure with all arm errors listed as issues.

Verified by: src/schemas/union/union.test.ts (full suite — first-match short-circuit + aggregated arm errors).

VL-08 — Async refinements

.refine(fn) MUST accept both synchronous and async predicate functions. Async refinements are awaited via .parseAsync() / .safeParseAsync(). The synchronous parse paths (.parse() / .safeParse()) MUST NOT silently skip async refinements: see VL-13.

Verified by: src/core/schema/schema.test.ts::"async refinement passes" AND src/core/schema/schema.test.ts::"async refinement fails" AND src/core/schema/schema.test.ts::"sync parse throws ASYNC_REFINEMENT_REQUIRES_PARSEASYNC when an async refinement is present".

VL-09 — Coerce utilities

coerce(schema) MUST coerce string inputs to the target type before parsing. coerceString, coerceNumber, coerceBoolean, coerceDate are pure functions; a failed coercion returns null (which the wrapper translates into ParseFailure) rather than throwing. coerceString returns null for non-coercible inputs (symbol); other inputs are coerced via String().

Verified by: src/utils/coerce.test.ts (full suite — coerceString / coerceNumber / coerceBoolean / coerceDate + coerce wrapper) AND src/utils/coerce.test.ts::"returns null for symbol input (non-coercible per VL-09)".

VL-10 — Hooks

Schemas MUST support optional lifecycle hooks { beforeValidate, afterValidate, onError } settable via a .withHooks(hooks) method on BaseSchema that returns an immutable clone with the supplied hooks merged onto any pre-existing hooks (per-key replace semantics). Hooks are called from every terminal branch of parse / safeParse / parseAsync / safeParseAsync: beforeValidate is called before _parse, afterValidate on successful resolution, onError immediately before ValidationError is thrown / surfaced. In the synchronous path the hook return value (including any returned promise) is discarded; in the async path returned promises are awaited.

Verified by: src/core/schema/schema.spec-conformance.test.ts::"calls beforeValidate and afterValidate on successful sync parse" AND src/core/schema/schema.spec-conformance.test.ts::"calls onError on sync parse failure" AND src/core/schema/schema.spec-conformance.test.ts::"calls onError when sync refinement fails" AND src/core/schema/schema.spec-conformance.test.ts::"awaits async beforeValidate/afterValidate in parseAsync" AND src/core/schema/schema.spec-conformance.test.ts::"awaits async onError on async parse failure (type mismatch)" AND src/core/schema/schema.spec-conformance.test.ts::"awaits async onError when async refinement fails" AND src/core/schema/schema.spec-conformance.test.ts::"merges hooks across multiple withHooks calls (clone path)".

VL-11 — OTel instrumentation

validation.parse.success and validation.parse.failure counters and validation.parse.duration_ms histogram MUST be emitted via @opentelemetry/api from every terminal branch of BaseSchema.parse and BaseSchema.parseAsync (not opt-in). Each metric carries a schema.type attribute equal to this.constructor.name (e.g. "StringSchema", "ObjectSchema"). Duration is measured via performance.now(). All emission is a no-op when no MeterProvider is registered.

Verified by: src/core/schema/schema.spec-conformance.test.ts::"emits validation.parse.success on successful parse" AND src/core/schema/schema.spec-conformance.test.ts::"emits validation.parse.failure on failed parse" AND src/core/schema/schema.spec-conformance.test.ts::"emits failure on sync refinement failure" AND src/core/schema/schema.spec-conformance.test.ts::"emits success on async parse + emits duration histogram" AND src/core/schema/schema.spec-conformance.test.ts::"emits failure on async parse failure (type mismatch)" AND src/core/schema/schema.spec-conformance.test.ts::"emits failure on async refinement failure" AND src/otel/validation-otel.test.ts (no-op + lazy init).

VL-12 — sideEffects: false + coverage 100%

"sideEffects": false with tree-shake test. Vitest thresholds at 100/100/100/100. No .skip / .only in src/.

Verified by: tests/tree-shake.test.ts::"sideEffects is false" AND coverage gate bunx turbo test:coverage --filter=@suaveplan/validation.

VL-13 — Async-refinement loud-fail in synchronous parse

BaseSchema.parse and BaseSchema.safeParse MUST refuse to run when any refinement attached to the schema is async. parse MUST throw a ValidationError whose first issue carries the code VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC. safeParse MUST return ParseFailure with the same code. This eliminates the prior silent-skip behaviour where async predicates' promises were discarded and the value was reported valid regardless of the predicate's eventual result, which is a security-grade footgun for revocation / uniqueness / rate-limit checks. parseAsync / safeParseAsync are unchanged — they continue to await async refinements normally.

Verified by: src/core/schema/schema.test.ts::"sync parse throws ASYNC_REFINEMENT_REQUIRES_PARSEASYNC when an async refinement is present" AND src/core/schema/schema.test.ts::"safeParse returns ASYNC_REFINEMENT_REQUIRES_PARSEASYNC failure when async refinement is present" AND src/core/schema/schema.test.ts::"async refinement passes" AND src/core/schema/schema.test.ts::"async refinement fails".