first commit
This commit is contained in:
88
docs/foundation/flow/rules/src/validators/field/field.md
Normal file
88
docs/foundation/flow/rules/src/validators/field/field.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# FieldValidator — `@suaveplan/rules/validators/field`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`FieldValidator` is a chainable, immutable-per-call validator for a single value. Every `withX` method returns a fresh validator with the new stage appended, so preset chains can be shared across requests without aliasing concerns. `validateSync(value)` runs purely-sync chains; `validate(value, options?)` returns a Promise and threads `AbortSignal` through every stage. The module ships 12 preset chains covering the common shapes (email, password, username, phone, URL, UUID, date, integer, age, currency, percentage, positive number).
|
||||
|
||||
The chain is the natural fit when validating one value at a time (form field, query parameter, header). For object-tree validation, prefer `SchemaValidator`; for plug-in-style reusable validators with a registry, prefer `CustomValidator`.
|
||||
|
||||
## Features
|
||||
|
||||
- Immutable chain — every `.withX` returns a new `FieldValidator`.
|
||||
- Sync + async stages; `validateSync` throws `RULES.VALIDATOR_INVALID` when invoked on an async chain.
|
||||
- 9 builder methods: `required`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `oneOf`, `custom`, `customAsync`.
|
||||
- 12 preset factories.
|
||||
- OTel histogram (`rules.validator.validate.duration_ms{kind:"field"}`) + counter (`rules.validator.validations_total{kind:"field",outcome}`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { field } from "@suaveplan/rules/validators/field";
|
||||
|
||||
const validator = field().required().minLength(3).maxLength(20);
|
||||
const result = await validator.validate("ada");
|
||||
result.valid; // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
field,
|
||||
passwordFieldValidator,
|
||||
usernameFieldValidator,
|
||||
} from "@suaveplan/rules/validators/field";
|
||||
|
||||
// Compose a preset with caller-specific extensions
|
||||
const adminPasswordValidator = passwordFieldValidator()
|
||||
.minLength(12, "admin passwords must be at least 12 chars")
|
||||
.customAsync(async (value) => {
|
||||
const breached = await checkHibp(value as string);
|
||||
return breached ? "password appears in known-breach corpus" : true;
|
||||
});
|
||||
|
||||
const username = usernameFieldValidator();
|
||||
expect((await username.validate("ada42")).valid).toBe(true);
|
||||
|
||||
declare function checkHibp(password: string): Promise<boolean>;
|
||||
declare const expect: { (received: unknown): { toBe(v: unknown): void } };
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class FieldValidator`
|
||||
|
||||
- `FieldValidator.create()` / `createFieldValidator()` / `field()` — entry points.
|
||||
- `required(message?)`, `minLength(min, message?)`, `maxLength(max, message?)`, `min(min, message?)`, `max(max, message?)`, `pattern(re, message?)`, `oneOf(values, message?)`, `custom(fn, message?)`, `customAsync(fn, message?)` — each returns a new `FieldValidator`.
|
||||
- `validate(value, options?)` → `Promise<FieldValidatorResult>` where `FieldValidatorResult = { valid: boolean; errors: readonly string[] }`. Honours `options.signal`.
|
||||
- `validateSync(value)` → `FieldValidatorResult`. Throws `RulesError(RULES.VALIDATOR_INVALID)` when the chain has any async stage.
|
||||
- `isAsync()` → `true` when the chain contains any async stage.
|
||||
|
||||
### Preset chains
|
||||
|
||||
| Factory | Behaviour |
|
||||
|---|---|
|
||||
| `emailFieldValidator()` | required + email regex |
|
||||
| `urlFieldValidator()` | required + `^https?://` |
|
||||
| `uuidFieldValidator()` | required + RFC-4122 8-4-4-4-12 |
|
||||
| `phoneFieldValidator()` | required + `^\+?[\d\s\-().]{7,}$` |
|
||||
| `usernameFieldValidator()` | required + `^[a-z0-9_]{3,32}$` (case-insensitive) |
|
||||
| `passwordFieldValidator()` | required + minLength 8 + upper + lower + digit |
|
||||
| `dateFieldValidator()` | `instanceof Date && !NaN` |
|
||||
| `integerFieldValidator()` | `Number.isInteger` |
|
||||
| `ageValidator()` | required + range 0..150 |
|
||||
| `currencyValidator()` | required + non-negative number |
|
||||
| `percentageValidator()` | required + range 0..100 |
|
||||
| `positiveNumberValidator()` | required + > 0 |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Every stage runs against the original input value (not the previous stage's output) — `FieldValidator` is a multi-predicate aggregator, not a transform pipeline. For transforms, use `SchemaValidator` with the `transforms` field.
|
||||
- `validate` always records every stage that fails — it does not short-circuit. This makes the error list complete in a single pass, which matches the convention used by Zod and Joi.
|
||||
- A pre-aborted signal short-circuits the loop and records the synthetic `"validation aborted"` error. Subsequent stages do not run.
|
||||
- The async-stage detector (`isAsync`) is `O(stages)` and runs once on `validateSync`. Callers that want fast-path sync validation should check `isAsync()` first.
|
||||
- `custom` accepts a fn returning `true | false | string`. `false` falls back to either the supplied `message` or the default `"custom validation failed"`; a string return is treated as the error message verbatim.
|
||||
- Preset chains are factory functions (not module-level constants) so consumers get an independent instance per call and can chain further without affecting other callers.
|
||||
Reference in New Issue
Block a user