@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 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`) containing generic arguments.
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 that returns `true` if a value already matches `T` (no coercion; `from` is ignored).
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 already matches `T` and narrows the outer scope (no coercion; `from` is ignored).
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 | ValidationOptions): input is ResolveDefaults<T>`
170
+ #### `is<T>(input: unknown, options?: ValidationMode | GuardOptions): input is ResolveDefaults<T>`
170
171
 
171
- A type guard function checking if the input matches type `T`. Does **not** coerce; `from` is ignored.
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 `ValidationOptions` object (`from` has no effect).
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 | ValidationOptions): ResolveDefaults<T>`
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 a `ValidationOptions` object.
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 | ValidationOptions): asserts input is ResolveDefaults<T>`
199
+ #### `assertGuard<T>(input: unknown, options?: ValidationMode | AssertGuardOptions): asserts input is ResolveDefaults<T>`
199
200
 
200
- An assertion guard that throws a validation error if the input does not match type `T`. Does **not** coerce; `from` is ignored.
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 a `ValidationOptions` object (`from` has no effect).
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
- Configuration options to customize validator behavior.
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
- | `mode` | `'strict' \| 'relaxed' \| 'strip'` | `'strict'` | Validation mode strategy (strict key checking, relaxed, or key stripping). |
280
- | `from` | `'json' \| 'query' \| ((key, value, type) => any)` | `undefined` | Input conversion mode for `validate` / `assert`. `'json'` revives JSON-impossible types (Date, RegExp, bigint, Set, Map). `'query'` also coerces querystring shapes (string→number/boolean, timestamps→Date, etc.). A function is called only on type mismatch. Ignored by `is` / `assertGuard`. |
281
- | `wrapArrays` | `boolean` | `false` | Wraps non-array values into single-element arrays if an array is expected. |
282
- | `mutate` | `boolean` | `false` | When true, write validated/coerced values onto the input. Default false: always return new containers. |
283
- | `schema` | `any` | `undefined` | Custom JSON Schema instance. |
284
- | `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory for `assert` throwing. |
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, ctx) => boolean`. |
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 (non-empty numeric strings `parseFloat`).
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, new RegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
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, new RegExp(${JSON.stringify(regexStr)}), ${JSON.stringify(expected)})`;
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__': ts.factory.createArrayLiteralExpression(allowedKeys),
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__': ts.factory.createArrayLiteralExpression(allowedKeys),
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)) Object.assign(data, val);
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;
@@ -1,39 +1,43 @@
1
1
  import ts from '../ts.js';
2
2
  export function hoistRegistrations(sourceFile, cache, requiredUtils, schemasMap) {
3
- if (cache.size === 0 && requiredUtils.size === 0) {
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
- if (!hasVariableDeclaration(sourceFile.statements, 'validators') &&
11
- !hasVariableDeclaration(utilityStatements, 'validators')) {
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 (!hasVariableDeclaration(sourceFile.statements, 'MetadataStore') &&
17
- !hasVariableDeclaration(utilityStatements, 'MetadataStore')) {
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 (!hasVariableDeclaration(sourceFile.statements, `__val_${hash}`) &&
27
- !hasVariableDeclaration(utilityStatements, `__val_${hash}`) &&
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(`__val_${hash}`), undefined, undefined, expr)
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(`__val_${hash}`)
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 hasVariableDeclaration(statements, name) {
99
+ function collectDeclaredNames(statements) {
100
+ const names = new Set();
96
101
  for (const statement of statements) {
97
- if (ts.isVariableStatement(statement)) {
98
- for (const decl of statement.declarationList.declarations) {
99
- if (ts.isIdentifier(decl.name) && decl.name.text === name) {
100
- return true;
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 false;
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>): ts.Expression;
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;
@@ -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 hash = generateHash(type, checker);
134
- if (validatorsMap.has(hash)) {
135
- return ts.factory.createIdentifier(`__val_${hash}`);
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(hash, ts.factory.createIdentifier(`PENDING_${hash}`));
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(hash);
541
- validatorsMap.set(hash, result);
542
- return ts.factory.createIdentifier(`__val_${hash}`);
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
- return createHash('sha256').update(structuralSig).digest('hex').substring(0, 16);
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
- export type ValidationMode = 'strict' | 'relaxed' | 'strip';
9
- export interface ValidationOptions {
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 | ValidationOptions): ResolveDefaults<T>;
22
- /** Asserts `input` already matches `T`. Does not coerce; `from` is ignored. */
23
- export declare function assertGuard<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): asserts _input is ResolveDefaults<T>;
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
- /** Returns whether `input` already matches `T`. Does not coerce; `from` is ignored. */
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` already matches `T`. Does not coerce; `from` is ignored. */
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';
@@ -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
- result[newKey] = convertPropertyCasing(value, casing, options);
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
- /** Internal expected-type labels for custom `from` callbacks. Not exported from the package. */
10
- type BaseType = 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'symbol' | 'never' | 'Date' | 'RegExp' | 'Set' | 'Map' | 'Array' | 'Object' | 'instance' | 'null' | 'undefined' | 'tuple' | 'literal';
11
- type FromOption = 'json' | 'query' | ((key: string, value: any, type: BaseType) => any);
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
- export interface ValidationOptions {
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
- wrapArrays?: boolean;
25
- /** When true, write validated/coerced values onto the input. Default false: always return new containers. */
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
- schema?: any;
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[], childValidator: Function) => void;
58
- object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[], expected?: string) => any;
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 | ValidationOptions): boolean;
94
- assert(validator: Function, value: any, options?: ValidationMode | ValidationOptions): any;
95
- assertGuard(validator: Function, value: any, options?: ValidationMode | ValidationOptions): void;
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: any;
132
+ data?: any;
100
133
  };
101
134
  }
102
135
  export declare function groupErrorsByPath(errors: IValidationError[]): Record<string, {