@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 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,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
- Configuration options to customize validator behavior.
336
+ Extends `GuardOptions` for `validate` / `validateSchema` (adds `mutate`; no `errorFactory`).
276
337
 
277
338
  | Property | Type | Default | Description |
278
339
  | :--- | :--- | :--- | :--- |
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. |
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, ctx) => boolean`. |
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
- 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';
@@ -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
- /** 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`: always write onto the input. `false` (default): always 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
  }
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 | ValidationOptions): boolean;
94
- assert(validator: Function, value: any, options?: ValidationMode | ValidationOptions): any;
95
- assertGuard(validator: Function, value: any, options?: ValidationMode | ValidationOptions): void;
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 pathKey(path) {
48
- if (!path) {
49
- return '';
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
- const bracket = path.lastIndexOf('[');
52
- const dot = path.lastIndexOf('.');
53
- if (bracket > dot) {
54
- const end = path.lastIndexOf(']');
55
- if (end > bracket) {
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 (dot >= 0) {
60
- return path.slice(dot + 1);
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, type) {
74
+ function fromCustom(ctx, path, value, kind) {
65
75
  if (typeof ctx.from !== 'function') {
66
76
  return value;
67
77
  }
68
- return ctx.from(pathKey(path), value, type);
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.wrapArrays && v !== undefined && v !== null) {
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
- const pathParts = tokenizePath(path);
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
- // Type predicates require the value already match T never coerce.
1149
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1150
- const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1151
- const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
1152
- validator(value, '', ctx);
1153
- return ctx.success;
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, wrapArrays, mutate, root: value };
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
- // Assertion predicates require the value already match T never coerce.
1175
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1176
- const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1177
- const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
1178
- validator(value, '', ctx);
1179
- if (!ctx.success) {
1180
- if (typeof opt === 'object' && opt?.errorFactory) {
1181
- throw opt.errorFactory(ctx.errors);
1182
- }
1183
- throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
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, wrapArrays, mutate, root: value };
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
  }
@@ -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 RUNTIME_FUNCTIONS = ['is', 'assert', 'assertGuard', 'validate', 'jsonSchema'];
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 (RUNTIME_FUNCTIONS.includes(fnName)) {
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
- let getCall;
36
- const arg0 = node.arguments[0] || ts.factory.createIdentifier('undefined');
37
- const arg1 = node.arguments[1] || ts.factory.createIdentifier('undefined');
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
- const getSchemaAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'getSchema');
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
- else if (fnName === 'is') {
87
- const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'is');
88
- return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
63
+ if (fnName === 'validate') {
64
+ return metadataCall('validate', [getValidator, inputArg, optionsArg]);
89
65
  }
90
- else if (fnName === 'assert') {
91
- const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'assert');
92
- return ts.factory.createCallExpression(mdStoreAccess, undefined, [getCall, arg0, arg1]);
66
+ if (fnName === 'is') {
67
+ return metadataCall('is', [getValidator, inputArg, optionsArg]);
93
68
  }
94
- else if (fnName === 'assertGuard') {
95
- const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'assertGuard');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webergency-utils/typechecker",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "TypeScript compiler plugin for runtime validation",
6
6
  "author": "radixxko",