# SchemaValidator — `@suaveplan/rules/validators/schema` _(no submodules)_ ## Purpose `SchemaValidator` validates a `Record` against a JSON-schema-like `Schema` definition. It covers the 8 primitive `FieldType`s (`string`, `number`, `integer`, `boolean`, `object`, `array`, `date`, `datetime`, `any`), 7 string formats (`email`, `url`, `uuid`, `date`, `datetime`, `ipv4`, `ipv6`, `regex`), 5 transforms (`trim`, `lowercase`, `uppercase`, `parseNumber`, `parseDate`), `enum` checks, `min`/`max`/`minLength`/`maxLength`/`pattern` constraints, recursive nested objects, recursive array items, and caller-supplied custom validators keyed by field path. Every input schema is Zod-validated at construction — malformed schemas raise `RulesError(RULES.SCHEMA_INVALID)`. The `validate` method returns a `ValidationResult` rather than throwing for any normal failure mode (missing required, wrong type, format miss, custom-refinement failure). OTel histogram and counter fire per invocation. ## Features - 8 field types with strict matching (e.g. `integer` rejects non-integer numbers). - 7 format checks via shared regex map. - 5 transforms applied before validation; non-applicable transforms (e.g. `lowercase` on a number) are no-ops. - Nested objects + arrays validated recursively with structured paths (`user.profile.name`, `items[3].sku`). - `enum` membership checks. - `min` / `max` / `minLength` / `maxLength` / `pattern` constraints. - Custom validators registered by field path; return `true`, `false`, or a string error message. - Zod-validated `Schema` at construction → `RULES.SCHEMA_INVALID` on rejection. - Pre-aborted `AbortSignal` short-circuits the recursion with `RULES.SCHEMA_INVALID`. - Returns `{ valid, errors, value }` where `value` carries the transformed data tree. ## Usage: Basic Example ```ts import { SchemaValidator } from "@suaveplan/rules/validators/schema"; const validator = new SchemaValidator({ email: { type: "string", required: true, format: "email" }, age: { type: "integer", min: 18 }, }); const result = await validator.validate({ email: "ada@example.com", age: 30 }); result.valid; // true ``` ## Usage: Advanced Example ```ts import { SchemaValidator } from "@suaveplan/rules/validators/schema"; const validator = new SchemaValidator( { username: { type: "string", required: true, minLength: 3, maxLength: 20, transforms: ["trim", "lowercase"], }, profile: { type: "object", properties: { bio: { type: "string", maxLength: 240 }, tags: { type: "array", items: { type: "string", minLength: 2 }, }, }, }, registeredAt: { type: "date", transforms: ["parseDate"], required: true }, }, { customValidators: { username: async (value) => typeof value === "string" && !value.startsWith("admin") ? true : "reserved username prefix", }, }, ); const result = await validator.validate({ username: " ADA ", profile: { bio: "Mathematician", tags: ["math", "logic"] }, registeredAt: "1815-12-10", }); if (!result.valid) { for (const err of result.errors) console.warn(err.path, err.code, err.message); } console.log(result.value); // { username: "ada", profile: {...}, registeredAt: Date } ``` ## API Reference ### `class SchemaValidator` - `new SchemaValidator(schema, options?)` — Zod-validates the schema; raises `RULES.SCHEMA_INVALID` on rejection. - `options.customValidators: Record` — keyed by field path. Each validator receives `(value, ValidatorContext)` and returns `true`, `false`, or a `string` error. - `validate(data, options?)` → `Promise`. `options.signal` short-circuits. ### `interface Schema` `Record` where `FieldSchema` carries `type`, `required`, `format`, `min`, `max`, `minLength`, `maxLength`, `pattern`, `enum`, `items` (recursive), `properties` (recursive), `transforms`, `default`. ### Format dispatch | Format | Regex | |---|---| | `email` | `^[^\s@]+@[^\s@]+\.[^\s@]+$` | | `url` | `^https?://.+` (case-insensitive) | | `uuid` | RFC-4122 8-4-4-4-12 hex | | `date` | `^\d{4}-\d{2}-\d{2}$` | | `datetime` | ISO 8601 prefix `YYYY-MM-DDTHH:MM:SS` | | `ipv4` | dotted-quad shape | | `ipv6` | hex-and-colons (not strictly compliant; pre-filter only) | | `regex` | accepts anything (`.*`) — placeholder slot for future tightening | ### Transform dispatch | Transform | Applies when | Effect | |---|---|---| | `trim` | string | `value.trim()` | | `lowercase` | string | `value.toLowerCase()` | | `uppercase` | string | `value.toUpperCase()` | | `parseNumber` | string | `Number(value)` if not `NaN`; else leave | | `parseDate` | string | `new Date(value)` if valid; else leave | ## Implementation Notes - `validateField` is recursive. Pre-aborted signals are checked at each top-level field boundary; nested recursion inherits the signal via the `ValidatorContext`. - The transformed value is accumulated into a parallel `transformed` tree mirroring the schema shape. Callers consume `result.value` to get the post-transform view of the input. - `format: "regex"` is a placeholder that always matches. The field-level `pattern` option is the way to assert a specific regex; future hardening can replace the format slot with a per-pattern compile-cache. - The validator deliberately does NOT short-circuit on first error — it accumulates every issue across the tree so callers can present them all at once. Tests assert specific codes against the first error for clarity; production callers should iterate `result.errors`. - The CustomValidatorFn return type `boolean | string | Promise` matches the convention used by zxcvbn and Joi: a string return is treated as a custom error message, otherwise a falsy return generates the default message. - Type matching is strict: `number` rejects `NaN`; `date` / `datetime` accept only `Date` instances with valid `getTime()`. The `parseDate` transform converts ISO strings into `Date` before the type check fires.