@webergency-utils/typechecker 0.2.1 → 0.2.3
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 +99 -21
- package/dist/engine/generators.js +6 -5
- package/dist/engine/hoister.js +23 -17
- package/dist/engine/resolver.d.ts +1 -1
- package/dist/engine/resolver.js +16 -9
- package/dist/index.d.ts +14 -16
- package/dist/index.js +18 -2
- package/dist/runtime/casing.js +18 -1
- package/dist/runtime/validators.d.ts +48 -15
- package/dist/runtime/validators.js +781 -111
- package/dist/transformer.js +102 -74
- package/package.json +2 -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,35 @@ 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
|
+
Only the documented schema subset is compiled. Recognized validation keywords that are not implemented
|
|
228
|
+
(for example `enum`, `oneOf`, and `not`) throw during schema compilation instead of silently accepting data.
|
|
229
|
+
Schema `pattern` values are rejected when they exceed the safety limit or contain common catastrophic-backtracking constructs.
|
|
230
|
+
|
|
231
|
+
- **Parameters**:
|
|
232
|
+
- `schema`: JSON Schema object.
|
|
233
|
+
- `input`: The value to validate.
|
|
234
|
+
- `options` (optional): Same as `validate`.
|
|
235
|
+
- **Example**:
|
|
236
|
+
```typescript
|
|
237
|
+
const result = validateSchema({ type: 'string', minLength: 2 }, name);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### `isSchema(schema: any, input: unknown, options?: ValidationMode | GuardOptions): boolean`
|
|
241
|
+
|
|
242
|
+
Schema type-predicate. Always mutates in place; root replacement fails the guard.
|
|
243
|
+
|
|
244
|
+
#### `assertSchema<T = any>(schema: any, input: unknown, options?: ValidationMode | AssertOptions): T`
|
|
245
|
+
|
|
246
|
+
Like `assert`, but against a JSON Schema value.
|
|
247
|
+
|
|
248
|
+
#### `assertGuardSchema(schema: any, input: unknown, options?: ValidationMode | AssertGuardOptions): void`
|
|
249
|
+
|
|
250
|
+
Like `assertGuard`, but against a JSON Schema value. Root replacement throws.
|
|
251
|
+
|
|
222
252
|
---
|
|
223
253
|
|
|
224
254
|
### Utility Functions and Classes
|
|
@@ -226,6 +256,8 @@ Generates a raw JSON Schema draft-07 object matching type `T` at compile time.
|
|
|
226
256
|
#### `convertPropertyCasing<T, C extends CasingFormat>(obj: T, casing: C, options?: ConvertCasingOptions): ConvertPropertyCasing<T, C>`
|
|
227
257
|
|
|
228
258
|
Recursively converts all property keys of an object to the specified casing format.
|
|
259
|
+
If two source keys normalize to the same output key, conversion throws instead of silently discarding a value.
|
|
260
|
+
Special keys such as `__proto__` are preserved as own data properties without changing the result prototype.
|
|
229
261
|
|
|
230
262
|
- **Parameters**:
|
|
231
263
|
- `obj`: The source object.
|
|
@@ -270,25 +302,71 @@ An error class wrapper that transforms internal validation errors into a Zod-lik
|
|
|
270
302
|
|
|
271
303
|
### Interfaces and Types
|
|
272
304
|
|
|
305
|
+
#### `type ValidationMode`
|
|
306
|
+
|
|
307
|
+
Controls **unknown object keys only**. It does **not** coerce or revive values — that is exclusively `from`.
|
|
308
|
+
|
|
309
|
+
| Value | Behavior |
|
|
310
|
+
| :--- | :--- |
|
|
311
|
+
| `'strict'` (default) | Reject properties not declared on the type/schema. |
|
|
312
|
+
| `'relaxed'` | Allow unknown properties and keep them on the result. No type conversion. |
|
|
313
|
+
| `'strip'` | Drop unknown properties from the result (in place when mutating). |
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// OK: extra keys kept; age stays a string unless you also set from
|
|
317
|
+
validate<User>(data, 'relaxed');
|
|
318
|
+
|
|
319
|
+
// Coercion is a separate axis:
|
|
320
|
+
validate<User>(data, { mode: 'relaxed', from: 'query' });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### `interface GuardOptions`
|
|
324
|
+
|
|
325
|
+
Options for `is` / `isSchema`. Always mutate in place (no `mutate` / `errorFactory`).
|
|
326
|
+
|
|
327
|
+
| Property | Type | Default | Description |
|
|
328
|
+
| :--- | :--- | :--- | :--- |
|
|
329
|
+
| `mode` | `ValidationMode` | `'strict'` | Unknown-key policy (`strict` / `relaxed` / `strip`). Not coercion — see `ValidationMode` above. |
|
|
330
|
+
| `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. |
|
|
331
|
+
|
|
332
|
+
#### `interface AssertGuardOptions`
|
|
333
|
+
|
|
334
|
+
Extends `GuardOptions` for `assertGuard` / `assertGuardSchema`.
|
|
335
|
+
|
|
336
|
+
| Property | Type | Default | Description |
|
|
337
|
+
| :--- | :--- | :--- | :--- |
|
|
338
|
+
| `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory when the guard throws. |
|
|
339
|
+
|
|
273
340
|
#### `interface ValidationOptions`
|
|
274
341
|
|
|
275
|
-
|
|
342
|
+
Extends `GuardOptions` for `validate` / `validateSchema` (adds `mutate`; no `errorFactory`).
|
|
343
|
+
|
|
344
|
+
| Property | Type | Default | Description |
|
|
345
|
+
| :--- | :--- | :--- | :--- |
|
|
346
|
+
| `mode` | `ValidationMode` | `'strict'` | Unknown-key policy (`strict` / `relaxed` / `strip`). Not coercion — see `ValidationMode` above. |
|
|
347
|
+
| `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). |
|
|
348
|
+
| `mutate` | `boolean` | `false` | `true`: write in place while validating (half-changed input on failure is allowed; union arms still use a side tree). `false`: always allocate new containers. |
|
|
349
|
+
|
|
350
|
+
#### `interface AssertOptions`
|
|
351
|
+
|
|
352
|
+
Extends `ValidationOptions` for `assert` / `assertSchema`.
|
|
276
353
|
|
|
277
354
|
| Property | Type | Default | Description |
|
|
278
355
|
| :--- | :--- | :--- | :--- |
|
|
279
|
-
| `
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
356
|
+
| `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory when assert throws. |
|
|
357
|
+
|
|
358
|
+
#### `type CoercionKind` / `interface PathContext` / `type FromCoercionContext`
|
|
359
|
+
|
|
360
|
+
- `CoercionKind`: expected runtime kind for custom `from` (`'Date' \| 'Array' \| …`) — a dispatch tag, not `typeof`.
|
|
361
|
+
- `PathContext`: `{ key, path, parent, root, index? }` shared by `constraint.Custom` and custom `from`.
|
|
362
|
+
- `FromCoercionContext`: `PathContext & { kind: CoercionKind }`.
|
|
285
363
|
|
|
286
364
|
#### `interface IValidation<T>`
|
|
287
365
|
|
|
288
366
|
Result object returned by `validate`.
|
|
289
367
|
|
|
290
368
|
- `success`: `boolean`
|
|
291
|
-
- `data?`: `T`
|
|
369
|
+
- `data?`: `T` — present only when `success` is `true`
|
|
292
370
|
- `errors?`: `IValidationError[]`
|
|
293
371
|
|
|
294
372
|
#### `interface IValidationError`
|
|
@@ -331,7 +409,7 @@ Used to apply value constraints to types.
|
|
|
331
409
|
| `constraint.MinItems<N, Msg?>` | Restricts array length to $\ge N$. |
|
|
332
410
|
| `constraint.MaxItems<N, Msg?>` | Restricts array length to $\le N$. |
|
|
333
411
|
| `constraint.UniqueItems<Msg?>` | Restricts arrays to deeply unique items. |
|
|
334
|
-
| `constraint.Custom<Fn, Msg?>` | Runs a custom validation function: `(val,
|
|
412
|
+
| `constraint.Custom<Fn, Msg?>` | Runs a custom validation function: `(val, PathContext) => boolean` (`key`, `path`, `parent`, `root`, optional `index`). |
|
|
335
413
|
| `constraint.Requires<Path | [Paths], Msg?>` | Enforces that other object property paths exist. |
|
|
336
414
|
| `constraint.Message<Msg>` | Fallback custom error message. |
|
|
337
415
|
|
|
@@ -371,7 +449,7 @@ Sanitizes and converts input values during validation.
|
|
|
371
449
|
- `transform.LowerCase`: Converts string to lowercase.
|
|
372
450
|
- `transform.UpperCase`: Converts string to uppercase.
|
|
373
451
|
- `transform.Capitalize`: Capitalizes the first letter.
|
|
374
|
-
- `transform.ToNumber`: Same coercion as `from: 'query'` for numbers
|
|
452
|
+
- `transform.ToNumber`: Same coercion as `from: 'query'` for numbers. The entire trimmed string must be a finite decimal/scientific number; partial strings, hexadecimal syntax, `NaN`, and infinities are rejected.
|
|
375
453
|
- `transform.ToBoolean`: Same coercion as `from: 'query'` for booleans (`true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`); unknown values are left unchanged and fail the boolean check.
|
|
376
454
|
- `transform.ToDate`: Same coercion as `from: 'query'` for dates (parseable strings and finite timestamps).
|
|
377
455
|
- `transform.Custom<Fn>`: Custom mapping function: `(val) => any`.
|
|
@@ -76,7 +76,7 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
|
|
|
76
76
|
return `validators.multipleOf(v, path, ctx, ${valStr}${msgArg})`;
|
|
77
77
|
}
|
|
78
78
|
if (c.type === 'pattern') {
|
|
79
|
-
return `validators.pattern(v, path, ctx,
|
|
79
|
+
return `validators.pattern(v, path, ctx, validators.safeRegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
|
|
80
80
|
}
|
|
81
81
|
if (c.type === 'format') {
|
|
82
82
|
return `v = validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
|
|
@@ -171,7 +171,7 @@ export function createArrayCheck(elementValidator, requiredUtils) {
|
|
|
171
171
|
}
|
|
172
172
|
export function createTemplateLiteralCheck(regexStr, expected, requiredUtils) {
|
|
173
173
|
requiredUtils.add('validators');
|
|
174
|
-
const tpl = `(v, path, ctx) => validators.templateLiteral(v, path, ctx,
|
|
174
|
+
const tpl = `(v, path, ctx) => validators.templateLiteral(v, path, ctx, validators.safeRegExp(${JSON.stringify(regexStr)}), ${JSON.stringify(expected)})`;
|
|
175
175
|
return stripPositions(templateToAst(tpl));
|
|
176
176
|
}
|
|
177
177
|
export function createUnionCheck(checks, requiredUtils, expected = 'Type<Union>') {
|
|
@@ -196,6 +196,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
196
196
|
p.validator
|
|
197
197
|
]));
|
|
198
198
|
const allowedKeys = props.map(p => ts.factory.createStringLiteral(p.name));
|
|
199
|
+
const allowedKeySet = ts.factory.createNewExpression(ts.factory.createIdentifier('Set'), undefined, [ts.factory.createArrayLiteralExpression(allowedKeys)]);
|
|
199
200
|
if (indexValidator) {
|
|
200
201
|
const tpl = `
|
|
201
202
|
(v, path, ctx) => {
|
|
@@ -208,7 +209,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
208
209
|
}
|
|
209
210
|
`;
|
|
210
211
|
return injectNodes(templateToAst(tpl), {
|
|
211
|
-
'__KEYS__':
|
|
212
|
+
'__KEYS__': allowedKeySet,
|
|
212
213
|
'__EXPECTED__': ts.factory.createStringLiteral(expected),
|
|
213
214
|
'__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true),
|
|
214
215
|
'__INDEX__': indexValidator
|
|
@@ -225,7 +226,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
225
226
|
}
|
|
226
227
|
`;
|
|
227
228
|
return injectNodes(templateToAst(tpl), {
|
|
228
|
-
'__KEYS__':
|
|
229
|
+
'__KEYS__': allowedKeySet,
|
|
229
230
|
'__EXPECTED__': ts.factory.createStringLiteral(expected),
|
|
230
231
|
'__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true)
|
|
231
232
|
});
|
|
@@ -266,7 +267,7 @@ export function createIntersectionCheck(checks, requiredUtils) {
|
|
|
266
267
|
let data = validators.objectShell(v, ctx);
|
|
267
268
|
for (let i = 0; i < checks.length; i++) {
|
|
268
269
|
const val = checks[i](v, path, ctx);
|
|
269
|
-
if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data))
|
|
270
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data)) validators.assign(data, val);
|
|
270
271
|
else data = val;
|
|
271
272
|
}
|
|
272
273
|
return data;
|
package/dist/engine/hoister.js
CHANGED
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
import ts from '../ts.js';
|
|
2
2
|
export function hoistRegistrations(sourceFile, cache, requiredUtils, schemasMap) {
|
|
3
|
-
|
|
3
|
+
const hasSchemas = !!(schemasMap && schemasMap.size > 0);
|
|
4
|
+
if (cache.size === 0 && requiredUtils.size === 0 && !hasSchemas) {
|
|
4
5
|
return sourceFile;
|
|
5
6
|
}
|
|
7
|
+
const declaredNames = collectDeclaredNames(sourceFile.statements);
|
|
6
8
|
const utilityStatements = [
|
|
7
9
|
// 1. import "@webergency-utils/typechecker/runtime";
|
|
8
10
|
ts.factory.createImportDeclaration(undefined, undefined, ts.factory.createStringLiteral('@webergency-utils/typechecker/runtime'), undefined)
|
|
9
11
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const utilityNames = new Set();
|
|
13
|
+
if (!declaredNames.has('validators') && !utilityNames.has('validators')) {
|
|
14
|
+
utilityNames.add('validators');
|
|
12
15
|
utilityStatements.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
13
16
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('validators'), undefined, undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('globalThis'), '__WEBERGENCY_TYPECHECKER_VALIDATORS__'))
|
|
14
17
|
], ts.NodeFlags.Const)));
|
|
15
18
|
}
|
|
16
|
-
if (!
|
|
17
|
-
|
|
19
|
+
if (!declaredNames.has('MetadataStore') && !utilityNames.has('MetadataStore')) {
|
|
20
|
+
utilityNames.add('MetadataStore');
|
|
18
21
|
utilityStatements.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
19
22
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('MetadataStore'), undefined, undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('globalThis'), '__WEBERGENCY_TYPECHECKER_METADATA_STORE__'))
|
|
20
23
|
], ts.NodeFlags.Const)));
|
|
21
24
|
}
|
|
22
25
|
const variablePrepends = [];
|
|
23
26
|
const registrationAppends = [];
|
|
27
|
+
const prependedNames = new Set(utilityNames);
|
|
24
28
|
for (const [hash, expr] of cache.entries()) {
|
|
29
|
+
const valName = `__val_${hash}`;
|
|
25
30
|
// const __val_hash = expr;
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
!hasVariableDeclaration(variablePrepends, `__val_${hash}`)) {
|
|
31
|
+
if (!declaredNames.has(valName) && !prependedNames.has(valName)) {
|
|
32
|
+
prependedNames.add(valName);
|
|
29
33
|
variablePrepends.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
30
|
-
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(
|
|
34
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(valName), undefined, undefined, expr)
|
|
31
35
|
], ts.NodeFlags.Const)));
|
|
32
36
|
}
|
|
33
37
|
// MetadataStore.registerValidator(hash, __val_hash);
|
|
34
38
|
registrationAppends.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'registerValidator'), undefined, [
|
|
35
39
|
ts.factory.createStringLiteral(hash),
|
|
36
|
-
ts.factory.createIdentifier(
|
|
40
|
+
ts.factory.createIdentifier(valName)
|
|
37
41
|
])));
|
|
38
42
|
}
|
|
39
43
|
if (schemasMap) {
|
|
@@ -92,15 +96,17 @@ function findInsertionIndex(statements) {
|
|
|
92
96
|
}
|
|
93
97
|
return statements.length;
|
|
94
98
|
}
|
|
95
|
-
function
|
|
99
|
+
function collectDeclaredNames(statements) {
|
|
100
|
+
const names = new Set();
|
|
96
101
|
for (const statement of statements) {
|
|
97
|
-
if (ts.isVariableStatement(statement)) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (!ts.isVariableStatement(statement)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const decl of statement.declarationList.declarations) {
|
|
106
|
+
if (ts.isIdentifier(decl.name)) {
|
|
107
|
+
names.add(decl.name.text);
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
|
-
return
|
|
111
|
+
return names;
|
|
106
112
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from '../ts.js';
|
|
2
|
-
export declare function buildValidator(type: ts.Type, checker: ts.TypeChecker, validatorsMap: Map<string, ts.Expression>, requiredUtils: Set<string
|
|
2
|
+
export declare function buildValidator(type: ts.Type, checker: ts.TypeChecker, validatorsMap: Map<string, ts.Expression>, requiredUtils: Set<string>, hash?: string): ts.Expression;
|
|
3
3
|
export declare function generateHash(type: ts.Type, checker: ts.TypeChecker): string;
|
|
4
4
|
export declare function objectToAst(val: any): ts.Expression;
|
|
5
5
|
export declare function getTypeComplexity(type: ts.Type, checker: ts.TypeChecker, visited: Set<number>): number;
|
package/dist/engine/resolver.js
CHANGED
|
@@ -129,13 +129,13 @@ function tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils
|
|
|
129
129
|
}
|
|
130
130
|
return createObjectCheck([...propMap.values()], requiredUtils, expected, indexValidator);
|
|
131
131
|
}
|
|
132
|
-
export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
133
|
-
const
|
|
134
|
-
if (validatorsMap.has(
|
|
135
|
-
return ts.factory.createIdentifier(`__val_${
|
|
132
|
+
export function buildValidator(type, checker, validatorsMap, requiredUtils, hash) {
|
|
133
|
+
const resolvedHash = hash ?? generateHash(type, checker);
|
|
134
|
+
if (validatorsMap.has(resolvedHash)) {
|
|
135
|
+
return ts.factory.createIdentifier(`__val_${resolvedHash}`);
|
|
136
136
|
}
|
|
137
137
|
// Set placeholder to handle circularity
|
|
138
|
-
validatorsMap.set(
|
|
138
|
+
validatorsMap.set(resolvedHash, ts.factory.createIdentifier(`PENDING_${resolvedHash}`));
|
|
139
139
|
let result;
|
|
140
140
|
const flags = type.getFlags();
|
|
141
141
|
const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
|
|
@@ -537,9 +537,9 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
537
537
|
result = createPrimitiveCheck('any', requiredUtils);
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
|
-
validatorsMap.delete(
|
|
541
|
-
validatorsMap.set(
|
|
542
|
-
return ts.factory.createIdentifier(`__val_${
|
|
540
|
+
validatorsMap.delete(resolvedHash);
|
|
541
|
+
validatorsMap.set(resolvedHash, result);
|
|
542
|
+
return ts.factory.createIdentifier(`__val_${resolvedHash}`);
|
|
543
543
|
}
|
|
544
544
|
function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
545
545
|
const flags = type.getFlags();
|
|
@@ -674,9 +674,16 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
|
674
674
|
}
|
|
675
675
|
return 'any';
|
|
676
676
|
}
|
|
677
|
+
const hashByType = new WeakMap();
|
|
677
678
|
export function generateHash(type, checker) {
|
|
679
|
+
const cached = hashByType.get(type);
|
|
680
|
+
if (cached) {
|
|
681
|
+
return cached;
|
|
682
|
+
}
|
|
678
683
|
const structuralSig = buildStructuralSignature(type, checker);
|
|
679
|
-
|
|
684
|
+
const hash = createHash('sha256').update(structuralSig).digest('hex').substring(0, 16);
|
|
685
|
+
hashByType.set(type, hash);
|
|
686
|
+
return hash;
|
|
680
687
|
}
|
|
681
688
|
export function objectToAst(val) {
|
|
682
689
|
if (val === null) {
|
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';
|
package/dist/runtime/casing.js
CHANGED
|
@@ -70,9 +70,26 @@ export function convertPropertyCasing(obj, casing, options) {
|
|
|
70
70
|
return obj.map((item) => convertPropertyCasing(item, casing, options));
|
|
71
71
|
}
|
|
72
72
|
const result = {};
|
|
73
|
+
const sourceKeys = new Map();
|
|
73
74
|
for (const [key, value] of Object.entries(obj)) {
|
|
74
75
|
const newKey = formatCasing(key, casing, options);
|
|
75
|
-
|
|
76
|
+
const previousKey = sourceKeys.get(newKey);
|
|
77
|
+
if (previousKey !== undefined) {
|
|
78
|
+
throw new Error(`Casing conversion collision: ${previousKey} and ${key} both map to ${newKey}`);
|
|
79
|
+
}
|
|
80
|
+
sourceKeys.set(newKey, key);
|
|
81
|
+
const nested = convertPropertyCasing(value, casing, options);
|
|
82
|
+
if (newKey !== '__proto__' && newKey !== 'constructor' && newKey !== 'prototype') {
|
|
83
|
+
result[newKey] = nested;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
Object.defineProperty(result, newKey, {
|
|
87
|
+
value: nested,
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
76
93
|
}
|
|
77
94
|
return result;
|
|
78
95
|
}
|
|
@@ -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,27 +7,57 @@ 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`: write in place while validating. `false` (default): 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
|
}
|
|
60
|
+
declare function createSafeRegex(source: string, flags?: string): RegExp;
|
|
30
61
|
/** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
|
|
31
62
|
export declare function coerceQueryNumber(v: any): any;
|
|
32
63
|
/** Query-style boolean coercion — shared by `from: 'query'` and `transform.ToBoolean`. */
|
|
@@ -40,6 +71,8 @@ export declare const validators: {
|
|
|
40
71
|
coerceQueryBoolean: typeof coerceQueryBoolean;
|
|
41
72
|
coerceQueryDate: typeof coerceQueryDate;
|
|
42
73
|
coerceJsonDate: typeof coerceJsonDate;
|
|
74
|
+
safeRegExp: typeof createSafeRegex;
|
|
75
|
+
assign: (target: any, source: any) => any;
|
|
43
76
|
string: (v: any, path: string, ctx: ValidationContext) => any;
|
|
44
77
|
number: (v: any, path: string, ctx: ValidationContext) => any;
|
|
45
78
|
bigint: (v: any, path: string, ctx: ValidationContext) => any;
|
|
@@ -53,9 +86,9 @@ export declare const validators: {
|
|
|
53
86
|
array: (v: any, path: string, ctx: ValidationContext, childValidator: Function) => any;
|
|
54
87
|
props: (v: any, data: any, path: string, ctx: ValidationContext, props: [string, boolean, Function][]) => void;
|
|
55
88
|
objectShell: (v: any, ctx: ValidationContext) => any;
|
|
56
|
-
stripExtras: (data: any, ctx: ValidationContext, allowedKeys?: string[]) => any;
|
|
57
|
-
additionalProps: (v: any, data: any, path: string, ctx: ValidationContext, knownKeys: string[]
|
|
58
|
-
object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[]
|
|
89
|
+
stripExtras: (data: any, ctx: ValidationContext, allowedKeys?: string[] | Set<string>) => any;
|
|
90
|
+
additionalProps: (v: any, data: any, path: string, ctx: ValidationContext, knownKeys: string[] | Set<string>, childValidator: Function) => void;
|
|
91
|
+
object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[] | Set<string>, expected?: string) => any;
|
|
59
92
|
templateLiteral: (v: any, path: string, ctx: ValidationContext, regex: RegExp, expected: string) => any;
|
|
60
93
|
minLength: (v: string, path: string, ctx: ValidationContext, min: number, message?: string) => string;
|
|
61
94
|
maxLength: (v: string, path: string, ctx: ValidationContext, max: number, message?: string) => string;
|
|
@@ -90,13 +123,13 @@ export declare class MetadataStoreClass {
|
|
|
90
123
|
registerSchema(hash: string, schema: any): void;
|
|
91
124
|
getSchema(hash: string): any;
|
|
92
125
|
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 |
|
|
126
|
+
is(validator: Function, value: any, options?: ValidationMode | GuardOptions): boolean;
|
|
127
|
+
assert(validator: Function, value: any, options?: ValidationMode | AssertOptions): any;
|
|
128
|
+
assertGuard(validator: Function, value: any, options?: ValidationMode | AssertGuardOptions): void;
|
|
96
129
|
validate(validator: Function, value: any, options?: ValidationMode | ValidationOptions): {
|
|
97
130
|
success: boolean;
|
|
98
131
|
errors: IValidationError[];
|
|
99
|
-
data
|
|
132
|
+
data?: any;
|
|
100
133
|
};
|
|
101
134
|
}
|
|
102
135
|
export declare function groupErrorsByPath(errors: IValidationError[]): Record<string, {
|