first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
# @suaveplan/validation - Runtime schema validation with Zod-compatible API — self-contained parse engine, exhaustive schema types, async refinements, hooks, structured errors
## Summary
Runtime schema validation with a Zod-compatible API. This package is a self-contained parse engine — it does not import Zod or any other validation library — so it is safe to use in environments where Zod is absent or where bundle size is a constraint. It covers the full schema surface expected from a production validator: all primitive and composite types, `parse`/`safeParse`/`parseAsync`, `refine` (sync + async), `transform`, `describe`, object shape builders (`pick`, `omit`, `partial`, `extend`, `merge`), three unknown-key modes, async lifecycle hooks, and composable helpers (`pipe`, `mergeResults`). Structured errors surface via `@suaveplan/error` with stable `VALIDATION.*` codes. When Zod is already a project dependency and you need its ecosystem (inference helpers, plugins), use Zod directly; use this package for internal platform code and zero-dep environments.
<!-- begin:auto-docs -->
## Documentation tree
- [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validation-otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [common](./src/types/common.md) — `common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes,…
- [validation-hooks](./src/types/validation-hooks.md) — `validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers…
- [coerce](./src/utils/coerce.md) — The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They…
- [compose](./src/utils/compose.md) — The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` ag…
<!-- end:auto-docs --> - [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — `common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes,…
- [validation-hooks](./src/types/validation-hooks.md) — `validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers…
- [coerce](./src/utils/coerce.md) — The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They…
- [compose](./src/utils/compose.md) — The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` ag…
- [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — `common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes,…
- [validation-hooks](./src/types/validation-hooks.md) — `validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers…
- [coerce](./src/utils/coerce.md) — The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They…
- [compose](./src/utils/compose.md) — The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` ag…
- [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [validation-otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — `common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes,…
- [validation-hooks](./src/types/validation-hooks.md) — `validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers…
- [coerce](./src/utils/coerce.md) — The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They…
- [compose](./src/utils/compose.md) — The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` ag…
- [schema](./src/core/schema/schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validation-otel](./src/otel/validation-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_ - [array](./src/schemas/array/array.md) — <!-- begin:auto-docs --> _(no submodules)_- [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_ - [boolean](./src/schemas/boolean/boolean.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ - [date](./src/schemas/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_ - [enum](./src/schemas/enum/enum.md) — <!-- begin:auto-docs --> _(no submodules)_- [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_ - [intersection](./src/schemas/intersection/intersection.md) — <!-- begin:auto-docs --> _(no submodules)_- [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_ - [literal](./src/schemas/literal/literal.md) — <!-- begin:auto-docs --> _(no submodules)_- [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_ - [nullable](./src/schemas/nullable/nullable.md) — <!-- begin:auto-docs --> _(no submodules)_- [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_ - [number](./src/schemas/number/number.md) — <!-- begin:auto-docs --> _(no submodules)_- [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_ - [object](./src/schemas/object/object.md) — <!-- begin:auto-docs --> _(no submodules)_- [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_ - [optional](./src/schemas/optional/optional.md) — <!-- begin:auto-docs --> _(no submodules)_- [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_ - [record](./src/schemas/record/record.md) — <!-- begin:auto-docs --> _(no submodules)_- [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_ - [string](./src/schemas/string/string.md) — <!-- begin:auto-docs --> _(no submodules)_- [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_ - [tuple](./src/schemas/tuple/tuple.md) — <!-- begin:auto-docs --> _(no submodules)_- [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_ - [union](./src/schemas/union/union.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — `common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes,…
- [validation-hooks](./src/types/validation-hooks.md) — `validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers…
- [coerce](./src/utils/coerce.md) — The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They…
- [compose](./src/utils/compose.md) — The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` ag…
## Features
- Exhaustive schema types: `string`, `number`, `boolean`, `date`, `array`, `object`, `record`, `tuple`, `literal`, `enum`, `union`, `intersection`, `optional`, `nullable`
- `parse()` / `safeParse()` / `parseAsync()` / `safeParseAsync()` — `parse()` and `safeParse()` throw `VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC` when the schema carries any async refinement (no silent skip; use `parseAsync()` for async-refined schemas)
- `refine()` (sync and async), `transform()`, `describe()`
- Object shape builders: `pick`, `omit`, `partial`, `extend`, `merge`, three unknown-key modes
- Input coercion utilities: `coerce`, `coerceString`, `coerceNumber`, `coerceBoolean`, `coerceDate`
- Functional combinators: `mergeResults`, `pipe`
- Structured errors: `ValidationError` with `flatten()` and `format()`, 21 typed codes
- OTel instrumentation: success/failure counters and duration histogram
- `Infer<S>` type helper for extracting output types
## Quick Start
```typescript
import { object, string, number, array, Infer } from "@suaveplan/validation";
const UserSchema = object({
id: string().uuid(),
name: string().min(1).max(100),
age: number().int().min(0),
tags: array(string()).max(10),
});
type User = Infer<typeof UserSchema>;
const result = UserSchema.safeParse({ id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice", age: 30, tags: [] });
if (result.success) {
console.log(result.data);
} else {
const { formErrors, fieldErrors } = result.error.flatten();
console.error(formErrors, fieldErrors);
}
```
## Module Docs
- [Core engine](src/core/schema/schema.md)
- [Errors](src/errors/errors.md)
- [StringSchema](src/schemas/string/string.md)
- [NumberSchema](src/schemas/number/number.md)
- [BooleanSchema](src/schemas/boolean/boolean.md)
- [DateSchema](src/schemas/date/date.md)
- [ArraySchema](src/schemas/array/array.md)
- [ObjectSchema](src/schemas/object/object.md)
- [RecordSchema](src/schemas/record/record.md)
- [TupleSchema](src/schemas/tuple/tuple.md)
- [OptionalSchema](src/schemas/optional/optional.md)
- [NullableSchema](src/schemas/nullable/nullable.md)
- [LiteralSchema](src/schemas/literal/literal.md)
- [EnumSchema](src/schemas/enum/enum.md)
- [UnionSchema](src/schemas/union/union.md)
- [IntersectionSchema](src/schemas/intersection/intersection.md)
- [Coerce utilities](src/utils/coerce.md)
- [Compose utilities](src/utils/compose.md)
- [OTel instrumentation](src/otel/validation-otel.md)

View File

@@ -0,0 +1,117 @@
# BaseSchema — Core Parse Engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BaseSchema` is the abstract foundation of the entire `@suaveplan/validation` parse engine. Every schema type (`StringSchema`, `ObjectSchema`, `UnionSchema`, etc.) extends this class and implements a single `_parse` method. The class provides the full public API — `parse`, `safeParse`, `parseAsync`, `safeParseAsync`, `refine`, `transform`, `optional`, `nullable`, and `describe` — without any schema-specific logic leaking into the base.
The design is intentionally minimal: the parse path is a straight function call tree with no event emitter overhead, no try/catch inside `_parse`, and no dynamic dispatch. The sentinel symbol `INVALID` avoids null/undefined ambiguity and eliminates boxing of failure states.
## Features
- `parse(input)` — throws `ValidationError` on failure (synchronous)
- `safeParse(input)` — returns `ParseResult<T>` (never throws)
- `parseAsync(input)` — async variant, awaits async refinements
- `safeParseAsync(input)` — async safe variant
- `refine(fn, message)` — append a synchronous or async refinement predicate
- `transform(fn)` — produce a `TransformSchema` wrapping this schema
- `optional()` — produce an `OptionalSchema` wrapping this schema
- `nullable()` — produce a `NullableSchema` wrapping this schema
- `describe(description)` — attach a documentation string (returns clone)
- Immutable builder pattern: all builder methods return a new instance
## Usage: Basic Example
```typescript
import { string } from "@suaveplan/validation";
const schema = string().min(3).max(50);
const result = schema.safeParse("hello");
// result.success === true, result.data === "hello"
```
## Usage: Advanced Example
```typescript
import { string } from "@suaveplan/validation";
// Chain refinements, transforms, and optional wrapping
const emailSchema = string()
.email()
.toLowerCase()
.refine((v) => !v.endsWith("@blocked.com"), "Blocked domain")
.describe("User email address");
const optEmail = emailSchema.optional();
// Async refinement (e.g., check uniqueness)
const uniqueEmail = emailSchema.refine(
async (email) => {
// simulate async check
return email !== "taken@example.com";
},
"Email already in use",
);
const result = await uniqueEmail.safeParseAsync("user@example.com");
```
## API Reference
### `abstract _parse(input: unknown, ctx: ParseContext): TOutput | INVALID`
Must be implemented by subclasses. Push issues to `ctx.issues` and return `INVALID` on failure.
### `parse(input: unknown): TOutput`
Runs `_parse` and synchronous refinements. Throws `ValidationError` if any issue is collected. **Throws `ValidationError(VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC)` if the schema carries any async refinement** — the previous silent-skip behavior was a security footgun (e.g. an async revocation check would never run, returning valid for revoked tokens). Use `parseAsync()` whenever a schema has async refinements.
### `safeParse(input: unknown): ParseResult<TOutput>`
Same as `parse` but wraps all failures in a `ParseFailure` result; never throws. The async-refinement guard from `parse()` is preserved: if the schema carries any async refinement, the result is a `ParseFailure` with `code === "VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC"` rather than a silent skip.
### `parseAsync(input: unknown): Promise<TOutput>`
Like `parse` but awaits async refinements. Throws `ValidationError` on failure.
### `safeParseAsync(input: unknown): Promise<ParseResult<TOutput>>`
Like `safeParse` but async-safe.
### `refine(fn, message?): this`
Appends a refinement to this schema. Sync functions run in both `parse()` and `parseAsync()`. Async functions run **only** in `parseAsync()` / `safeParseAsync()`; calling `parse()` / `safeParse()` against a schema that carries any async refinement throws `VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC` (the engine refuses to silently skip an unverifiable check).
- `fn: RefineFn<T> | AsyncRefineFn<T>` — predicate returning boolean or Promise<boolean>
- `message?: string` — error message when predicate returns false (default: `"Refinement failed"`)
### `transform<U>(fn: TransformFn<TOutput, U>): TransformSchema<TOutput, U>`
Returns a new schema that passes the parsed value through `fn`.
### `optional(): OptionalSchema<TOutput>`
Returns a schema that accepts `undefined` in addition to the inner type.
### `nullable(): NullableSchema<TOutput>`
Returns a schema that accepts `null` in addition to the inner type.
### `describe(description: string): this`
Returns a clone of the schema with `_description` set.
### `withHooks(hooks: ValidationHookMap<TOutput>): this`
Attach lifecycle hooks. Returns an immutable clone with the supplied hooks merged onto any pre-existing hooks (per-key replace semantics). `beforeValidate` runs before `_parse`, `afterValidate` runs on successful resolution, and `onError` runs immediately before a `ValidationError` surfaces. The synchronous parse path discards hook return values; the async path awaits them.
## Implementation Notes
- `INVALID` is a unique symbol (`Symbol("INVALID")`) so it can never collide with real output values.
- The `_clone()` method uses `Object.create(proto) + Object.assign` to preserve the subclass prototype, then deep-copies `_refinements` and `_hooks`.
- Async refinements **loud-fail** the synchronous parse path (`parse` / `safeParse`) with `VALIDATION.ASYNC_REFINEMENT_REQUIRES_PARSEASYNC`. Silent-skipping was a security-grade footgun (revocation / uniqueness / rate-limit checks would no-op); callers must use `parseAsync` / `safeParseAsync` for any schema carrying an async refinement.
- `safeParse` is implemented as a `try { return parse } catch` shim so OTel + hook emission flows through both paths from a single source of truth.
- Hook return values are awaited in `parseAsync` and discarded (via `void`) in `parse`. `onError` only fires for thrown `ValidationError` — non-`ValidationError` throws bypass the hook and are wrapped into a `ParseFailure` with `VALIDATION.INVALID_TYPE` only when reaching `safeParse` / `safeParseAsync`.
- OTel emission uses `performance.now()` for the duration histogram and `this.constructor.name` (e.g. `"StringSchema"`) as the `schema.type` attribute. All emission is a no-op when no `MeterProvider` is registered.

View File

@@ -0,0 +1,90 @@
# ValidationError
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ValidationError` is the single error class for the `@suaveplan/validation` package. It extends `SuaveplanError` from `@suaveplan/error`, giving it a stable `code` (taken from the first issue), a `timestamp`, and optional `traceId`. Its `issues` array carries every `ValidationIssue` produced during a parse run, enabling rich error reporting for form validation, API input checking, and CLI tools.
The `VALIDATION_CODES` registry uses `defineCodes` from `@suaveplan/error` to produce fully-qualified, type-safe code strings like `"VALIDATION.STRING.TOO_SHORT"`. All 21 validation codes are registered there.
## Features
- Extends `SuaveplanError` — compatible with `walkCauses`, `toSpanEvent`, `toJSON`
- `issues: ValidationIssue[]` — full list of all parse failures with path and message
- `flatten()` — splits issues into `formErrors` (root path) and `fieldErrors` (field-keyed, dot-separated path)
- `format()` — builds a nested `Record<string, unknown>` with `_errors` arrays per node
- `VALIDATION_CODES` — typed registry of all error codes
## Usage: Basic Example
```typescript
import { string } from "@suaveplan/validation";
const result = string().min(5).safeParse("hi");
if (!result.success) {
console.log(result.error.issues);
// [{ code: "VALIDATION.STRING.TOO_SHORT", path: [], message: "..." }]
}
```
## Usage: Advanced Example
```typescript
import { object, string, number } from "@suaveplan/validation";
const schema = object({ name: string().min(2), age: number().positive() });
const result = schema.safeParse({ name: "A", age: -1 });
if (!result.success) {
const { formErrors, fieldErrors } = result.error.flatten();
// fieldErrors: { name: ["String must be at least 2 character(s)"], age: ["Number must be positive"] }
const formatted = result.error.format();
// { _errors: [], name: { _errors: ["..."] }, age: { _errors: ["..."] } }
}
```
## API Reference
### `class ValidationError extends SuaveplanError<ValidationCode>`
#### Constructor
`new ValidationError(issues: ValidationIssue[], opts?: ValidationErrorOptions)`
- `issues` — array of `ValidationIssue`; the first issue's `code` and `message` become the error's primary code and message
- `opts.cause` — optional underlying cause
- `opts.traceId` — optional OTel trace id
#### `readonly issues: ValidationIssue[]`
All issues collected during parsing.
#### `flatten(): FlattenedErrors`
Returns `{ formErrors: string[], fieldErrors: Record<string, string[]> }`. Root-level issues (empty path) go to `formErrors`; all others are keyed by dot-joined path segments.
#### `format(): Record<string, unknown>`
Returns a nested record where each node has an `_errors: string[]` array. Useful for rendering field-level errors in UI frameworks.
### `VALIDATION_CODES`
Frozen record of all `VALIDATION.*` codes. Use when constructing custom issues.
### `ValidationErrorOptions`
```typescript
interface ValidationErrorOptions {
cause?: unknown;
traceId?: string;
}
```
## Implementation Notes
- An empty `issues` array produces `code: "VALIDATION.INVALID_TYPE"` and message `"Validation failed"` as safe fallbacks.
- `format()` traverses path segments iteratively, creating intermediate nodes as needed, so deeply nested paths are handled without recursion.
- `flatten()` uses a dot-joined path key (`"user.address.city"`) matching the most common form-library convention.

View File

@@ -0,0 +1,83 @@
# Validation OTel Instrumentation
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`validation-otel.ts` provides OpenTelemetry instrumentation for parse operations in `@suaveplan/validation`. It exposes two counter instruments (`validation.parse.success` and `validation.parse.failure`) and one histogram (`validation.parse.duration_ms`) through the OTel metrics API. All instruments are labelled with a `schema.type` attribute so dashboards can break down metrics per schema class.
Instruments are lazily initialised on first use and cached as module-level singletons, avoiding meter lookup overhead on the hot parse path.
## Features
- Counter `validation.parse.success` incremented per successful parse
- Counter `validation.parse.failure` incremented per failed parse
- Histogram `validation.parse.duration_ms` records duration in milliseconds
- `schema.type` attribute on all instruments for per-schema breakdown
- `_resetInstruments()` for test isolation
## Usage: Basic Example
```typescript
import { recordParseSuccess, recordParseFailure } from "@suaveplan/validation";
const start = performance.now();
const result = mySchema.safeParse(input);
const duration = performance.now() - start;
if (result.success) {
recordParseSuccess({ schemaType: "ObjectSchema" }, duration);
} else {
recordParseFailure({ schemaType: "ObjectSchema" }, duration);
}
```
## Usage: Advanced Example
```typescript
import { recordParseSuccess, recordParseFailure } from "@suaveplan/validation";
// Wrap any schema with automatic OTel instrumentation
function instrumentedParse<T>(schema: { safeParse: (v: unknown) => { success: boolean; data?: T } }, input: unknown, schemaType: string) {
const start = performance.now();
const result = schema.safeParse(input);
const duration = performance.now() - start;
if (result.success) {
recordParseSuccess({ schemaType }, duration);
} else {
recordParseFailure({ schemaType }, duration);
}
return result;
}
```
## API Reference
### `recordParseSuccess(opts: ParseObservabilityOptions, durationMs: number): void`
Increments the success counter and records the duration histogram.
### `recordParseFailure(opts: ParseObservabilityOptions, durationMs: number): void`
Increments the failure counter and records the duration histogram.
### `_resetInstruments(): void`
Clears cached instrument singletons. For test use only.
### `ParseObservabilityOptions`
```typescript
interface ParseObservabilityOptions {
schemaType: string; // e.g. "ObjectSchema", "StringSchema"
}
```
## Implementation Notes
- Instruments are lazily created via `metrics.getMeter()` on first call to either `recordParseSuccess` or `recordParseFailure`. This avoids OTel initialisation at module import time.
- The OTel `metrics` global (`@opentelemetry/api`) is a no-op by default unless a real metrics provider is configured, so these calls are safe in environments without OTel.
- `_resetInstruments()` sets all singletons to `undefined`, causing the next call to re-create them — useful for testing multiple instrumentation scenarios in the same process.

View File

@@ -0,0 +1,63 @@
# ArraySchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ArraySchema<T>` validates that an input is a JavaScript array and that every element satisfies an inner schema. It collects element-level errors with positional paths (`[0]`, `[1]`, …) so callers can display per-item validation feedback. All constraint builders return new instances; the inner element schema is never mutated.
## Features
- Type check: rejects non-arrays with `VALIDATION.INVALID_TYPE`
- Element validation: every item parsed with the element schema; all errors collected
- `min(n)` / `max(n)` — length constraints
- `length(n)` — exact length
- `nonempty()` — shorthand for `min(1)`
## Usage: Basic Example
```typescript
import { array, string } from "@suaveplan/validation";
const tags = array(string()).min(1).max(10);
tags.parse(["typescript", "validation"]); // ok
tags.parse([]); // throws (too short)
```
## Usage: Advanced Example
```typescript
import { array, object, string, number } from "@suaveplan/validation";
const items = array(
object({ sku: string(), qty: number().int().positive() })
).nonempty();
const cart = items.safeParse([
{ sku: "ABC", qty: 2 },
{ sku: "XYZ", qty: 0 }, // fails positive()
]);
// cart.error.issues[0].path === [1, "qty"]
```
## API Reference
### `array<T>(schema: BaseSchema<unknown, T>): ArraySchema<T>`
Factory accepting the element schema.
### Constraint methods (all return new `ArraySchema<T>`)
| Method | Error code | Description |
|--------|-----------|-------------|
| `min(n)` | `ARRAY.TOO_SHORT` | Length must be ≥ n |
| `max(n)` | `ARRAY.TOO_LONG` | Length must be ≤ n |
| `length(n)` | `ARRAY.TOO_SHORT` | Length must be exactly n |
| `nonempty()` | `ARRAY.TOO_SHORT` | Must have at least one element |
## Implementation Notes
- All element errors are accumulated even when earlier elements fail, giving the caller a complete picture. The parse returns `INVALID` after collecting all issues.
- Element error paths include the numeric index: `ctx.path = [...parentPath, i]`.
- Array length checks run before element validation to avoid unnecessary work on obviously wrong inputs.

View File

@@ -0,0 +1,70 @@
# BooleanSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BooleanSchema` validates that an input is a native JavaScript `boolean` — exactly `true` or `false`. It does not coerce string representations like `"true"` or numeric values like `1`; use `coerceBoolean` from `@suaveplan/validation` if coercion is needed. The schema is stateless and the `boolean()` factory always returns the same shape.
## Features
- Type check: rejects non-booleans with `VALIDATION.INVALID_TYPE`
- Zero-configuration: no constraints beyond type checking
- Compatible with `optional()`, `nullable()`, `refine()`, and `transform()` from `BaseSchema`
## Usage: Basic Example
```typescript
import { boolean } from "@suaveplan/validation";
const active = boolean();
active.parse(true); // true
active.parse(false); // false
active.parse(1); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { boolean, object, string } from "@suaveplan/validation";
const featureFlag = object({
name: string(),
enabled: boolean(),
experimental: boolean().optional(),
});
const result = featureFlag.safeParse({
name: "dark-mode",
enabled: true,
});
// { success: true, data: { name: "dark-mode", enabled: true, experimental: undefined } }
```
## API Reference
### `boolean(): BooleanSchema`
Returns a `BooleanSchema` instance.
### `_parse(input, ctx): boolean | INVALID`
Returns the input unchanged if it is a `boolean`, otherwise pushes an `VALIDATION.INVALID_TYPE` issue and returns `INVALID`.
## Implementation Notes
- `BooleanSchema` is the simplest concrete schema — it has no constraint fields and no builder methods beyond those inherited from `BaseSchema`.
- Coercion (string `"true"` → `true`) is intentionally excluded from this schema to keep the boolean validation surface predictable. Use `coerce(boolean())` when HTTP query parameters need coercion.
### Parse Behaviour
`z.boolean()` accepts only actual `true` / `false` values in `parse` / `safeParse`. Use `z.coerce.boolean()` when you want string/number coercion (`"true"`, `1`, `"1"` → `true`). All coercion rules follow the Zod v4 reference table so schemas here are drop-in replacements for Zod schemas elsewhere in the ecosystem.
## Further Usage: Basic Example
```ts
import z from "@suaveplan/validation";
const s = z.boolean();
s.parse(true); // true
```

View File

@@ -0,0 +1,68 @@
# DateSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DateSchema` validates that an input is a JavaScript `Date` instance with a valid (non-NaN) time value, and optionally enforces inclusive `min` / `max` date boundaries. It does not coerce ISO strings or timestamps to `Date`; use `coerceDate` from `@suaveplan/validation` for that. The schema is immutable — each constraint builder returns a new `DateSchema`.
## Features
- Type check: rejects non-Date values with `VALIDATION.INVALID_TYPE`
- Invalid date detection: rejects `new Date("invalid")` with `VALIDATION.DATE.INVALID`
- `min(d)` — date must be on or after `d` (inclusive)
- `max(d)` — date must be on or before `d` (inclusive)
## Usage: Basic Example
```typescript
import { date } from "@suaveplan/validation";
const born = date().max(new Date());
born.parse(new Date("1990-01-01")); // valid
born.parse(new Date("2099-01-01")); // throws ValidationError (too late)
```
## Usage: Advanced Example
```typescript
import { date, object } from "@suaveplan/validation";
const booking = object({
checkIn: date().min(new Date()),
checkOut: date(),
}).refine(
(v) => v.checkOut > v.checkIn,
"Check-out must be after check-in",
);
const result = booking.safeParse({
checkIn: new Date("2025-06-01"),
checkOut: new Date("2025-06-07"),
});
```
## API Reference
### `date(): DateSchema`
Returns an unconstrained `DateSchema`.
### `min(d: Date): DateSchema`
Returns a new `DateSchema` requiring the input date to be ≥ `d`. Emits `VALIDATION.DATE.TOO_EARLY` on failure.
### `max(d: Date): DateSchema`
Returns a new `DateSchema` requiring the input date to be ≤ `d`. Emits `VALIDATION.DATE.TOO_LATE` on failure.
## Implementation Notes
- `Date` validity is checked via `Number.isNaN(input.getTime())`, which correctly identifies `new Date("invalid")` without requiring string inspection.
- Boundary comparisons use JavaScript `<` / `>` on `Date` objects, which internally compares millisecond timestamps.
- String-to-Date coercion is intentionally omitted. Pair with `coerceDate` when accepting JSON payloads.
### Parse Behaviour
`z.date()` accepts only `Date` instances; `z.coerce.date()` additionally accepts ISO-8601 strings and millisecond-epoch numbers. Invalid dates (`new Date("nope")` — a `Date` with `NaN` time) are rejected as `invalid_date`. Refinements `min()` / `max()` compare `getTime()` values so time-zone differences fold into the numeric comparison.

View File

@@ -0,0 +1,57 @@
# EnumSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`EnumSchema<T>` validates that an input is one of a fixed set of allowed values (a closed-ended enumeration). It differs from `LiteralSchema` in that it checks membership in an array rather than equality with a single value, and from TypeScript's `enum` keyword in that it operates on plain string/number arrays at runtime.
## Features
- Membership check via `Array.includes`
- Supports `string` and `number` value arrays
- Returns `VALIDATION.ENUM.INVALID_VALUE` with the list of allowed values on failure
## Usage: Basic Example
```typescript
import { enumeration } from "@suaveplan/validation";
const status = enumeration(["active", "inactive", "pending"] as const);
status.parse("active"); // "active"
status.parse("deleted"); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { enumeration, object, string } from "@suaveplan/validation";
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number];
const RoleAssignment = object({
userId: string().uuid(),
role: enumeration(ROLES),
});
```
## API Reference
### `enumeration<T extends string | number>(values: readonly T[]): EnumSchema<T>`
Factory accepting the array of allowed values. The name `enumeration` avoids conflict with the reserved `enum` keyword.
### `_parse(input, ctx): T | INVALID`
Checks `Array.prototype.includes`. On failure, the error message lists all allowed values via `JSON.stringify`.
## Implementation Notes
- Membership check uses `Array.prototype.includes` which is O(n) in the values array. For very large enumerations (100+ values), consider a `Set`-based lookup — though typical enum sizes (< 20 values) make this irrelevant.
- `as const` on the values array is recommended to get the narrowest literal type for `T`.
### Parse Behaviour
`z.enum(["a", "b", "c"])` accepts only the listed string literal values and produces a branded union type so downstream code gets the narrow literal rather than `string`. A helper `.extract([...])` returns a subset enum; `.exclude([...])` returns the complement. Both preserve the original ordering so telemetry and UI code can rely on it.

View File

@@ -0,0 +1,64 @@
# IntersectionSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`IntersectionSchema` validates that an input satisfies two schemas simultaneously and merges their outputs. For object schemas, the merge is performed via `Object.assign`, which implements a right-biased merge where overlapping keys from the second schema take precedence. For non-object schemas (strings, numbers, etc.), the second schema's output is used directly.
## Features
- Both schemas must pass — issues from both arms are aggregated on failure
- Object output merging via `Object.assign({}, resultA, resultB)`
- Non-object fallback: returns second schema's result
## Usage: Basic Example
```typescript
import { intersection, object, string, number } from "@suaveplan/validation";
const Named = object({ name: string() });
const Aged = object({ age: number() });
const Person = intersection(Named, Aged);
Person.parse({ name: "Alice", age: 30 });
// { name: "Alice", age: 30 }
```
## Usage: Advanced Example
```typescript
import { intersection, object, string } from "@suaveplan/validation";
const Timestamped = object({ createdAt: string(), updatedAt: string() });
const Entity = object({ id: string().uuid(), type: string() });
const TimestampedEntity = intersection(Entity, Timestamped);
TimestampedEntity.parse({
id: "550e8400-e29b-41d4-a716-446655440000",
type: "user",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-06-01T00:00:00Z",
});
```
## API Reference
### `intersection(a: AnySchema, b: AnySchema): IntersectionSchema`
Factory accepting two schemas to intersect.
### `_parse(input, ctx): unknown | INVALID`
Both schemas parse with independent child contexts. If either fails, all issues are pushed to `ctx.issues`. If both succeed and both results are non-null objects, they are merged via `Object.assign`.
## Implementation Notes
- Both arms always run (not short-circuit), ensuring all issues are collected even when the first arm fails.
- `Object.assign` is a shallow merge; deep intersection of nested objects is not performed — callers should compose nested `intersection()` calls explicitly.
- This differs from Zod's `z.intersection()` in that it does not attempt deep merging of arrays or complex types.
### Parse Behaviour
`z.intersection(a, b)` runs both schemas against the input and merges the results; either failure fails the whole schema. Object schemas merge shallowly. For deep merges, prefer `a.merge(b)` on object schemas directly — `intersection` exists for the rarer case where the operands are not both plain objects (e.g. object + record, union + discriminated-union, etc.).

View File

@@ -0,0 +1,65 @@
# LiteralSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LiteralSchema<T>` validates that an input is strictly equal (`===`) to a specific primitive value. It supports strings, numbers, booleans, `null`, and `undefined` as literal values. This is the foundation for discriminated union patterns and exhaustive type guards.
## Features
- Strict equality check (`===`) against the literal value
- Supports `string | number | boolean | null | undefined` as literal types
- Returns `VALIDATION.LITERAL.MISMATCH` on failure
## Usage: Basic Example
```typescript
import { literal } from "@suaveplan/validation";
const ok = literal("ok");
ok.parse("ok"); // "ok"
ok.parse("error"); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { union, literal, object, string } from "@suaveplan/validation";
// Discriminated union pattern
const SuccessResponse = object({ type: literal("success"), data: string() });
const ErrorResponse = object({ type: literal("error"), message: string() });
const Response = union([SuccessResponse, ErrorResponse]);
Response.parse({ type: "success", data: "ok" });
Response.parse({ type: "error", message: "not found" });
```
## API Reference
### `literal<T extends string | number | boolean | null | undefined>(value: T): LiteralSchema<T>`
Factory accepting the literal value to match.
### `_parse(input, ctx): T | INVALID`
Uses `===` comparison. Returns `VALIDATION.LITERAL.MISMATCH` on failure with a message showing both expected and received values (via `JSON.stringify`).
## Implementation Notes
- `JSON.stringify` is used in error messages to safely represent all primitive types including `undefined` (shown as `undefined` string) and `null`.
- Literal schemas are particularly efficient: the `_parse` body is a single equality check with no iteration.
### Parse Behaviour
`z.literal("ready")` accepts only the exact value `"ready"` and infers the literal type `"ready"` rather than the widened type `string`. Literal unions are written as `z.union([z.literal("a"), z.literal("b")])` or the sugar `z.enum(["a", "b"])` — the enum form is marginally faster because it compiles to a single `Set` membership check.
## Further Usage: Basic Example
```ts
import z from "@suaveplan/validation";
const ready = z.literal("ready");
ready.parse("ready"); // "ready"
```

View File

@@ -0,0 +1,67 @@
# NullableSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NullableSchema<T>` wraps an inner schema so that `null` passes through without validation. Any other value is delegated to the inner schema. It mirrors the behavior of `OptionalSchema` but for `null` rather than `undefined`. Both can be composed: `optional(nullable(string()))` accepts `undefined | null | string`.
## Features
- Passes `null` through without calling the inner schema
- Delegates all other values to the inner schema
- Composable with `optional()` for full nullish handling
## Usage: Basic Example
```typescript
import { nullable, string } from "@suaveplan/validation";
const bio = nullable(string());
bio.parse(null); // null
bio.parse("hello"); // "hello"
bio.parse(42); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { object, string, nullable, optional } from "@suaveplan/validation";
const User = object({
name: string(),
deletedAt: nullable(string()), // null when not deleted
nickname: optional(nullable(string())), // undefined or null or string
});
```
## API Reference
### `nullable<T>(schema: BaseSchema<unknown, T>): NullableSchema<T>`
Factory wrapping an inner schema.
### `NullableSchema<T>`
Class extending `BaseSchema<T | null, T | null>`. The `_parse` method returns `null` immediately for `null` inputs.
## Implementation Notes
- `NullableSchema` is also defined in the core `schema.ts` module and re-exported here for convenience.
- Only `null` bypasses the inner schema; `undefined` is not treated as null (use `optional()` for that).
### Parse Behaviour
`schema.nullable()` wraps any schema so `null` is accepted in addition to the underlying type. The inferred output type is `T | null`. Combine with `optional()` (either order) for `T | null | undefined`. Nullable schemas round-trip cleanly through JSON because `null` is a JSON primitive.
## Further Usage: Basic Example
```ts
import z from "@suaveplan/validation";
const s = z.string().nullable();
s.parse(null); // null
s.parse("hi"); // "hi"
```
Internally `.nullable()` is a thin wrapper that returns a new schema whose parse function short-circuits on a `null` input and delegates everything else to the inner schema. The inner schemas `refinements` and `transforms` run only on the non-null branch, so nullability is cleanly orthogonal to the rest of the schema pipeline.

View File

@@ -0,0 +1,70 @@
# NumberSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NumberSchema` validates that an input is a JavaScript `number` and enforces numeric constraints. By default, NaN and Infinity are rejected (the `finite` check runs for all `NumberSchema` instances unless explicitly disabled). All constraint methods return new instances, preserving immutability across builder chains.
This schema covers the full range of server-side numeric validation: integer enforcement, range clamping, sign checks, and divisibility rules.
## Features
- Type check: rejects non-numbers with `VALIDATION.INVALID_TYPE`
- Finite check by default: rejects NaN and Infinity with `VALIDATION.NUMBER.NOT_FINITE`
- `min(n)` / `max(n)` — inclusive range
- `int()` — integer-only (via `Number.isInteger`)
- `positive()` — must be > 0
- `negative()` — must be < 0
- `multipleOf(n)` — divisibility check
- `finite()` — explicit finite constraint (same as default)
## Usage: Basic Example
```typescript
import { number } from "@suaveplan/validation";
const age = number().int().min(0).max(150);
const result = age.safeParse(25);
// { success: true, data: 25 }
```
## Usage: Advanced Example
```typescript
import { number } from "@suaveplan/validation";
// Pagination offset: non-negative integer, multiple of 10
const offset = number().int().min(0).multipleOf(10);
// Latitude: float in range
const latitude = number().min(-90).max(90);
// Positive price with two-decimal precision (via multipleOf)
const cents = number().int().positive();
```
## API Reference
### `number(): NumberSchema`
Factory returning a `NumberSchema` with default finite enforcement.
### Constraint methods (all return new `NumberSchema`)
| Method | Error code | Description |
|--------|-----------|-------------|
| `min(n)` | `NUMBER.TOO_SMALL` | Value must be ≥ n |
| `max(n)` | `NUMBER.TOO_LARGE` | Value must be ≤ n |
| `int()` | `NUMBER.NOT_INT` | Must be integer |
| `positive()` | `NUMBER.TOO_SMALL` | Must be > 0 |
| `negative()` | `NUMBER.TOO_LARGE` | Must be < 0 |
| `multipleOf(n)` | `NUMBER.NOT_MULTIPLE` | Must satisfy `value % n === 0` |
| `finite()` | `NUMBER.NOT_FINITE` | Must be finite (explicit) |
## Implementation Notes
- The finite check runs first on every `NumberSchema` unless the `finite` constraint property is explicitly `false`. This prevents NaN from silently passing other checks.
- `positive()` and `negative()` use strict inequality (0 is neither positive nor negative).
- `multipleOf` uses the JavaScript modulo operator, which preserves sign for negative dividends — callers should be aware when combining with `negative()`.

View File

@@ -0,0 +1,77 @@
# ObjectSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ObjectSchema<S>` validates plain JavaScript objects against a named-key shape. Each key in the shape is validated against its corresponding schema, and errors carry full dot-path information. Three modes control unknown key handling: `strip` (default, silently removes extras), `strict` (rejects extras), and `passthrough` (copies extras to output). Rich shape-manipulation builders — `pick`, `omit`, `partial`, `extend`, `merge` — make it easy to derive new schemas from existing ones without duplication.
All builder methods return new `ObjectSchema` instances; the original is never modified.
## Features
- Type check: rejects non-objects, null, and arrays with `VALIDATION.INVALID_TYPE`
- Field validation with per-field error paths
- Three unknown-key modes: `strip`, `strict`, `passthrough`
- `pick(keys)` — project to a subset of keys
- `omit(keys)` — exclude keys
- `partial()` — make all fields optional
- `required()` — return schema as-is (identity, for symmetry)
- `extend(shape)` — add/override fields
- `merge(other)` — combine with another `ObjectSchema`
## Usage: Basic Example
```typescript
import { object, string, number } from "@suaveplan/validation";
const User = object({ name: string(), age: number() });
const user = User.parse({ name: "Alice", age: 30 });
// { name: "Alice", age: 30 }
```
## Usage: Advanced Example
```typescript
import { object, string, number } from "@suaveplan/validation";
const BaseUser = object({ id: string().uuid(), name: string() });
const AdminUser = BaseUser.extend({ role: string(), level: number().int() });
const PartialUpdate = AdminUser.partial().omit(["id"]);
const update = PartialUpdate.parse({ name: "Bob" });
// { name: "Bob" } — all fields optional, id omitted
const strict = AdminUser.strict();
strict.parse({ id: "...", name: "Alice", role: "admin", level: 1, extra: true });
// throws: Unrecognized key(s): extra
```
## API Reference
### `object<S extends ObjectShape>(shape: S): ObjectSchema<S>`
Factory with default `strip` mode.
### Mode methods (return new `ObjectSchema<S>`)
- `strict()` — reject unknown keys with `VALIDATION.OBJECT.UNRECOGNIZED_KEYS`
- `passthrough()` — copy unknown keys to output
- `strip()` — silently drop unknown keys (default)
### Shape manipulation methods
- `pick<K>(keys: K[]): ObjectSchema<Pick<S, K>>` — subset of keys
- `omit<K>(keys: K[]): ObjectSchema<Omit<S, K>>` — exclude keys
- `partial(): ObjectSchema<...>` — wrap all values in `OptionalSchema`
- `required(): ObjectSchema<S>` — identity (for symmetry with `partial`)
- `extend<E>(shape: E): ObjectSchema<S & E>` — add/override fields
- `merge<E>(other: ObjectSchema<E>): ObjectSchema<S & E>` — merge shapes
## Implementation Notes
- Field errors are reported under the field's key path. Nested object errors propagate the full path chain.
- `partial()` wraps each field schema with `.optional()`, so the output type has all values as `T | undefined`.
- `passthrough` copies unknown keys before iterating known ones, then known-key parsing runs normally on top.
- `strict` checks for unknown keys using a `Set` of shape keys — O(n) where n = number of input keys.

View File

@@ -0,0 +1,68 @@
# OptionalSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`OptionalSchema<T>` wraps an inner schema so that `undefined` passes through without validation. Any other value is delegated to the inner schema. It is the canonical way to mark fields as optional in object shapes and as the return type of `.optional()` on any `BaseSchema`.
## Features
- Passes `undefined` through without calling the inner schema
- Delegates all other values to the inner schema
- Composable: `optional(nullable(string()))` is valid
## Usage: Basic Example
```typescript
import { optional, string } from "@suaveplan/validation";
const nickname = optional(string().min(2));
nickname.parse(undefined); // undefined
nickname.parse("Al"); // "Al"
nickname.parse(42); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { object, string, number, optional } from "@suaveplan/validation";
const ProfileUpdate = object({
displayName: optional(string().min(1).max(50)),
bio: optional(string().max(500)),
age: optional(number().int().min(13)),
});
ProfileUpdate.parse({}); // all optional, empty object valid
```
## API Reference
### `optional<T>(schema: BaseSchema<unknown, T>): OptionalSchema<T>`
Factory wrapping an inner schema.
### `OptionalSchema<T>`
Class extending `BaseSchema<T | undefined, T | undefined>`. The `_parse` method returns `undefined` immediately for `undefined` inputs.
## Implementation Notes
- `OptionalSchema` is also defined in the core `schema.ts` module and re-exported here for convenience.
- `undefined` is the only value that bypasses the inner schema; `null` is not treated as absent (use `nullable()` for that).
### Parse Behaviour
`schema.optional()` wraps any schema so `undefined` is accepted in addition to the underlying type. The inferred output type is `T | undefined`. Object schemas treat missing keys as `undefined`, so `z.object({ x: z.string().optional() }).parse({})` is valid.
## Further Usage: Basic Example
```ts
import z from "@suaveplan/validation";
const s = z.string().optional();
s.parse(undefined); // undefined
```
Internally `.optional()` is a thin wrapper that short-circuits on `undefined` and delegates everything else to the inner schema. Optionality composes cleanly with nullability, defaults, and refinements — the inner transforms only see defined, non-null inputs so their implementations stay simple.

View File

@@ -0,0 +1,56 @@
# RecordSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`RecordSchema<T>` validates that an input is a plain object where every value satisfies a given schema. Unlike `ObjectSchema`, which checks a fixed set of named keys, `RecordSchema` iterates all present keys and validates each value dynamically — making it ideal for dictionaries, maps, and index-signature types.
## Features
- Type check: rejects non-objects, null, and arrays with `VALIDATION.INVALID_TYPE`
- Value validation: every value parsed against the element schema
- All value errors collected with key-based paths
## Usage: Basic Example
```typescript
import { record, number } from "@suaveplan/validation";
const scores = record(number());
scores.parse({ alice: 95, bob: 88 }); // { alice: 95, bob: 88 }
scores.parse({ alice: "high" }); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { record, object, string, number } from "@suaveplan/validation";
// Indexed config map
const config = record(object({ value: string(), ttl: number().int() }));
config.parse({
featureA: { value: "enabled", ttl: 3600 },
featureB: { value: "disabled", ttl: 0 },
});
```
## API Reference
### `record<T>(valueSchema: BaseSchema<unknown, T>): RecordSchema<T>`
Factory accepting the value schema.
### `_parse(input, ctx): Record<string, T> | INVALID`
Validates `typeof input === "object"`, not null, not array, then iterates `Object.keys(input)` and parses each value. All errors collected; returns `INVALID` if any fail.
## Implementation Notes
- `record` does not validate keys (keys are always strings in JS objects). Key validation would require a `RecordSchema<K, V>` variant — not currently implemented.
- Error paths use the key string as a path segment: `[...parentPath, key]`.
### Parse Behaviour
`z.record(keySchema, valueSchema)` validates every key/value pair in the input object against the supplied schemas. Unknown keys are preserved. Pair with `z.string()` / `z.enum([...])` as the key schema to narrow the allowed key set. `strict()` / `strip()` / `passthrough()` are not applicable — records are inherently open-shape.

View File

@@ -0,0 +1,90 @@
# StringSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StringSchema` validates that an input is a JavaScript `string` and optionally enforces structural constraints: minimum/maximum length, exact length, regex pattern, email format, URL format, UUID format, prefix/suffix/substring checks. It also supports output transforms (trim, toLowerCase, toUpperCase) applied before constraint evaluation. Every builder method returns a new instance, making the API fully immutable.
This schema is the most commonly used primitive in form and API validation. All constraints are evaluated in a single `_parse` call with no heap allocation beyond the output string, making it suitable for high-frequency validation paths.
## Features
- Type check: rejects non-string with `VALIDATION.INVALID_TYPE`
- `min(n)` / `max(n)` / `length(n)` — length constraints
- `regex(re)` — arbitrary pattern matching
- `email()` — basic RFC-style email check (`user@domain.tld`)
- `url()` — requires `http://` or `https://` prefix
- `uuid()` — 36-character UUID v4-style regex
- `startsWith(prefix)` / `endsWith(suffix)` / `includes(substr)` — substring checks
- `trim()` / `toLowerCase()` / `toUpperCase()` — output transforms
## Usage: Basic Example
```typescript
import { string } from "@suaveplan/validation";
const username = string().min(3).max(20);
const result = username.safeParse("alice");
// { success: true, data: "alice" }
```
## Usage: Advanced Example
```typescript
import { string } from "@suaveplan/validation";
// Email with domain block refinement
const email = string()
.email()
.toLowerCase()
.trim()
.refine((v) => !v.endsWith("@spam.com"), "Spam domains not allowed")
.describe("User contact email");
// UUID validator
const id = string().uuid().describe("Resource identifier");
// URL with path check
const webhookUrl = string()
.url()
.startsWith("https://")
.includes("/webhook");
```
## API Reference
### `string(): StringSchema`
Factory returning an unconstrained `StringSchema`.
### Constraint methods (all return new `StringSchema`)
| Method | Error code | Description |
|--------|-----------|-------------|
| `min(n)` | `STRING.TOO_SHORT` | Length must be ≥ n |
| `max(n)` | `STRING.TOO_LONG` | Length must be ≤ n |
| `length(n)` | `STRING.INVALID_FORMAT` | Length must be exactly n |
| `regex(re)` | `STRING.INVALID_FORMAT` | Must match regex |
| `email()` | `STRING.INVALID_FORMAT` | Must be valid email |
| `url()` | `STRING.INVALID_FORMAT` | Must start with `http://` or `https://` |
| `uuid()` | `STRING.INVALID_FORMAT` | Must be valid UUID |
| `startsWith(p)` | `STRING.INVALID_FORMAT` | Must begin with prefix p |
| `endsWith(s)` | `STRING.INVALID_FORMAT` | Must end with suffix s |
| `includes(sub)` | `STRING.INVALID_FORMAT` | Must contain substring |
### Transform methods (all return new `StringSchema`)
| Method | Effect |
|--------|--------|
| `trim()` | `String.prototype.trim()` before checks |
| `toLowerCase()` | Converts to lowercase before checks |
| `toUpperCase()` | Converts to uppercase before checks |
## Implementation Notes
- Transforms are applied before constraint checks. This means `string().trim().min(3)` validates the trimmed string.
- The email regex (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`) is intentionally simple — RFC 5321 compliance is not the goal; rejecting obviously malformed inputs is.
- `uuid()` uses a case-insensitive regex matching the standard 8-4-4-4-12 hex format.
- All builder methods create new `StringSchema` instances via spread of `_constraints`, so chaining is safe across branches.

View File

@@ -0,0 +1,55 @@
# TupleSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TupleSchema<T>` validates fixed-length arrays where each positional element has its own schema. Unlike `ArraySchema` (homogeneous elements), `TupleSchema` is heterogeneous: position 0 can be a `string`, position 1 a `number`, etc. It strictly enforces both the array type and the exact length.
## Features
- Type check: rejects non-arrays with `VALIDATION.INVALID_TYPE`
- Length check: rejects arrays with wrong length with `VALIDATION.INVALID_TYPE`
- Per-position schema validation with positional error paths
## Usage: Basic Example
```typescript
import { tuple, string, number } from "@suaveplan/validation";
const coord = tuple([number(), number()]);
coord.parse([48.8566, 2.3522]); // [48.8566, 2.3522]
coord.parse([48.8566]); // throws (wrong length)
```
## Usage: Advanced Example
```typescript
import { tuple, string, number, boolean } from "@suaveplan/validation";
// CSV row parser: [id, name, score, active]
const row = tuple([string().uuid(), string().min(1), number(), boolean()]);
const result = row.safeParse(["550e8400-e29b-41d4-a716-446655440000", "Alice", 99, true]);
```
## API Reference
### `tuple<T extends AnySchema[]>(schemas: T): TupleSchema<T>`
Factory accepting an ordered array of schemas.
### `_parse(input, ctx): TupleOutputs<T> | INVALID`
Validates input is an array of the exact expected length, then parses each element against its positional schema.
## Implementation Notes
- Length mismatch is reported as `VALIDATION.INVALID_TYPE` (no dedicated `TUPLE` code) with a descriptive message including both expected and actual lengths.
- Error paths include the element index: `[...parentPath, i]`.
- Empty tuples `tuple([])` are valid and accept only `[]`.
### Parse Behaviour
`z.tuple([a, b, c])` validates fixed-length arrays where each position has a distinct schema. Extra trailing items are rejected unless you add a rest element with `.rest(schema)`. Inferred output is a typed tuple (`[A, B, C]`) rather than an array, so downstream destructuring stays fully typed.

View File

@@ -0,0 +1,60 @@
# UnionSchema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`UnionSchema` validates that an input satisfies at least one of a list of member schemas. It short-circuits on the first successful parse, returning that result without checking remaining members. When all members fail, it aggregates all their issues into the `ValidationError`, giving the caller visibility into why each arm failed.
## Features
- Short-circuit success: first matching schema wins
- All-failure aggregation: every arm's issues collected into one `ValidationError`
- Empty union: always fails with `VALIDATION.UNION.NO_MATCH`
## Usage: Basic Example
```typescript
import { union, string, number } from "@suaveplan/validation";
const stringOrNumber = union([string(), number()]);
stringOrNumber.parse("hello"); // "hello"
stringOrNumber.parse(42); // 42
stringOrNumber.parse(true); // throws ValidationError
```
## Usage: Advanced Example
```typescript
import { union, literal, object, string } from "@suaveplan/validation";
// Discriminated union
const Event = union([
object({ type: literal("created"), id: string() }),
object({ type: literal("deleted"), id: string(), reason: string() }),
]);
Event.parse({ type: "created", id: "abc" });
// { type: "created", id: "abc" }
```
## API Reference
### `union(schemas: AnySchema[]): UnionSchema`
Factory accepting an array of member schemas.
### `_parse(input, ctx): unknown | INVALID`
Iterates schemas in order. Returns the first non-`INVALID` result. On all-failure, pushes all collected issues to `ctx.issues` and returns `INVALID`. Empty union always adds a single `VALIDATION.UNION.NO_MATCH` issue.
## Implementation Notes
- Each arm runs with its own `childCtx` so its issues don't contaminate the parent context until the all-failure path is confirmed.
- The order of schemas matters: place more specific schemas first to avoid false positives from permissive schemas like `string()`.
- The aggregated issue list can be long for wide unions; callers may want to use `flatten()` to extract meaningful field-level errors.
### Parse Behaviour
`z.union([a, b, c])` tries each member in order and returns the first success; all failures are aggregated into a `ZodUnionError` with per-member diagnostics so callers can show a useful error. For discriminated unions (a shared literal key across members), prefer `z.discriminatedUnion("kind", [...])` which is faster and produces cleaner errors.

View File

@@ -0,0 +1,83 @@
# Common Types
## Purpose
`common.ts` defines the core type contracts shared across the entire `@suaveplan/validation` package: validation codes, issue structures, parse result types, context types, schema type helpers, refinement/transform function types, and option bundles. By centralising these types, all schema implementations and consumers can import from a single well-known location without circular dependencies.
## Features
- `ValidationCode` — exhaustive union of all 21 validation error codes
- `ValidationIssue` — structured issue with code, path, message, and optional input
- `ParseSuccess<T>` / `ParseFailure` / `ParseResult<T>` — discriminated result union
- `ParseContext` — mutable context object threaded through the parse tree
- `FlattenedErrors` — flat form-friendly error structure
- Schema type helpers: `AnySchema`, `Infer<S>`, `InferInput<S>`, `SchemaInput<S>`, `SchemaOutput<S>`
- Refinement types: `RefineFn<T>`, `AsyncRefineFn<T>`, `TransformFn<T, U>`, `CustomRule<T>`
- Option bundles: `StringRuleOptions`, `NumberRuleOptions`, `ObjectShape`
## Usage: Basic Example
```typescript
import type { Infer, ParseResult } from "@suaveplan/validation";
import { string } from "@suaveplan/validation";
const NameSchema = string().min(1);
type Name = Infer<typeof NameSchema>; // string
function validateName(input: unknown): ParseResult<Name> {
return NameSchema.safeParse(input);
}
```
## Usage: Advanced Example
```typescript
import type { AnySchema, SchemaOutput, ValidationIssue } from "@suaveplan/validation";
function extractErrors(schema: AnySchema, input: unknown): ValidationIssue[] {
const result = schema.safeParse(input);
if (!result.success) return result.error.issues;
return [];
}
```
## API Reference
### `ValidationCode`
Union of 21 string literals covering all possible validation failures. Used as the `code` field in `ValidationIssue` and as the generic parameter to `ValidationError`.
### `ValidationIssue`
```typescript
interface ValidationIssue {
code: ValidationCode;
path: (string | number)[];
message: string;
input?: unknown;
}
```
### `ParseResult<T>`
Discriminated union: `ParseSuccess<T> | ParseFailure`. Test `result.success` to narrow.
### `ParseContext`
```typescript
interface ParseContext {
path: (string | number)[];
issues: ValidationIssue[];
}
```
Mutable; passed by reference through the schema tree. Schemas push issues and return `INVALID`.
### `Infer<S extends AnySchema>`
Extracts the output type of a schema: `Infer<typeof string()>` → `string`.
## Implementation Notes
- These are type-only declarations; the file emits no runtime code beyond the type imports.
- `ObjectShape` is a simple `Record<string, AnySchema>` alias used by `ObjectSchema` and `RecordSchema` factory signatures.

View File

@@ -0,0 +1,100 @@
# Validation Hooks
## Purpose
`validation-hooks.ts` defines the lifecycle hook types for schema validation observability. These hooks allow consumers to intercept the validation lifecycle at three points — before validation begins, after it completes successfully, and when it fails — enabling logging, metrics, and auditing without modifying schema logic.
## Features
- `ValidationHookData<T>` — payload carrying input, optional output, and optional error
- `ValidationValidatingHandler<T>` — async-compatible hook handler type
- `ValidationHookMap<T>` — named map of three lifecycle hooks
## Usage: Basic Example
```typescript
import type { ValidationHookMap } from "@suaveplan/validation";
const hooks: ValidationHookMap<string> = {
beforeValidate: ({ input }) => console.log("validating", input),
afterValidate: ({ output }) => console.log("valid", output),
onError: ({ error }) => console.error("invalid", error?.message),
};
```
## Usage: Advanced Example
```typescript
import type { ValidationHookMap } from "@suaveplan/validation";
import { recordParseSuccess, recordParseFailure } from "@suaveplan/validation";
function makeOtelHooks<T>(schemaType: string): ValidationHookMap<T> {
let start = 0;
return {
beforeValidate: () => { start = performance.now(); },
afterValidate: () => recordParseSuccess({ schemaType }, performance.now() - start),
onError: () => recordParseFailure({ schemaType }, performance.now() - start),
};
}
```
## API Reference
### `ValidationHookData<T>`
```typescript
interface ValidationHookData<T> {
input: unknown;
output?: T;
error?: ValidationError;
}
```
### `ValidationValidatingHandler<T>`
`(data: ValidationHookData<T>) => void | Promise<void>`
### `ValidationHookMap<T>`
```typescript
interface ValidationHookMap<T> {
beforeValidate?: ValidationValidatingHandler<T>;
afterValidate?: ValidationValidatingHandler<T>;
onError?: ValidationValidatingHandler<T>;
}
```
## Implementation Notes
- These types are interface definitions only; wiring hooks into schema instances is left to the consumer. They are provided as a contract for instrumentation libraries built on top of `@suaveplan/validation`.
- All handlers are optional in `ValidationHookMap<T>`, so partial hook sets are valid.
### Hook Points
`ValidationHooks` defines the lifecycle the validation runtime emits events for: `onParseStart`, `onParseSuccess`, `onParseFailure`, `onRefinementStart`, `onRefinementFailure`. Hooks are wired through `@suaveplan/events` so consumers can plug an `EventBus` in without the validation package taking a hard dep on any specific bus implementation. Hook callbacks are side-effect only; their return values are ignored.
## Further Usage: Basic Example
```ts
import type { ValidationHooks } from "@suaveplan/validation/types/validation-hooks";
const hooks: ValidationHooks = { onParseFailure: (err) => console.warn(err.code) };
```
## Further Usage: Advanced Example
```ts
import type { ValidationHooks } from "@suaveplan/validation/types/validation-hooks";
import { EventBus } from "@suaveplan/events";
const bus = new EventBus<{ "validation.failed": { code: string } }>();
const hooks: ValidationHooks = {
onParseFailure: (err) => bus.emit("validation.failed", { code: err.code }),
};
```
## Further API Reference
`ValidationHooks` — record of optional lifecycle callbacks.
## Further Implementation Notes
Hooks fire synchronously in the parse hot path so implementations must return quickly; offload work to a bus listener for anything expensive.

View File

@@ -0,0 +1,74 @@
# Coerce Utilities
## Purpose
The coerce utilities allow schemas to accept a broader range of input types by converting them before validation. They bridge the gap between raw HTTP parameters (always strings), form data, and strongly-typed schema expectations. Each coerce function is pure and never throws — failures return `null`. The higher-order `coerce(schema)` wrapper applies the appropriate coercion strategy for string inputs and falls back to the raw schema if no coercion path works.
## Features
- `coerceString(input)` — always succeeds via `String(input)`
- `coerceNumber(input)` — via `Number(input)`, returns `null` for NaN
- `coerceBoolean(input)` — handles `"true"/"false"/"1"/"0"/1/0/true/false`
- `coerceDate(input)` — converts ISO strings and timestamps to `Date`
- `coerce(schema)` — wraps any schema with automatic string coercion
## Usage: Basic Example
```typescript
import { coerce, number } from "@suaveplan/validation";
const ageSchema = coerce(number().int().min(0));
ageSchema.parse("25"); // 25 (string coerced to number)
ageSchema.parse(25); // 25 (passthrough)
```
## Usage: Advanced Example
```typescript
import { coerce, boolean, date, object } from "@suaveplan/validation";
// Query parameter parsing (all values arrive as strings)
const QueryParams = object({
active: coerce(boolean()),
since: coerce(date()),
limit: coerce(number().int().max(100)),
});
QueryParams.parse({
active: "true",
since: "2024-01-01",
limit: "50",
});
```
## API Reference
### `coerceString(input: unknown): string`
Calls `String(input)`. Always succeeds; never returns null.
### `coerceNumber(input: unknown): number | null`
Calls `Number(input)`. Returns `null` if the result is `NaN`.
### `coerceBoolean(input: unknown): boolean | null`
Recognises `true/false` (booleans passthrough), `"true"/"1"/1` → `true`, `"false"/"0"/0` → `false`. Returns `null` for unrecognised input.
### `coerceDate(input: unknown): Date | null`
Passes through existing `Date` instances. Converts strings and numbers via `new Date(input)`. Returns `null` if the result is an invalid date.
### `coerce<T>(schema: BaseSchema<unknown, T>): CoercedSchema<T>`
Returns a `CoerceWrapperSchema` that attempts string coercions in order (number → boolean → date) for string inputs, falling back to the raw schema if none succeed.
### `type CoercedSchema<T>`
Alias for `BaseSchema<unknown, T>`.
## Implementation Notes
- `coerce(schema)` clears `ctx.issues` between coercion attempts so that intermediate failures don't pollute the final error list.
- Coercion order for string inputs is: numeric → boolean → date → raw. The first successful conversion is used.
- For non-string inputs, `coerce(schema)` delegates directly to the inner schema with no transformation.

View File

@@ -0,0 +1,51 @@
# Compose Utilities
## Purpose
The compose utilities provide functional combinators for working with validation results and schemas. `mergeResults` aggregates multiple `ParseResult<T>` values into a single `ParseResult<T[]>`, collecting all issues from failures. `pipe` composes a schema with one or more transform functions into a new pipeline schema, enabling clean left-to-right data transformation chains without manual wrapping.
## Features
- `mergeResults<T>(results)` — aggregate array of results, collecting all failures
- `pipe<T, U>(schema, ...transforms)` — compose schema with sequential transforms
## Usage: Basic Example
```typescript
import { pipe, string } from "@suaveplan/validation";
const trimmedLength = pipe(
string(),
(v: unknown) => (v as string).trim(),
(v: unknown) => (v as string).length,
);
trimmedLength.parse(" hello "); // 5
```
## Usage: Advanced Example
```typescript
import { mergeResults, number } from "@suaveplan/validation";
const n = number().min(0);
const results = [1, 2, -3, 4].map((v) => n.safeParse(v));
const merged = mergeResults(results);
// merged.success === false, merged.error.issues has one issue for -3
```
## API Reference
### `mergeResults<T>(results: ParseResult<T>[]): ParseResult<T[]>`
If all results are successes, returns `{ success: true, data: T[] }`.
If any result is a failure, returns `{ success: false, error: ValidationError }` with all issues aggregated from all failures.
### `pipe<T, U>(schema: BaseSchema<unknown, T>, ...transforms: TransformFn<unknown, unknown>[]): BaseSchema<unknown, U>`
Chains `schema` with each transform in order using `TransformSchema`. Each transform receives the previous stage's output as `unknown` (type-cast by the caller). Returns a new schema producing type `U`.
## Implementation Notes
- `mergeResults` with an empty array returns `{ success: true, data: [] }`.
- `pipe` with no transforms returns the original schema (zero iterations of the loop).
- Transform functions in `pipe` accept `unknown` to avoid TypeScript variance issues across the chain; callers must cast appropriately.