@webergency-utils/typechecker 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -19
- package/dist/index.d.ts +14 -16
- package/dist/index.js +18 -2
- package/dist/runtime/validators.d.ts +41 -11
- package/dist/runtime/validators.js +54 -41
- package/dist/transformer.js +37 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ graph TD
|
|
|
107
107
|
G --> H[Emit optimized JavaScript files]
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
1. **Build-Time Transformation**: The compiler transformer intercepts calls to validation helper functions (`validate`, `is`, `assert`, `assertGuard`, `jsonSchema`
|
|
110
|
+
1. **Build-Time Transformation**: The compiler transformer intercepts calls to validation helper functions (`validate`, `is`, `assert`, `assertGuard`, `jsonSchema`, `validateSchema`, `isSchema`, `assertSchema`, `assertGuardSchema`).
|
|
111
111
|
2. **Type Extraction & Analysis**: It parses the target TS type structure, extracting intersection constraints, formats, transforms, and defaults recursively.
|
|
112
112
|
3. **Hoisted Registry**: The transformer generates highly optimized, direct JavaScript validator functions for each resolved type shape, generates a unique hash, and registers them in a global `MetadataStore`.
|
|
113
113
|
4. **Call Replacement**: The transformer replaces the original compile-time call expressions with direct, zero-reflection references to the runtime `MetadataStore`.
|
|
@@ -135,10 +135,11 @@ const age: number & constraint.Minimum<18> = 5;
|
|
|
135
135
|
## Glossary
|
|
136
136
|
|
|
137
137
|
- [`validate`](src/index.ts): Validates a value against a type, returning a structured result containing the validation status and a detailed list of errors.
|
|
138
|
-
- [`is`](src/index.ts): A type guard
|
|
138
|
+
- [`is`](src/index.ts): A type guard for `T`. Always mutates in place; `from` may coerce nested fields. Root replacement fails the guard.
|
|
139
139
|
- [`assert`](src/index.ts): Validates a value and returns it, throwing a validation error on failure (supports `from` coercion).
|
|
140
|
-
- [`assertGuard`](src/index.ts): Asserts a value
|
|
140
|
+
- [`assertGuard`](src/index.ts): Asserts a value is `T`. Always mutates in place; `from` may coerce nested fields. Root replacement throws.
|
|
141
141
|
- [`jsonSchema`](src/index.ts): Generates and returns a JSON Schema representation matching a TypeScript type at compile time (draft-07 shaped, with `x-typescript-type` for Date/RegExp/Set/Map/bigint/etc.).
|
|
142
|
+
- [`validateSchema`](src/index.ts) / [`isSchema`](src/index.ts) / [`assertSchema`](src/index.ts) / [`assertGuardSchema`](src/index.ts): Same entrypoints against a runtime JSON Schema value instead of a TypeScript generic.
|
|
142
143
|
- [`WithModifiers`](src/runtime/tags.ts): A utility type that applies constraint, format, or transformation tags to properties of deeply nested or external types using dot-separated path mappings.
|
|
143
144
|
- [`ResolveDefaults`](src/runtime/tags.ts): A helper type that removes the optional flag (`?`) from properties that have defined default values.
|
|
144
145
|
- [`convertPropertyCasing`](src/runtime/casing.ts): A runtime utility to recursively change the casing of object keys.
|
|
@@ -166,13 +167,13 @@ Validates input data against type `T` and returns a structured validation result
|
|
|
166
167
|
const result = validate<User>(data, 'strip');
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
#### `is<T>(input: unknown, options?: ValidationMode |
|
|
170
|
+
#### `is<T>(input: unknown, options?: ValidationMode | GuardOptions): input is ResolveDefaults<T>`
|
|
170
171
|
|
|
171
|
-
A type guard
|
|
172
|
+
A type guard for type `T`. Always mutates in place (no `mutate` option). `from` may coerce nested fields onto the same object; if validation would replace the root value (e.g. primitive `"42"` → `42`), returns `false`.
|
|
172
173
|
|
|
173
174
|
- **Parameters**:
|
|
174
175
|
- `input`: The value to check.
|
|
175
|
-
- `options` (optional): Either a `ValidationMode` string or a `
|
|
176
|
+
- `options` (optional): Either a `ValidationMode` string or a `GuardOptions` object.
|
|
176
177
|
- **Returns**: `boolean` (`true` if valid, `false` otherwise). Narrows type of `input` to `ResolveDefaults<T>` on success.
|
|
177
178
|
- **Example**:
|
|
178
179
|
```typescript
|
|
@@ -181,13 +182,13 @@ A type guard function checking if the input matches type `T`. Does **not** coerc
|
|
|
181
182
|
}
|
|
182
183
|
```
|
|
183
184
|
|
|
184
|
-
#### `assert<T>(input: unknown, options?: ValidationMode |
|
|
185
|
+
#### `assert<T>(input: unknown, options?: ValidationMode | AssertOptions): ResolveDefaults<T>`
|
|
185
186
|
|
|
186
187
|
Validates input data and returns it, throwing a validation error on failure.
|
|
187
188
|
|
|
188
189
|
- **Parameters**:
|
|
189
190
|
- `input`: The value to validate.
|
|
190
|
-
- `options` (optional): Either a `ValidationMode` string or
|
|
191
|
+
- `options` (optional): Either a `ValidationMode` string or an `AssertOptions` object.
|
|
191
192
|
- **Returns**: `ResolveDefaults<T>` (the validated value with defaults resolved).
|
|
192
193
|
- **Throws**: `Error` containing a list of path and constraint failures, or a custom error via `options.errorFactory`.
|
|
193
194
|
- **Example**:
|
|
@@ -195,13 +196,13 @@ Validates input data and returns it, throwing a validation error on failure.
|
|
|
195
196
|
const user = assert<User>(data);
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
#### `assertGuard<T>(input: unknown, options?: ValidationMode |
|
|
199
|
+
#### `assertGuard<T>(input: unknown, options?: ValidationMode | AssertGuardOptions): asserts input is ResolveDefaults<T>`
|
|
199
200
|
|
|
200
|
-
An assertion guard
|
|
201
|
+
An assertion guard for type `T`. Always mutates in place (no `mutate` option). `from` may coerce nested fields; root replacement fails with a normal type error (re-checked without `from`).
|
|
201
202
|
|
|
202
203
|
- **Parameters**:
|
|
203
204
|
- `input`: The value to check.
|
|
204
|
-
- `options` (optional): Either a `ValidationMode` string or
|
|
205
|
+
- `options` (optional): Either a `ValidationMode` string or an `AssertGuardOptions` object.
|
|
205
206
|
- **Returns**: `void`. Narrows the type of `input` in the enclosing scope on success.
|
|
206
207
|
- **Throws**: `Error` if validation fails (or a custom error via `options.errorFactory`).
|
|
207
208
|
- **Example**:
|
|
@@ -219,6 +220,31 @@ Generates a raw JSON Schema draft-07 object matching type `T` at compile time.
|
|
|
219
220
|
const userSchema = jsonSchema<User>();
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
#### `validateSchema<T = any>(schema: any, input: unknown, options?: ValidationMode | ValidationOptions): IValidation<T>`
|
|
224
|
+
|
|
225
|
+
Validates `input` against a **runtime JSON Schema** value (not a TypeScript generic).
|
|
226
|
+
|
|
227
|
+
- **Parameters**:
|
|
228
|
+
- `schema`: JSON Schema object.
|
|
229
|
+
- `input`: The value to validate.
|
|
230
|
+
- `options` (optional): Same as `validate`.
|
|
231
|
+
- **Example**:
|
|
232
|
+
```typescript
|
|
233
|
+
const result = validateSchema({ type: 'string', minLength: 2 }, name);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `isSchema(schema: any, input: unknown, options?: ValidationMode | GuardOptions): boolean`
|
|
237
|
+
|
|
238
|
+
Schema type-predicate. Always mutates in place; root replacement fails the guard.
|
|
239
|
+
|
|
240
|
+
#### `assertSchema<T = any>(schema: any, input: unknown, options?: ValidationMode | AssertOptions): T`
|
|
241
|
+
|
|
242
|
+
Like `assert`, but against a JSON Schema value.
|
|
243
|
+
|
|
244
|
+
#### `assertGuardSchema(schema: any, input: unknown, options?: ValidationMode | AssertGuardOptions): void`
|
|
245
|
+
|
|
246
|
+
Like `assertGuard`, but against a JSON Schema value. Root replacement throws.
|
|
247
|
+
|
|
222
248
|
---
|
|
223
249
|
|
|
224
250
|
### Utility Functions and Classes
|
|
@@ -270,18 +296,64 @@ An error class wrapper that transforms internal validation errors into a Zod-lik
|
|
|
270
296
|
|
|
271
297
|
### Interfaces and Types
|
|
272
298
|
|
|
299
|
+
#### `type ValidationMode`
|
|
300
|
+
|
|
301
|
+
Controls **unknown object keys only**. It does **not** coerce or revive values — that is exclusively `from`.
|
|
302
|
+
|
|
303
|
+
| Value | Behavior |
|
|
304
|
+
| :--- | :--- |
|
|
305
|
+
| `'strict'` (default) | Reject properties not declared on the type/schema. |
|
|
306
|
+
| `'relaxed'` | Allow unknown properties and keep them on the result. No type conversion. |
|
|
307
|
+
| `'strip'` | Drop unknown properties from the result (in place when mutating). |
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// OK: extra keys kept; age stays a string unless you also set from
|
|
311
|
+
validate<User>(data, 'relaxed');
|
|
312
|
+
|
|
313
|
+
// Coercion is a separate axis:
|
|
314
|
+
validate<User>(data, { mode: 'relaxed', from: 'query' });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `interface GuardOptions`
|
|
318
|
+
|
|
319
|
+
Options for `is` / `isSchema`. Always mutate in place (no `mutate` / `errorFactory`).
|
|
320
|
+
|
|
321
|
+
| Property | Type | Default | Description |
|
|
322
|
+
| :--- | :--- | :--- | :--- |
|
|
323
|
+
| `mode` | `ValidationMode` | `'strict'` | Unknown-key policy (`strict` / `relaxed` / `strip`). Not coercion — see `ValidationMode` above. |
|
|
324
|
+
| `from` | `'json' \| 'query' \| ((val, ctx) => any)` | `undefined` | In-place coercion for nested fields. Custom callbacks receive `(val, PathContext & { kind: CoercionKind })`. Root replacement fails the guard. |
|
|
325
|
+
|
|
326
|
+
#### `interface AssertGuardOptions`
|
|
327
|
+
|
|
328
|
+
Extends `GuardOptions` for `assertGuard` / `assertGuardSchema`.
|
|
329
|
+
|
|
330
|
+
| Property | Type | Default | Description |
|
|
331
|
+
| :--- | :--- | :--- | :--- |
|
|
332
|
+
| `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory when the guard throws. |
|
|
333
|
+
|
|
273
334
|
#### `interface ValidationOptions`
|
|
274
335
|
|
|
275
|
-
|
|
336
|
+
Extends `GuardOptions` for `validate` / `validateSchema` (adds `mutate`; no `errorFactory`).
|
|
276
337
|
|
|
277
338
|
| Property | Type | Default | Description |
|
|
278
339
|
| :--- | :--- | :--- | :--- |
|
|
279
|
-
| `mode` | `
|
|
280
|
-
| `from` | `'json' \| 'query' \| ((
|
|
281
|
-
| `
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
340
|
+
| `mode` | `ValidationMode` | `'strict'` | Unknown-key policy (`strict` / `relaxed` / `strip`). Not coercion — see `ValidationMode` above. |
|
|
341
|
+
| `from` | `'json' \| 'query' \| ((val, ctx) => any)` | `undefined` | Input conversion mode. `'json'` revives JSON-impossible types. `'query'` also coerces querystring shapes. A custom function is `(val, { key, path, parent, root, index?, kind }) => any` and runs only on type mismatch. `kind` is a `CoercionKind` dispatch tag (not `typeof` / a TS type). `key` is the nearest named path segment (for `[n]` leaves, the closest named key above). |
|
|
342
|
+
| `mutate` | `boolean` | `false` | `true`: always write onto the input. `false`: always allocate new containers. |
|
|
343
|
+
|
|
344
|
+
#### `interface AssertOptions`
|
|
345
|
+
|
|
346
|
+
Extends `ValidationOptions` for `assert` / `assertSchema`.
|
|
347
|
+
|
|
348
|
+
| Property | Type | Default | Description |
|
|
349
|
+
| :--- | :--- | :--- | :--- |
|
|
350
|
+
| `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory when assert throws. |
|
|
351
|
+
|
|
352
|
+
#### `type CoercionKind` / `interface PathContext` / `type FromCoercionContext`
|
|
353
|
+
|
|
354
|
+
- `CoercionKind`: expected runtime kind for custom `from` (`'Date' \| 'Array' \| …`) — a dispatch tag, not `typeof`.
|
|
355
|
+
- `PathContext`: `{ key, path, parent, root, index? }` shared by `constraint.Custom` and custom `from`.
|
|
356
|
+
- `FromCoercionContext`: `PathContext & { kind: CoercionKind }`.
|
|
285
357
|
|
|
286
358
|
#### `interface IValidation<T>`
|
|
287
359
|
|
|
@@ -331,7 +403,7 @@ Used to apply value constraints to types.
|
|
|
331
403
|
| `constraint.MinItems<N, Msg?>` | Restricts array length to $\ge N$. |
|
|
332
404
|
| `constraint.MaxItems<N, Msg?>` | Restricts array length to $\le N$. |
|
|
333
405
|
| `constraint.UniqueItems<Msg?>` | Restricts arrays to deeply unique items. |
|
|
334
|
-
| `constraint.Custom<Fn, Msg?>` | Runs a custom validation function: `(val,
|
|
406
|
+
| `constraint.Custom<Fn, Msg?>` | Runs a custom validation function: `(val, PathContext) => boolean` (`key`, `path`, `parent`, `root`, optional `index`). |
|
|
335
407
|
| `constraint.Requires<Path | [Paths], Msg?>` | Enforces that other object property paths exist. |
|
|
336
408
|
| `constraint.Message<Msg>` | Fallback custom error message. |
|
|
337
409
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
import { ResolveDefaults } from './runtime/tags.js';
|
|
2
|
-
import type { IValidationError } from './runtime/validators.js';
|
|
2
|
+
import type { IValidationError, ValidationMode, GuardOptions, AssertGuardOptions, ValidationOptions, AssertOptions } from './runtime/validators.js';
|
|
3
3
|
export interface IValidation<T> {
|
|
4
4
|
success: boolean;
|
|
5
5
|
data?: T;
|
|
6
6
|
errors?: IValidationError[];
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
mode?: ValidationMode;
|
|
11
|
-
from?: 'json' | 'query' | ((key: string, value: any, type: 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'symbol' | 'never' | 'Date' | 'RegExp' | 'Set' | 'Map' | 'Array' | 'Object' | 'instance' | 'null' | 'undefined' | 'tuple' | 'literal') => any);
|
|
12
|
-
wrapArrays?: boolean;
|
|
13
|
-
/** When true, write validated/coerced values onto the input. Default false: always return new containers. */
|
|
14
|
-
mutate?: boolean;
|
|
15
|
-
schema?: any;
|
|
16
|
-
errorFactory?: (errors: any[]) => Error;
|
|
17
|
-
}
|
|
18
|
-
/** Returns whether `input` already matches `T`. Does not coerce; `from` is ignored. */
|
|
19
|
-
export declare function is<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): _input is ResolveDefaults<T>;
|
|
8
|
+
/** Type guard for `T`. Mutates in place; root-level coercion that replaces the value fails the guard. */
|
|
9
|
+
export declare function is<T>(_input: unknown, _options?: ValidationMode | GuardOptions): _input is ResolveDefaults<T>;
|
|
20
10
|
/** Validates and returns the (possibly coerced) value. Use `from` when conversion is needed. */
|
|
21
|
-
export declare function assert<T>(_input: unknown, _options?: ValidationMode |
|
|
22
|
-
/** Asserts `input`
|
|
23
|
-
export declare function assertGuard<T>(_input: unknown, _options?: ValidationMode |
|
|
11
|
+
export declare function assert<T>(_input: unknown, _options?: ValidationMode | AssertOptions): ResolveDefaults<T>;
|
|
12
|
+
/** Asserts `input` is `T`. Mutates in place; root-level coercion that replaces the value throws. */
|
|
13
|
+
export declare function assertGuard<T>(_input: unknown, _options?: ValidationMode | AssertGuardOptions): asserts _input is ResolveDefaults<T>;
|
|
24
14
|
export declare function validate<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>;
|
|
25
15
|
export declare function jsonSchema<T>(): any;
|
|
16
|
+
/** Schema type-predicate. Mutates in place; root-level coercion that replaces the value fails. */
|
|
17
|
+
export declare function isSchema(_schema: any, _input: unknown, _options?: ValidationMode | GuardOptions): boolean;
|
|
18
|
+
/** Validates `input` against a JSON Schema value and returns the (possibly coerced) data. */
|
|
19
|
+
export declare function assertSchema<T = any>(_schema: any, _input: unknown, _options?: ValidationMode | AssertOptions): T;
|
|
20
|
+
/** Schema assertion guard. Mutates in place; root-level coercion that replaces the value throws. */
|
|
21
|
+
export declare function assertGuardSchema(_schema: any, _input: unknown, _options?: ValidationMode | AssertGuardOptions): void;
|
|
22
|
+
/** Validates `input` against a JSON Schema value. */
|
|
23
|
+
export declare function validateSchema<T = any>(_schema: any, _input: unknown, _options?: ValidationMode | ValidationOptions): IValidation<T>;
|
|
26
24
|
export * from './runtime/validators.js';
|
|
27
25
|
export * from './runtime/tags.js';
|
|
28
26
|
export * from './runtime/casing.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const TRANSFORMER_MISSING = 'Typechecker transformer was not applied. Register { "transform": "@webergency-utils/typechecker/transformer" } in tsconfig plugins (requires ts-patch).';
|
|
2
|
-
/**
|
|
2
|
+
/** Type guard for `T`. Mutates in place; root-level coercion that replaces the value fails the guard. */
|
|
3
3
|
export function is(_input, _options) {
|
|
4
4
|
throw new Error(TRANSFORMER_MISSING);
|
|
5
5
|
}
|
|
@@ -7,7 +7,7 @@ export function is(_input, _options) {
|
|
|
7
7
|
export function assert(_input, _options) {
|
|
8
8
|
throw new Error(TRANSFORMER_MISSING);
|
|
9
9
|
}
|
|
10
|
-
/** Asserts `input`
|
|
10
|
+
/** Asserts `input` is `T`. Mutates in place; root-level coercion that replaces the value throws. */
|
|
11
11
|
export function assertGuard(_input, _options) {
|
|
12
12
|
throw new Error(TRANSFORMER_MISSING);
|
|
13
13
|
}
|
|
@@ -17,6 +17,22 @@ export function validate(_input, _options) {
|
|
|
17
17
|
export function jsonSchema() {
|
|
18
18
|
throw new Error(TRANSFORMER_MISSING);
|
|
19
19
|
}
|
|
20
|
+
/** Schema type-predicate. Mutates in place; root-level coercion that replaces the value fails. */
|
|
21
|
+
export function isSchema(_schema, _input, _options) {
|
|
22
|
+
throw new Error(TRANSFORMER_MISSING);
|
|
23
|
+
}
|
|
24
|
+
/** Validates `input` against a JSON Schema value and returns the (possibly coerced) data. */
|
|
25
|
+
export function assertSchema(_schema, _input, _options) {
|
|
26
|
+
throw new Error(TRANSFORMER_MISSING);
|
|
27
|
+
}
|
|
28
|
+
/** Schema assertion guard. Mutates in place; root-level coercion that replaces the value throws. */
|
|
29
|
+
export function assertGuardSchema(_schema, _input, _options) {
|
|
30
|
+
throw new Error(TRANSFORMER_MISSING);
|
|
31
|
+
}
|
|
32
|
+
/** Validates `input` against a JSON Schema value. */
|
|
33
|
+
export function validateSchema(_schema, _input, _options) {
|
|
34
|
+
throw new Error(TRANSFORMER_MISSING);
|
|
35
|
+
}
|
|
20
36
|
export * from './runtime/validators.js';
|
|
21
37
|
export * from './runtime/tags.js';
|
|
22
38
|
export * from './runtime/casing.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Controls unknown object keys only — not coercion. Use `from` for conversion. */
|
|
1
2
|
export type ValidationMode = 'strict' | 'relaxed' | 'strip';
|
|
2
3
|
export interface IValidationError {
|
|
3
4
|
path: string;
|
|
@@ -6,25 +7,54 @@ export interface IValidationError {
|
|
|
6
7
|
/** Nested failures (e.g. per-arm errors for a failed union). */
|
|
7
8
|
issues?: IValidationError[];
|
|
8
9
|
}
|
|
9
|
-
/**
|
|
10
|
-
type
|
|
11
|
-
|
|
10
|
+
/** Expected runtime kind for custom `from` — a dispatch tag, not `typeof` / a TS type. */
|
|
11
|
+
export type CoercionKind = 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'symbol' | 'never' | 'Date' | 'RegExp' | 'Set' | 'Map' | 'Array' | 'Object' | 'instance' | 'null' | 'undefined' | 'tuple' | 'literal';
|
|
12
|
+
/** Shared context for `constraint.Custom` and custom `from` callbacks. */
|
|
13
|
+
export interface PathContext {
|
|
14
|
+
/** Nearest named property; for `[n]` leaves, the closest named segment above. */
|
|
15
|
+
key: string;
|
|
16
|
+
path: string;
|
|
17
|
+
parent: any;
|
|
18
|
+
root: any;
|
|
19
|
+
/** Set when the leaf path segment is an array index. */
|
|
20
|
+
index?: number;
|
|
21
|
+
}
|
|
22
|
+
export type FromCoercionContext = PathContext & {
|
|
23
|
+
kind: CoercionKind;
|
|
24
|
+
};
|
|
25
|
+
type FromOption = 'json' | 'query' | ((val: any, ctx: FromCoercionContext) => any);
|
|
12
26
|
export interface ValidationContext {
|
|
13
27
|
success: boolean;
|
|
14
28
|
errors: IValidationError[];
|
|
15
29
|
mode: ValidationMode;
|
|
16
30
|
from?: FromOption;
|
|
17
|
-
wrapArrays?: boolean;
|
|
18
31
|
mutate?: boolean;
|
|
19
32
|
root?: any;
|
|
20
33
|
}
|
|
21
|
-
|
|
34
|
+
/** Options for `is` / `isSchema`. Always mutate; no `mutate` / `errorFactory`. */
|
|
35
|
+
export interface GuardOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Unknown-key policy for closed objects (default `'strict'`).
|
|
38
|
+
* - `'strict'` — reject properties not in the type/schema
|
|
39
|
+
* - `'relaxed'` — allow and keep unknown properties (does **not** coerce values)
|
|
40
|
+
* - `'strip'` — drop unknown properties from the result (in place when mutating)
|
|
41
|
+
*
|
|
42
|
+
* Coercion / revival is controlled only by `from`, never by `mode`.
|
|
43
|
+
*/
|
|
22
44
|
mode?: ValidationMode;
|
|
23
45
|
from?: FromOption;
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
}
|
|
47
|
+
/** Options for `assertGuard` / `assertGuardSchema`. */
|
|
48
|
+
export interface AssertGuardOptions extends GuardOptions {
|
|
49
|
+
errorFactory?: (errors: IValidationError[]) => Error;
|
|
50
|
+
}
|
|
51
|
+
/** Options for `validate` / `validateSchema`. */
|
|
52
|
+
export interface ValidationOptions extends GuardOptions {
|
|
53
|
+
/** `true`: always write onto the input. `false` (default): always allocate new containers. */
|
|
26
54
|
mutate?: boolean;
|
|
27
|
-
|
|
55
|
+
}
|
|
56
|
+
/** Options for `assert` / `assertSchema`. */
|
|
57
|
+
export interface AssertOptions extends ValidationOptions {
|
|
28
58
|
errorFactory?: (errors: IValidationError[]) => Error;
|
|
29
59
|
}
|
|
30
60
|
/** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
|
|
@@ -90,9 +120,9 @@ export declare class MetadataStoreClass {
|
|
|
90
120
|
registerSchema(hash: string, schema: any): void;
|
|
91
121
|
getSchema(hash: string): any;
|
|
92
122
|
getOrCompileSchema(schema: any): Function;
|
|
93
|
-
is(validator: Function, value: any, options?: ValidationMode |
|
|
94
|
-
assert(validator: Function, value: any, options?: ValidationMode |
|
|
95
|
-
assertGuard(validator: Function, value: any, options?: ValidationMode |
|
|
123
|
+
is(validator: Function, value: any, options?: ValidationMode | GuardOptions): boolean;
|
|
124
|
+
assert(validator: Function, value: any, options?: ValidationMode | AssertOptions): any;
|
|
125
|
+
assertGuard(validator: Function, value: any, options?: ValidationMode | AssertGuardOptions): void;
|
|
96
126
|
validate(validator: Function, value: any, options?: ValidationMode | ValidationOptions): {
|
|
97
127
|
success: boolean;
|
|
98
128
|
errors: IValidationError[];
|
|
@@ -44,28 +44,38 @@ function wantsQuery(ctx) {
|
|
|
44
44
|
function wantsJsonRevive(ctx) {
|
|
45
45
|
return ctx.from === 'json' || ctx.from === 'query';
|
|
46
46
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
function isIndexSegment(seg) {
|
|
48
|
+
return seg.startsWith('[') && seg.endsWith(']');
|
|
49
|
+
}
|
|
50
|
+
function pathContext(path, ctx) {
|
|
51
|
+
const pathParts = tokenizePath(path);
|
|
52
|
+
const parentPath = joinPathSegments(pathParts.slice(0, -1));
|
|
53
|
+
const parent = getValueAtPath(ctx.root, parentPath);
|
|
54
|
+
const last = pathParts[pathParts.length - 1];
|
|
55
|
+
let index;
|
|
56
|
+
if (last && isIndexSegment(last)) {
|
|
57
|
+
const parsed = parseInt(last.slice(1, -1), 10);
|
|
58
|
+
if (!Number.isNaN(parsed)) {
|
|
59
|
+
index = parsed;
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return path.slice(bracket + 1, end);
|
|
62
|
+
let key = '';
|
|
63
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
64
|
+
if (!isIndexSegment(pathParts[i])) {
|
|
65
|
+
key = pathParts[i];
|
|
66
|
+
break;
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
|
-
if (
|
|
60
|
-
return path.
|
|
69
|
+
if (index === undefined) {
|
|
70
|
+
return { key, path, parent, root: ctx.root };
|
|
61
71
|
}
|
|
62
|
-
return path;
|
|
72
|
+
return { key, path, parent, root: ctx.root, index };
|
|
63
73
|
}
|
|
64
|
-
function fromCustom(ctx, path, value,
|
|
74
|
+
function fromCustom(ctx, path, value, kind) {
|
|
65
75
|
if (typeof ctx.from !== 'function') {
|
|
66
76
|
return value;
|
|
67
77
|
}
|
|
68
|
-
return ctx.from(
|
|
78
|
+
return ctx.from(value, { ...pathContext(path, ctx), kind });
|
|
69
79
|
}
|
|
70
80
|
/** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
|
|
71
81
|
export function coerceQueryNumber(v) {
|
|
@@ -519,7 +529,7 @@ export const validators = {
|
|
|
519
529
|
},
|
|
520
530
|
array: (v, path, ctx, childValidator) => {
|
|
521
531
|
if (!Array.isArray(v)) {
|
|
522
|
-
if (ctx
|
|
532
|
+
if (wantsQuery(ctx) && v !== undefined && v !== null) {
|
|
523
533
|
v = [v];
|
|
524
534
|
}
|
|
525
535
|
else if (typeof ctx.from === 'function') {
|
|
@@ -807,12 +817,7 @@ export const validators = {
|
|
|
807
817
|
return v;
|
|
808
818
|
},
|
|
809
819
|
custom: (v, path, ctx, fn, message) => {
|
|
810
|
-
|
|
811
|
-
const parentPath = joinPathSegments(pathParts.slice(0, -1));
|
|
812
|
-
const parent = getValueAtPath(ctx.root, parentPath);
|
|
813
|
-
const last = pathParts[pathParts.length - 1];
|
|
814
|
-
const index = last && last.startsWith('[') ? parseInt(last.slice(1, -1), 10) : undefined;
|
|
815
|
-
if (!fn(v, { parent, root: ctx.root, path, index: Number.isNaN(index) ? undefined : index })) {
|
|
820
|
+
if (!fn(v, pathContext(path, ctx))) {
|
|
816
821
|
report(ctx, path, fn.name ? `Custom<${fn.name}>` : 'Custom', v, message);
|
|
817
822
|
}
|
|
818
823
|
return v;
|
|
@@ -1145,20 +1150,22 @@ export class MetadataStoreClass {
|
|
|
1145
1150
|
is(validator, value, options) {
|
|
1146
1151
|
const opt = options;
|
|
1147
1152
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1153
|
+
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1154
|
+
// Always mutate — no returned data. Root must stay the same binding (res === value).
|
|
1155
|
+
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1156
|
+
const res = validator(value, '', ctx);
|
|
1157
|
+
if (!ctx.success) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
// Primitive / replaced roots cannot update the caller's binding — treat as not matching.
|
|
1161
|
+
return res === value;
|
|
1154
1162
|
}
|
|
1155
1163
|
assert(validator, value, options) {
|
|
1156
1164
|
const opt = options;
|
|
1157
1165
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1158
1166
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1159
|
-
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
1160
1167
|
const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
|
|
1161
|
-
const ctx = { success: true, errors: [], mode, from,
|
|
1168
|
+
const ctx = { success: true, errors: [], mode, from, mutate, root: value };
|
|
1162
1169
|
const res = validator(value, '', ctx);
|
|
1163
1170
|
if (!ctx.success) {
|
|
1164
1171
|
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
@@ -1171,25 +1178,31 @@ export class MetadataStoreClass {
|
|
|
1171
1178
|
assertGuard(validator, value, options) {
|
|
1172
1179
|
const opt = options;
|
|
1173
1180
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1181
|
+
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1182
|
+
// Always mutate — no returned data. Root must stay the same binding (res === value).
|
|
1183
|
+
const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
|
|
1184
|
+
const res = validator(value, '', ctx);
|
|
1185
|
+
if (ctx.success && res === value) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
// Root was replaced (e.g. primitive coerce) — binding unchanged; report a normal type failure.
|
|
1189
|
+
if (ctx.success && res !== value) {
|
|
1190
|
+
ctx.success = true;
|
|
1191
|
+
ctx.errors.length = 0;
|
|
1192
|
+
ctx.from = undefined;
|
|
1193
|
+
validator(value, '', ctx);
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
1196
|
+
throw opt.errorFactory(ctx.errors);
|
|
1184
1197
|
}
|
|
1198
|
+
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
1185
1199
|
}
|
|
1186
1200
|
validate(validator, value, options) {
|
|
1187
1201
|
const opt = options;
|
|
1188
1202
|
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
1189
1203
|
const from = typeof opt === 'object' ? opt?.from : undefined;
|
|
1190
|
-
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
1191
1204
|
const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
|
|
1192
|
-
const ctx = { success: true, errors: [], mode, from,
|
|
1205
|
+
const ctx = { success: true, errors: [], mode, from, mutate, root: value };
|
|
1193
1206
|
const res = validator(value, '', ctx);
|
|
1194
1207
|
return { success: ctx.success, errors: ctx.errors, data: res };
|
|
1195
1208
|
}
|
package/dist/transformer.js
CHANGED
|
@@ -3,7 +3,14 @@ import { buildValidator, generateHash, buildJsonSchema, objectToAst } from './en
|
|
|
3
3
|
export { buildValidator, generateHash, buildJsonSchema } from './engine/resolver.js';
|
|
4
4
|
import { hoistRegistrations } from './engine/hoister.js';
|
|
5
5
|
import { installStaticConstraintDiagnostics } from './engine/staticAsserts.js';
|
|
6
|
-
const
|
|
6
|
+
const TYPE_FUNCTIONS = ['is', 'assert', 'assertGuard', 'validate', 'jsonSchema'];
|
|
7
|
+
const SCHEMA_FUNCTIONS = ['isSchema', 'assertSchema', 'assertGuardSchema', 'validateSchema'];
|
|
8
|
+
function metadataCall(method, args) {
|
|
9
|
+
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), method), undefined, args);
|
|
10
|
+
}
|
|
11
|
+
function argOrUndefined(args, index) {
|
|
12
|
+
return args[index] || ts.factory.createIdentifier('undefined');
|
|
13
|
+
}
|
|
7
14
|
export default function transformer(program) {
|
|
8
15
|
const checker = program.getTypeChecker();
|
|
9
16
|
installStaticConstraintDiagnostics(program);
|
|
@@ -13,14 +20,29 @@ export default function transformer(program) {
|
|
|
13
20
|
const schemasCache = new Map();
|
|
14
21
|
const requiredUtils = new Set();
|
|
15
22
|
const visitor = (node) => {
|
|
16
|
-
// Skip visiting ImportDeclarations to preserve their original symbols and avoid compiler crashes
|
|
17
23
|
if (ts.isImportDeclaration(node)) {
|
|
18
24
|
return node;
|
|
19
25
|
}
|
|
20
|
-
// Handle runtime function calls (is, assert, assertGuard, validate, jsonSchema)
|
|
21
26
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
22
27
|
const fnName = node.expression.text;
|
|
23
|
-
if (
|
|
28
|
+
if (SCHEMA_FUNCTIONS.includes(fnName)) {
|
|
29
|
+
requiredUtils.add('MetadataStore');
|
|
30
|
+
const schemaArg = argOrUndefined(node.arguments, 0);
|
|
31
|
+
const inputArg = argOrUndefined(node.arguments, 1);
|
|
32
|
+
const optionsArg = argOrUndefined(node.arguments, 2);
|
|
33
|
+
const compiled = metadataCall('getOrCompileSchema', [schemaArg]);
|
|
34
|
+
if (fnName === 'validateSchema') {
|
|
35
|
+
return metadataCall('validate', [compiled, inputArg, optionsArg]);
|
|
36
|
+
}
|
|
37
|
+
if (fnName === 'isSchema') {
|
|
38
|
+
return metadataCall('is', [compiled, inputArg, optionsArg]);
|
|
39
|
+
}
|
|
40
|
+
if (fnName === 'assertSchema') {
|
|
41
|
+
return metadataCall('assert', [compiled, inputArg, optionsArg]);
|
|
42
|
+
}
|
|
43
|
+
return metadataCall('assertGuard', [compiled, inputArg, optionsArg]);
|
|
44
|
+
}
|
|
45
|
+
if (TYPE_FUNCTIONS.includes(fnName)) {
|
|
24
46
|
const typeArg = node.typeArguments?.[0];
|
|
25
47
|
if (typeArg) {
|
|
26
48
|
const type = checker.getTypeFromTypeNode(typeArg);
|
|
@@ -32,69 +54,22 @@ export default function transformer(program) {
|
|
|
32
54
|
const schemaObj = buildJsonSchema(type, checker);
|
|
33
55
|
schemasCache.set(hash, objectToAst(schemaObj));
|
|
34
56
|
}
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
let hasSchema = false;
|
|
39
|
-
let schemaExpr;
|
|
40
|
-
if (node.arguments[1]) {
|
|
41
|
-
const optArg = node.arguments[1];
|
|
42
|
-
const optType = checker.getTypeAtLocation(optArg);
|
|
43
|
-
const schemaProp = optType.getProperty('schema');
|
|
44
|
-
if (schemaProp) {
|
|
45
|
-
hasSchema = true;
|
|
46
|
-
if (ts.isObjectLiteralExpression(optArg)) {
|
|
47
|
-
const prop = optArg.properties.find(p => {
|
|
48
|
-
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'schema') {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
if (ts.isShorthandPropertyAssignment(p) && p.name.text === 'schema') {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
});
|
|
56
|
-
if (prop) {
|
|
57
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
58
|
-
schemaExpr = prop.initializer;
|
|
59
|
-
}
|
|
60
|
-
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
61
|
-
schemaExpr = prop.name;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (!schemaExpr) {
|
|
66
|
-
schemaExpr = ts.factory.createPropertyAccessExpression(optArg, 'schema');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (hasSchema && schemaExpr) {
|
|
71
|
-
const compileAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'getOrCompileSchema');
|
|
72
|
-
getCall = ts.factory.createCallExpression(compileAccess, undefined, [schemaExpr]);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'getValidator');
|
|
76
|
-
getCall = ts.factory.createCallExpression(mdStoreAccess, undefined, [ts.factory.createStringLiteral(hash)]);
|
|
77
|
-
}
|
|
57
|
+
const inputArg = argOrUndefined(node.arguments, 0);
|
|
58
|
+
const optionsArg = argOrUndefined(node.arguments, 1);
|
|
59
|
+
const getValidator = metadataCall('getValidator', [ts.factory.createStringLiteral(hash)]);
|
|
78
60
|
if (fnName === 'jsonSchema') {
|
|
79
|
-
|
|
80
|
-
return ts.factory.createCallExpression(getSchemaAccess, undefined, [ts.factory.createStringLiteral(hash)]);
|
|
81
|
-
}
|
|
82
|
-
else if (fnName === 'validate') {
|
|
83
|
-
const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'validate');
|
|
84
|
-
return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
|
|
61
|
+
return metadataCall('getSchema', [ts.factory.createStringLiteral(hash)]);
|
|
85
62
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
|
|
63
|
+
if (fnName === 'validate') {
|
|
64
|
+
return metadataCall('validate', [getValidator, inputArg, optionsArg]);
|
|
89
65
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
|
|
66
|
+
if (fnName === 'is') {
|
|
67
|
+
return metadataCall('is', [getValidator, inputArg, optionsArg]);
|
|
93
68
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
|
|
69
|
+
if (fnName === 'assert') {
|
|
70
|
+
return metadataCall('assert', [getValidator, inputArg, optionsArg]);
|
|
97
71
|
}
|
|
72
|
+
return metadataCall('assertGuard', [getValidator, inputArg, optionsArg]);
|
|
98
73
|
}
|
|
99
74
|
}
|
|
100
75
|
}
|