@scalar/validation 0.5.0 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @scalar/validation
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#9262](https://github.com/scalar/scalar/pull/9262): feat(validation): support recursive schemas in validate and coerce
8
+
9
+ Add recursive schema support for `validate` and `coerce`: both functions now track visited object–schema pairs so cyclic values (self-referential nodes, mutual `lazy` graphs, cyclic records) terminate instead of overflowing the stack. Nested `intersection` members are validated and coerced recursively, and union branch scoring prefers property-less object schemas over primitives for empty objects.
10
+
11
+ Improve `Static` inference for circular schemas via `LazyStatic` and tuple-folding union statics; `union` and `intersection` use lightweight `UnionMember` / `IntersectionMember` constraints so `lazy(() => self)` call sites type-check without hitting depth limits. `coerce` returns `SafeStatic<S>` (precise `Static<S>` for concrete schemas, `any` when `S` is the full `Schema` union). Export `UnionMember` and `IntersectionMember` from the package entry point.
12
+
13
+ ### Patch Changes
14
+
15
+ - [#9262](https://github.com/scalar/scalar/pull/9262): fix(validation): detect cycles when scoring union branches
16
+
17
+ `scoreUnion` (used by `coerce` to pick the best matching `union` branch) now tracks the `(value, schema)` pairs that are live on its call stack and returns a neutral positive score on re-entry. A recursive lazy schema such as `lazy(() => union([object({ child: optional(lazy(() => T)) }), …]))` evaluated against a self-referential value previously caused `scoreUnion` to recurse through `lazy → union → object → property → lazy → …` indefinitely and overflow the stack, contradicting the rest of the cycle-handling work. The marker is cleared in `finally` so sibling union branches that share a schema reference are scored independently.
18
+
19
+ - [#9262](https://github.com/scalar/scalar/pull/9262): fix(validation): do not leak cycle-detection cache across union branches
20
+
21
+ Scope the in-progress `(value, schema)` cache used by `validate` to the live call stack instead of treating it as run-wide memoization. The marker for each pair is now cleared before the call returns, so a shared schema reference that failed in one `union` branch (for example the common `base` in `union([intersection([base, objA]), intersection([base, objB])])`) is re-validated in the next branch rather than short-circuiting to `true` from a stale entry. Cycle detection on self-referential and mutually recursive lazy graphs is unaffected because the marker is still present during recursive descent into the same value.
22
+
3
23
  ## 0.5.0
4
24
 
5
25
  ### Minor Changes
package/dist/coerce.d.ts CHANGED
@@ -1,5 +1,31 @@
1
1
  import type { Schema } from './schema.js';
2
2
  import type { Static } from './types.js';
3
+ /**
4
+ * Memoizes `schema.schema()` per lazy schema so that recursive definitions
5
+ * such as `lazy(() => object({ child: lazy(() => T) }))` resolve to the same
6
+ * inner schema reference across calls. Without this, every traversal would
7
+ * synthesize a fresh inner schema, defeating the `(value, schema)` cycle
8
+ * cache and producing infinite recursion on self-referential values.
9
+ *
10
+ * The cache is supplied by the top-level `coerce` call so it never leaks
11
+ * resolved schemas between unrelated invocations.
12
+ */
13
+ type LazyCache = WeakMap<object, Schema>;
14
+ /**
15
+ * Falls back to `any` when `S` widens all the way to the full `Schema` union and
16
+ * returns the precise `Static<S>` otherwise. Computing `Static<Schema>` forces
17
+ * TypeScript to expand every variant of the recursive `Schema` definition and
18
+ * exhausts the depth limit, surfacing at call sites as
19
+ * `TS2589: Type instantiation is excessively deep and possibly infinite`.
20
+ * Degrading to `any` in that single case keeps the type tractable; callers that
21
+ * pass a specific schema (for example an `intersection(...)` literal) still get
22
+ * the precise static type.
23
+ *
24
+ * `[Schema] extends [S]` is wrapped in tuples to prevent distribution over union
25
+ * members — we want a single check that the whole `Schema` union is assignable
26
+ * to `S`, not a check that runs once per variant.
27
+ */
28
+ type SafeStatic<S extends Schema> = [Schema] extends [S] ? any : Static<S>;
3
29
  /**
4
30
  * Coerces an unknown value toward the static type implied by `schema`. Values that
5
31
  * pass {@link validate} for that branch are kept; otherwise primitives default to
@@ -18,5 +44,6 @@ import type { Static } from './types.js';
18
44
  * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
19
45
  * on cyclic graphs; callers normally omit it.
20
46
  */
21
- export declare const coerce: <S extends Schema>(schema: S, value: unknown, cache?: WeakMap<object, Set<Schema>>) => Static<S>;
47
+ export declare const coerce: <S extends Schema>(schema: S, value: unknown, cache?: WeakMap<object, Map<Schema, unknown>>, lazyCache?: LazyCache) => SafeStatic<S>;
48
+ export {};
22
49
  //# sourceMappingURL=coerce.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AA0FrC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,MAAM,CAAC,CAAC,CAmHV,CAAA"}
1
+ {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAGrC;;;;;;;;;GASG;AACH,KAAK,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AA+SxC;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAiB,EAC5D,YAAW,SAAyB,KACnC,UAAU,CAAC,CAAC,CAAkE,CAAA"}
package/dist/coerce.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import { isObject } from './helpers/is-object.js';
2
2
  import { validate } from './validate.js';
3
+ const resolveLazy = (schema, lazyCache) => {
4
+ const cached = lazyCache.get(schema);
5
+ if (cached) {
6
+ return cached;
7
+ }
8
+ const resolved = schema.schema();
9
+ lazyCache.set(schema, resolved);
10
+ return resolved;
11
+ };
3
12
  /**
4
13
  * True when this property schema is only used to discriminate union branches
5
14
  * (single literal, or a union of literals). No presence bonus when the value
@@ -24,92 +33,127 @@ const isDiscriminatorProperty = (schema) => {
24
33
  * Higher score means a closer match. Literals and matching object shapes
25
34
  * are weighted more heavily. Objects are scored by shape/literals;
26
35
  * arrays/records by structural type; primitives by validation; unions try all branches.
36
+ *
37
+ * The `scoringCache` tracks `(value, schema)` pairs that are currently being
38
+ * scored higher up the call stack. Without it, a recursive lazy schema such as
39
+ * `lazy(() => union([object({ child: optional(lazy(() => T)) }), …]))` scored
40
+ * against a self-referential value would recurse forever through
41
+ * `lazy → union → object → property → lazy → …` and overflow the stack.
42
+ *
43
+ * On re-entry of a pair we return `1` rather than `0` — a neutral positive
44
+ * score consistent with `validateInner` short-circuiting cycles to `true`.
45
+ * Markers are removed in `finally` so sibling union branches that share a
46
+ * schema reference are scored independently rather than inheriting a stale
47
+ * "in cycle" marker.
27
48
  */
28
- const scoreUnion = (schema, value) => {
29
- if (schema.type === 'object') {
30
- if (!isObject(value)) {
31
- return 0;
32
- }
33
- // Missing keys contribute 0 (including optional keys — matches prior union heuristics).
34
- // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion;
35
- // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields
36
- // on another branch; mismatches score 0 (no "key present" tie-break).
37
- // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }`
38
- // can still prefer the branch that declares `a`.
39
- return Object.keys(schema.properties).reduce((acc, key) => {
40
- if (!(key in value)) {
41
- return acc;
42
- }
43
- const propSchema = schema.properties[key];
44
- const raw = value[key];
45
- const base = scoreUnion(propSchema, raw);
46
- if (isDiscriminatorProperty(propSchema)) {
47
- return acc + (base > 0 ? base * 10 : 0);
48
- }
49
- return acc + (base > 0 ? base : 1);
50
- }, 0);
49
+ const scoreUnion = (schema, value, lazyCache, scoringCache = new WeakMap()) => {
50
+ // Short-circuit on cycles: this exact (value, schema) pair is already being
51
+ // scored higher up the call stack. The enclosing call's score subsumes any
52
+ // contribution we could compute here, so return a neutral positive score.
53
+ if (isObject(value) && scoringCache.get(value)?.has(schema)) {
54
+ return 1;
51
55
  }
52
- if (schema.type === 'array') {
53
- // Score 1 if value is an array, otherwise 0
54
- return Array.isArray(value) ? 1 : 0;
55
- }
56
- if (schema.type === 'record') {
57
- // TODO: implement smarter scoring for records (just a placeholder for now)
58
- return isObject(value) ? 1 : 0;
59
- }
60
- if (schema.type === 'optional') {
61
- return value === undefined ? 1 : scoreUnion(schema.schema, value);
62
- }
63
- if (schema.type === 'union') {
64
- // For a union, use the highest score among all sub-schemas
65
- return Math.max(...schema.schemas.map((schema) => scoreUnion(schema, value)));
56
+ const trackable = isObject(value);
57
+ if (trackable) {
58
+ const schemas = scoringCache.get(value) ?? new Set();
59
+ schemas.add(schema);
60
+ scoringCache.set(value, schemas);
66
61
  }
67
- if (schema.type === 'intersection') {
68
- if (schema.schemas.length === 0) {
69
- return 1;
62
+ try {
63
+ if (schema.type === 'object') {
64
+ if (!isObject(value)) {
65
+ return 0;
66
+ }
67
+ const keys = Object.keys(schema.properties);
68
+ // If there are no properties, we want to score 1 since we want to outscore if there are inline primitives
69
+ if (keys.length === 0) {
70
+ return 1;
71
+ }
72
+ // Missing keys contribute 0 (including optional keys — matches prior union heuristics).
73
+ // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion;
74
+ // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields
75
+ // on another branch; mismatches score 0 (no "key present" tie-break).
76
+ // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }`
77
+ // can still prefer the branch that declares `a`.
78
+ return keys.reduce((acc, key) => {
79
+ if (!(key in value)) {
80
+ return acc;
81
+ }
82
+ const propSchema = schema.properties[key];
83
+ const raw = value[key];
84
+ const base = scoreUnion(propSchema, raw, lazyCache, scoringCache);
85
+ if (isDiscriminatorProperty(propSchema)) {
86
+ return acc + (base > 0 ? base * 10 : 0);
87
+ }
88
+ return acc + (base > 0 ? base : 1);
89
+ }, 0);
70
90
  }
71
- return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value), 0);
72
- }
73
- if (schema.type === 'lazy') {
74
- // For a lazy schema, evaluate the inner schema and recurse
75
- return scoreUnion(schema.schema(), value);
91
+ if (schema.type === 'array') {
92
+ // Score 1 if value is an array, otherwise 0
93
+ return Array.isArray(value) ? 1 : 0;
94
+ }
95
+ if (schema.type === 'record') {
96
+ // TODO: implement smarter scoring for records (just a placeholder for now)
97
+ return isObject(value) ? 1 : 0;
98
+ }
99
+ if (schema.type === 'optional') {
100
+ return value === undefined ? 1 : scoreUnion(schema.schema, value, lazyCache, scoringCache);
101
+ }
102
+ if (schema.type === 'union') {
103
+ // For a union, use the highest score among all sub-schemas
104
+ return Math.max(...schema.schemas.map((branch) => scoreUnion(branch, value, lazyCache, scoringCache)));
105
+ }
106
+ if (schema.type === 'intersection') {
107
+ if (schema.schemas.length === 0) {
108
+ return 1;
109
+ }
110
+ return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value, lazyCache, scoringCache), 0);
111
+ }
112
+ if (schema.type === 'lazy') {
113
+ // For a lazy schema, evaluate the inner schema and recurse
114
+ return scoreUnion(resolveLazy(schema, lazyCache), value, lazyCache, scoringCache);
115
+ }
116
+ if (schema.type === 'evaluate') {
117
+ // For an evaluate schema, evaluate the expression and recurse
118
+ return scoreUnion(schema.schema, schema.expression(value), lazyCache, scoringCache);
119
+ }
120
+ // For primitives and any other type, return 1 if valid, otherwise 0
121
+ return validate(schema, value) ? 1 : 0;
76
122
  }
77
- if (schema.type === 'evaluate') {
78
- // For an evaluate schema, evaluate the expression and recurse
79
- return scoreUnion(schema.schema, schema.expression(value));
123
+ finally {
124
+ // Clear the in-progress marker so sibling union branches that reference
125
+ // the same schema are scored independently rather than short-circuiting
126
+ // to the cycle-neutral score.
127
+ if (trackable) {
128
+ scoringCache.get(value)?.delete(schema);
129
+ }
80
130
  }
81
- // For primitives and any other type, return 1 if valid, otherwise 0
82
- return validate(schema, value) ? 1 : 0;
83
131
  };
84
132
  /**
85
- * Coerces an unknown value toward the static type implied by `schema`. Values that
86
- * pass {@link validate} for that branch are kept; otherwise primitives default to
87
- * `0`, `''`, or `false`, and arrays, records, and objects are built recursively.
88
- * Unions pick the best-matching branch; `evaluate` runs `expression` before the inner schema.
89
- *
90
- * @example
91
- * ```ts
92
- * import { coerce, number, object, string } from '@scalar/validation'
93
- *
94
- * coerce(number(), 42) // 42
95
- * coerce(number(), 'nope') // 0 — invalid number uses default
96
- * coerce(object({ id: number(), name: string() }), { id: '1', name: 'Ada' }) // { id: 0, name: 'Ada' }
97
- * ```
98
- *
99
- * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
100
- * on cyclic graphs; callers normally omit it.
133
+ * Records the in-progress `result` for a given `(value, schema)` pair so that
134
+ * recursive calls hitting the same pair return the already-allocated result
135
+ * instead of recursing forever. Plain objects and arrays are both tracked;
136
+ * other values cannot form cycles and are ignored.
101
137
  */
102
- export const coerce = (schema, value, cache = new WeakMap()) => {
103
- // Prevent infinite recursion
104
- if (isObject(value) && cache.get(value)?.has(schema)) {
105
- return value;
106
- }
107
- // Track visited schemas to prevent infinite recursion
108
- if (isObject(value)) {
109
- const schemas = cache.get(value) || new Set();
110
- schemas.add(schema);
138
+ const trackCycle = (value, schema, result, cache) => {
139
+ if (isObject(value) || Array.isArray(value)) {
140
+ const schemas = cache.get(value) || new Map();
141
+ schemas.set(schema, result);
111
142
  cache.set(value, schemas);
112
143
  }
144
+ };
145
+ /**
146
+ * Internal coercion implementation. Takes the wide `Schema` union and returns `unknown` so that
147
+ * recursive calls do not pay the cost of relating two generic `Static<S>` instantiations, which
148
+ * can overflow the type checker now that `LazyStatic` resolves recursive schemas without a depth
149
+ * cap. The public `coerce` wrapper preserves the typed surface.
150
+ */
151
+ const coerceInner = (schema, value, cache, lazyCache) => {
152
+ // Prevent infinite recursion by returning the in-progress result that was
153
+ // staged by an enclosing call via trackCycle.
154
+ if ((isObject(value) || Array.isArray(value)) && cache.get(value)?.has(schema)) {
155
+ return cache.get(value)?.get(schema);
156
+ }
113
157
  // If no schema is provided, return the value as is
114
158
  if (!schema) {
115
159
  return value;
@@ -121,25 +165,25 @@ export const coerce = (schema, value, cache = new WeakMap()) => {
121
165
  if (typeof value === 'function') {
122
166
  return value;
123
167
  }
124
- return (() => undefined);
168
+ return () => undefined;
125
169
  }
126
170
  if (schema.type === 'number') {
127
171
  if (validate(schema, value)) {
128
172
  return value;
129
173
  }
130
- return (schema.default ?? 0);
174
+ return schema.default ?? 0;
131
175
  }
132
176
  if (schema.type === 'string') {
133
177
  if (validate(schema, value)) {
134
178
  return value;
135
179
  }
136
- return (schema.default ?? '');
180
+ return schema.default ?? '';
137
181
  }
138
182
  if (schema.type === 'boolean') {
139
183
  if (validate(schema, value)) {
140
184
  return value;
141
185
  }
142
- return (schema.default ?? false);
186
+ return schema.default ?? false;
143
187
  }
144
188
  if (schema.type === 'nullable') {
145
189
  return null;
@@ -151,56 +195,92 @@ export const coerce = (schema, value, cache = new WeakMap()) => {
151
195
  if (value === undefined) {
152
196
  return undefined;
153
197
  }
154
- return coerce(schema.schema, value, cache);
198
+ return coerceInner(schema.schema, value, cache, lazyCache);
155
199
  }
156
200
  if (schema.type === 'array') {
157
201
  if (!Array.isArray(value)) {
158
202
  return [];
159
203
  }
160
- return value.map((item) => coerce(schema.items, item, cache));
204
+ // Pre-allocate so a self-referential array can be cached before we
205
+ // recurse into its items, breaking otherwise-infinite cycles.
206
+ const result = new Array(value.length);
207
+ trackCycle(value, schema, result, cache);
208
+ for (let i = 0; i < value.length; i++) {
209
+ result[i] = coerceInner(schema.items, value[i], cache, lazyCache);
210
+ }
211
+ return result;
161
212
  }
162
213
  if (schema.type === 'record') {
163
214
  if (!isObject(value)) {
164
215
  return {};
165
216
  }
166
- return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, coerce(schema.value, value, cache)]));
217
+ // Pre-allocate so a self-referential record can be cached before we
218
+ // recurse into its entries, breaking otherwise-infinite cycles.
219
+ const result = {};
220
+ trackCycle(value, schema, result, cache);
221
+ for (const key of Object.keys(value)) {
222
+ result[key] = coerceInner(schema.value, value[key], cache, lazyCache);
223
+ }
224
+ return result;
167
225
  }
168
226
  if (schema.type === 'object') {
169
227
  const keys = Object.keys(schema.properties);
170
228
  const target = isObject(value) ? value : null;
171
- const entries = [];
229
+ // Pre-allocate so a self-referential object can be cached before we
230
+ // recurse into its properties, breaking otherwise-infinite cycles.
231
+ const result = {};
232
+ trackCycle(value, schema, result, cache);
172
233
  for (const key of keys) {
173
234
  const propSchema = schema.properties[key];
174
235
  const raw = target?.[key];
175
236
  if (propSchema.type === 'optional' && raw === undefined) {
176
237
  continue;
177
238
  }
178
- entries.push([key, coerce(propSchema, raw, cache)]);
239
+ result[key] = coerceInner(propSchema, raw, cache, lazyCache);
179
240
  }
180
- return Object.fromEntries(entries);
241
+ return result;
181
242
  }
182
243
  if (schema.type === 'union') {
183
- const branch = schema.schemas.reduce((acc, schema) => {
184
- const score = scoreUnion(schema, value);
185
- return score > acc.score ? { schema, score } : acc;
244
+ const branch = schema.schemas.reduce((acc, branchSchema) => {
245
+ const score = scoreUnion(branchSchema, value, lazyCache);
246
+ return score > acc.score ? { schema: branchSchema, score } : acc;
186
247
  }, { schema: schema.schemas[0], score: 0 });
187
248
  // We need some way to pick one of the union values
188
- return coerce(branch.schema, value, cache);
249
+ return coerceInner(branch.schema, value, cache, lazyCache);
189
250
  }
190
251
  if (schema.type === 'intersection') {
191
- return schema.schemas.reduce((acc, subSchema) => Object.assign(acc, coerce(subSchema, value, cache)), {});
252
+ return schema.schemas.reduce((acc, subSchema) => Object.assign(acc, coerceInner(subSchema, value, cache, lazyCache)), {});
192
253
  }
193
254
  if (schema.type === 'literal') {
194
255
  return schema.value;
195
256
  }
196
257
  if (schema.type === 'lazy') {
197
- return coerce(schema.schema(), value, cache);
258
+ return coerceInner(resolveLazy(schema, lazyCache), value, cache, lazyCache);
198
259
  }
199
260
  if (schema.type === 'evaluate') {
200
- return coerce(schema.schema, schema.expression(value), cache);
261
+ return coerceInner(schema.schema, schema.expression(value), cache, lazyCache);
201
262
  }
202
263
  // We need to assert here that schema has the type never so we know we handle all cases
203
264
  const _exhaustive = schema;
204
265
  console.warn('Unknown schema type:', _exhaustive);
205
266
  return value;
206
267
  };
268
+ /**
269
+ * Coerces an unknown value toward the static type implied by `schema`. Values that
270
+ * pass {@link validate} for that branch are kept; otherwise primitives default to
271
+ * `0`, `''`, or `false`, and arrays, records, and objects are built recursively.
272
+ * Unions pick the best-matching branch; `evaluate` runs `expression` before the inner schema.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { coerce, number, object, string } from '@scalar/validation'
277
+ *
278
+ * coerce(number(), 42) // 42
279
+ * coerce(number(), 'nope') // 0 — invalid number uses default
280
+ * coerce(object({ id: number(), name: string() }), { id: '1', name: 'Ada' }) // { id: 0, name: 'Ada' }
281
+ * ```
282
+ *
283
+ * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
284
+ * on cyclic graphs; callers normally omit it.
285
+ */
286
+ export const coerce = (schema, value, cache = new WeakMap(), lazyCache = new WeakMap()) => coerceInner(schema, value, cache, lazyCache);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { coerce } from './coerce.js';
2
- export { type AnySchema, type ArraySchema, type BooleanSchema, type EvaluateSchema, type IntersectionSchema, type LazySchema, type LiteralSchema, type NotDefinedSchema, type NullableSchema, type NumberSchema, type ObjectSchema, type OptionalSchema, type RecordSchema, type Schema, type StringSchema, type UnionSchema, type UnknownSchema, type FunctionSchema, any, array, boolean, evaluate, fn, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, unknown, } from './schema.js';
2
+ export { type AnySchema, type ArraySchema, type BooleanSchema, type EvaluateSchema, type FunctionSchema, type IntersectionMember, type IntersectionSchema, type LazySchema, type LiteralSchema, type NotDefinedSchema, type NullableSchema, type NumberSchema, type ObjectSchema, type OptionalSchema, type RecordSchema, type Schema, type StringSchema, type UnionMember, type UnionSchema, type UnknownSchema, any, array, boolean, evaluate, fn, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, unknown, } from './schema.js';
3
3
  export { type GenerateTypesOptions, generateTypes } from './typegen.js';
4
4
  export type { Static } from './types.js';
5
5
  export { validate } from './validate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,EAAE,EACF,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,GACR,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,EAAE,EACF,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,GACR,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
package/dist/schema.d.ts CHANGED
@@ -67,10 +67,52 @@ export type ObjectSchema<Properties extends Record<string, Schema>> = {
67
67
  properties: Properties;
68
68
  } & Documentation;
69
69
  /** Schema that matches if any member schema matches (discriminated union when literals or object tags differ). */
70
- export type UnionSchema<Schemas extends Schema[]> = {
70
+ export type UnionSchema<Schemas extends readonly Schema[]> = {
71
71
  type: 'union';
72
72
  schemas: Schemas;
73
73
  } & Documentation;
74
+ /**
75
+ * Members that may appear inside a `union([...])`.
76
+ *
77
+ * Discriminant-only for the same reason as {@link IntersectionMember}: a full `Schema` constraint
78
+ * at the call site forces TypeScript to eagerly evaluate tuple elements and breaks circular
79
+ * inference when members use `lazy(() => self)` (for example recursive navigation trees).
80
+ */
81
+ export type UnionMember = {
82
+ type: 'number';
83
+ } | {
84
+ type: 'string';
85
+ } | {
86
+ type: 'boolean';
87
+ } | {
88
+ type: 'nullable';
89
+ } | {
90
+ type: 'notDefined';
91
+ } | {
92
+ type: 'any';
93
+ } | {
94
+ type: 'unknown';
95
+ } | {
96
+ type: 'function';
97
+ } | {
98
+ type: 'array';
99
+ } | {
100
+ type: 'record';
101
+ } | {
102
+ type: 'object';
103
+ } | {
104
+ type: 'union';
105
+ } | {
106
+ type: 'optional';
107
+ } | {
108
+ type: 'intersection';
109
+ } | {
110
+ type: 'literal';
111
+ } | {
112
+ type: 'lazy';
113
+ } | {
114
+ type: 'evaluate';
115
+ };
74
116
  /**
75
117
  * Schema that accepts `undefined` or a value matching the inner schema.
76
118
  * In {@link Static} and type generation, object properties use `key?:` instead of `T | undefined`.
@@ -80,10 +122,28 @@ export type OptionalSchema<S extends Schema> = {
80
122
  schema: S;
81
123
  } & Documentation;
82
124
  /**
83
- * `UnionSchema<any>` avoids a variance pitfall: `UnionSchema<[A, B]>` is not assignable to
84
- * `UnionSchema<ObjectSchema<any>[]>`, which breaks `infer` when resolving `Static`.
125
+ * Members that may appear inside an `intersection([...])`.
126
+ *
127
+ * This is intentionally a discriminant-only structural type rather than a union of the full
128
+ * schema types. If we use the full schema types here, the constraint check at the call site
129
+ * of `intersection` forces TypeScript to *eagerly* evaluate each tuple element, which breaks
130
+ * circular type inference when a member transitively contains a `lazy(() => self)` reference.
131
+ *
132
+ * The narrower discriminant shape only requires TypeScript to verify the `type` literal, which
133
+ * is cheap and does not trigger evaluation of nested schemas. Every `ObjectSchema`, `UnionSchema`,
134
+ * `IntersectionSchema`, and `LazySchema` is still assignable to this type because each carries
135
+ * the appropriate `type` field, so the public API and compile-time safety are preserved.
85
136
  */
86
- export type IntersectionSchema<Schemas extends readonly (ObjectSchema<any> | UnionSchema<any>)[]> = {
137
+ export type IntersectionMember = {
138
+ type: 'object';
139
+ } | {
140
+ type: 'union';
141
+ } | {
142
+ type: 'intersection';
143
+ } | {
144
+ type: 'lazy';
145
+ };
146
+ export type IntersectionSchema<Schemas extends readonly Schema[]> = {
87
147
  type: 'intersection';
88
148
  schemas: Schemas;
89
149
  } & Documentation;
@@ -110,7 +170,7 @@ export type EvaluateSchema<S extends Schema> = {
110
170
  expression: (value: unknown) => unknown;
111
171
  schema: S;
112
172
  };
113
- export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | UnknownSchema | FunctionSchema<any> | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<any[]> | OptionalSchema<any> | IntersectionSchema<readonly (ObjectSchema<any> | UnionSchema<ObjectSchema<any>[]>)[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>;
173
+ export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | UnknownSchema | FunctionSchema<any> | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<readonly Schema[]> | OptionalSchema<any> | IntersectionSchema<readonly Schema[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>;
114
174
  declare const number: (options?: Documentation & {
115
175
  default?: number;
116
176
  }) => NumberSchema;
@@ -128,8 +188,20 @@ declare const fn: <T extends (...args: any[]) => any = (...args: unknown[]) => u
128
188
  declare const array: <Item extends Schema>(items: Item, options?: Documentation) => ArraySchema<Item>;
129
189
  declare const record: <Key extends StringSchema | AnySchema, Value extends Schema>(key: Key, value: Value, options?: Documentation) => RecordSchema<Key, Value>;
130
190
  declare const object: <Properties extends Record<string, Schema>>(properties: Properties, options?: Documentation) => ObjectSchema<Properties>;
131
- declare const union: <Schemas extends Schema[]>(schemas: Schemas, options?: Documentation) => UnionSchema<Schemas>;
132
- declare const intersection: <const Schemas extends readonly (ObjectSchema<any> | UnionSchema<ObjectSchema<any>[]>)[]>(schemas: Schemas, options?: Documentation) => IntersectionSchema<Schemas>;
191
+ /**
192
+ * The conditional return type mirrors {@link intersection}: lightweight input constraint,
193
+ * precise `UnionSchema<Schemas>` output without re-triggering eager evaluation.
194
+ */
195
+ declare const union: <const Schemas extends readonly UnionMember[]>(schemas: Schemas, options?: Documentation) => Schemas extends readonly Schema[] ? UnionSchema<Schemas> : never;
196
+ /**
197
+ * The conditional return type is what unlocks circular `intersection([... lazy(() => self) ...])`.
198
+ *
199
+ * The input constraint is the lightweight `IntersectionMember` (discriminant-only) so the call-site
200
+ * constraint check does not force TypeScript to eagerly evaluate each tuple element. The conditional
201
+ * `Schemas extends readonly Schema[]` is always true in practice (every passed value is a real schema)
202
+ * and lets us produce a precise `IntersectionSchema<Schemas>` without re-introducing the heavy check.
203
+ */
204
+ declare const intersection: <const Schemas extends readonly IntersectionMember[]>(schemas: Schemas, options?: Documentation) => Schemas extends readonly Schema[] ? IntersectionSchema<Schemas> : never;
133
205
  declare const optional: <S extends Schema>(schema: S, options?: Documentation) => OptionalSchema<S>;
134
206
  declare const literal: <Value extends string | number | boolean | bigint>(value: Value) => LiteralSchema<Value>;
135
207
  declare const lazy: <S extends () => Schema>(schema: S) => LazySchema<S>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -60,12 +60,24 @@ const object = (properties, options) => ({
60
60
  typeName: options?.typeName,
61
61
  typeComment: options?.typeComment,
62
62
  });
63
+ /**
64
+ * The conditional return type mirrors {@link intersection}: lightweight input constraint,
65
+ * precise `UnionSchema<Schemas>` output without re-triggering eager evaluation.
66
+ */
63
67
  const union = (schemas, options) => ({
64
68
  type: 'union',
65
69
  schemas,
66
70
  typeName: options?.typeName,
67
71
  typeComment: options?.typeComment,
68
72
  });
73
+ /**
74
+ * The conditional return type is what unlocks circular `intersection([... lazy(() => self) ...])`.
75
+ *
76
+ * The input constraint is the lightweight `IntersectionMember` (discriminant-only) so the call-site
77
+ * constraint check does not force TypeScript to eagerly evaluate each tuple element. The conditional
78
+ * `Schemas extends readonly Schema[]` is always true in practice (every passed value is a real schema)
79
+ * and lets us produce a precise `IntersectionSchema<Schemas>` without re-introducing the heavy check.
80
+ */
69
81
  const intersection = (schemas, options) => ({
70
82
  type: 'intersection',
71
83
  schemas,
package/dist/types.d.ts CHANGED
@@ -1,5 +1,21 @@
1
1
  import type { AnySchema, ArraySchema, BooleanSchema, EvaluateSchema, FunctionSchema, IntersectionSchema, LazySchema, LiteralSchema, NotDefinedSchema, NullableSchema, NumberSchema, ObjectSchema, OptionalSchema, RecordSchema, Schema, StringSchema, UnionSchema, UnknownSchema } from './schema.js';
2
2
  export type Static<T> = _Static<T, 10>;
3
+ /**
4
+ * Indirection through a named generic alias for `LazySchema`.
5
+ *
6
+ * TypeScript caches and lazily expands named generic type aliases, so referencing
7
+ * `LazyStatic<F>` instead of inlining `_Static<ReturnType<F>>` lets the resolved
8
+ * type be re-entered for circular schemas (`lazy(() => self)`) without hitting the
9
+ * depth limit. Accessing properties on the resulting type expands one level at a
10
+ * time on demand, instead of materialising the whole structure up front.
11
+ */
12
+ type LazyStatic<F extends () => Schema> = F extends () => infer S ? (S extends Schema ? _Static<S, 10> : never) : never;
13
+ /**
14
+ * Folds union member schemas into a union of their static types.
15
+ * Uses `Schema` for tuple positions (not a narrower alias) so `infer First extends …` does not
16
+ * reject valid tuple elements and collapse to `never`.
17
+ */
18
+ type UnionObjectStatics<Schemas extends readonly Schema[], Depth extends number> = Schemas extends readonly [] ? never : Schemas extends readonly [infer First extends Schema, ...infer Rest extends readonly Schema[]] ? _Static<First, Depth> | UnionObjectStatics<Rest, Depth> : _Static<Schemas[number], Depth>;
3
19
  /**
4
20
  * Folds intersection member schemas into an intersection of their static types.
5
21
  * Uses `Schema` for tuple positions (not a narrower alias) so `infer First extends …` does not
@@ -22,7 +38,7 @@ type ObjectStatics<Properties, Depth extends number> = [keyof Properties] extend
22
38
  } & {
23
39
  [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>>;
24
40
  };
25
- type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends UnknownSchema ? unknown : T extends FunctionSchema<infer F> ? F : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? ObjectStatics<Properties, Depth> : T extends OptionalSchema<infer S> ? _Static<S, Prev<Depth>> | undefined : T extends IntersectionSchema<infer Schemas> ? IntersectObjectStatics<Schemas, Prev<Depth>> : T extends UnionSchema<infer Schemas> ? _Static<Schemas[number], Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? _Static<ReturnType<S>, Prev<Depth>> : never;
41
+ type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends UnknownSchema ? unknown : T extends FunctionSchema<infer F> ? F : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? ObjectStatics<Properties, Depth> : T extends OptionalSchema<infer S> ? _Static<S, Prev<Depth>> | undefined : T extends IntersectionSchema<infer Schemas> ? IntersectObjectStatics<Schemas, Prev<Depth>> : T extends UnionSchema<infer Schemas> ? UnionObjectStatics<Schemas, Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? LazyStatic<S> : never;
26
42
  type Prev<T extends number> = T extends 10 ? 9 : T extends 9 ? 8 : T extends 8 ? 7 : T extends 7 ? 6 : T extends 6 ? 5 : T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : 0;
27
43
  export {};
28
44
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -31,8 +31,12 @@ import type { Schema } from './schema.js';
31
31
  * validate(schema, { id: 1, name: 2 }) // false
32
32
  * ```
33
33
  *
34
+ * The optional `cache` argument tracks visited object–schema pairs to stop
35
+ * infinite recursion on cyclic value graphs (for example a node whose child
36
+ * points back at itself paired with a `lazy` schema). Callers normally omit it.
37
+ *
34
38
  * If schema is `undefined`, validation fails.
35
39
  * Returns true if the value matches the schema, false otherwise.
36
40
  */
37
- export declare const validate: (schema: Schema | undefined, value: unknown) => boolean;
41
+ export declare const validate: (schema: Schema | undefined, value: unknown, cache?: WeakMap<object, Set<Schema>>) => boolean;
38
42
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,GAAG,SAAS,EAAE,OAAO,OAAO,KAAG,OAwErE,CAAA"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAsHtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,QAAQ,GACnB,QAAQ,MAAM,GAAG,SAAS,EAC1B,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,OAA8C,CAAA"}
package/dist/validate.js CHANGED
@@ -1,4 +1,114 @@
1
1
  import { isObject } from './helpers/is-object.js';
2
+ /**
3
+ * Internal validation implementation. Threads a `cache` of `(object, schema)`
4
+ * pairs that are *currently in flight on the call stack* so cyclic graphs
5
+ * terminate instead of recursing forever. Re-entering a pair that is already
6
+ * being validated higher up the stack short-circuits to `true`: any concrete
7
+ * mismatch would surface at that enclosing call rather than via the cycle.
8
+ *
9
+ * Crucially the marker is removed before this call returns, so the cache only
10
+ * ever describes the live call stack and not "ever visited in this run".
11
+ * Without that removal a failed branch (for example the first member of a
12
+ * `union`) would leave stale entries that later sibling branches sharing the
13
+ * same schema reference would mistake for a successful cycle short-circuit.
14
+ */
15
+ const validateInner = (schema, value, cache) => {
16
+ if (!schema) {
17
+ return false;
18
+ }
19
+ // Short-circuit on cycles: this exact `(value, schema)` pair is already
20
+ // being validated higher up the call stack.
21
+ const trackable = isObject(value) || Array.isArray(value);
22
+ if (trackable && cache.get(value)?.has(schema)) {
23
+ return true;
24
+ }
25
+ // Mark this `(value, schema)` pair as in-progress for the duration of this
26
+ // call. Plain objects and arrays can form cycles; primitives and other
27
+ // non-plain objects do not need (or get) an entry.
28
+ if (trackable) {
29
+ const schemas = cache.get(value) ?? new Set();
30
+ schemas.add(schema);
31
+ cache.set(value, schemas);
32
+ }
33
+ try {
34
+ if (schema.type === 'any' || schema.type === 'unknown') {
35
+ return true;
36
+ }
37
+ if (schema.type === 'function') {
38
+ return typeof value === 'function';
39
+ }
40
+ if (schema.type === 'number') {
41
+ return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value);
42
+ }
43
+ if (schema.type === 'string') {
44
+ return typeof value === 'string';
45
+ }
46
+ if (schema.type === 'boolean') {
47
+ return typeof value === 'boolean';
48
+ }
49
+ if (schema.type === 'nullable') {
50
+ return value === null;
51
+ }
52
+ if (schema.type === 'notDefined') {
53
+ return value === undefined;
54
+ }
55
+ if (schema.type === 'array') {
56
+ return Array.isArray(value) && value.every((item) => validateInner(schema.items, item, cache));
57
+ }
58
+ if (schema.type === 'record') {
59
+ if (!isObject(value)) {
60
+ return false;
61
+ }
62
+ const keys = Object.keys(value);
63
+ return keys.every((key) => validateInner(schema.key, key, cache) && validateInner(schema.value, value[key], cache));
64
+ }
65
+ if (schema.type === 'object') {
66
+ if (!isObject(value)) {
67
+ return false;
68
+ }
69
+ const schemaKeys = Object.keys(schema.properties);
70
+ return schemaKeys.every((key) => validateInner(schema.properties[key], value[key], cache));
71
+ }
72
+ if (schema.type === 'optional') {
73
+ return value === undefined || validateInner(schema.schema, value, cache);
74
+ }
75
+ if (schema.type === 'union') {
76
+ return schema.schemas.some((branch) => validateInner(branch, value, cache));
77
+ }
78
+ if (schema.type === 'intersection') {
79
+ if (schema.schemas.length === 0) {
80
+ // Vacuous: no constraints (matches `Array.prototype.every` on an empty list).
81
+ return true;
82
+ }
83
+ if (!isObject(value)) {
84
+ return false;
85
+ }
86
+ return schema.schemas.every((subSchema) => validateInner(subSchema, value, cache));
87
+ }
88
+ if (schema.type === 'literal') {
89
+ return value === schema.value;
90
+ }
91
+ if (schema.type === 'lazy') {
92
+ return validateInner(schema.schema(), value, cache);
93
+ }
94
+ if (schema.type === 'evaluate') {
95
+ return validateInner(schema.schema, schema.expression(value), cache);
96
+ }
97
+ // We need to assert here that schema has the type never so we know we handle all cases
98
+ const _exhaustive = schema;
99
+ console.warn('Unknown schema type:', _exhaustive);
100
+ return false;
101
+ }
102
+ finally {
103
+ // Always clear the in-progress marker, even when a sub-call throws. This
104
+ // keeps the cache scoped to the live call stack so sibling branches (for
105
+ // example other `union` members or earlier-failed `intersection` members)
106
+ // re-validate the shared schema instead of inheriting a stale `true`.
107
+ if (trackable) {
108
+ cache.get(value)?.delete(schema);
109
+ }
110
+ }
111
+ };
2
112
  /**
3
113
  * Validates that a given value matches the specified schema.
4
114
  *
@@ -31,78 +141,11 @@ import { isObject } from './helpers/is-object.js';
31
141
  * validate(schema, { id: 1, name: 2 }) // false
32
142
  * ```
33
143
  *
144
+ * The optional `cache` argument tracks visited object–schema pairs to stop
145
+ * infinite recursion on cyclic value graphs (for example a node whose child
146
+ * points back at itself paired with a `lazy` schema). Callers normally omit it.
147
+ *
34
148
  * If schema is `undefined`, validation fails.
35
149
  * Returns true if the value matches the schema, false otherwise.
36
150
  */
37
- export const validate = (schema, value) => {
38
- if (!schema) {
39
- return false;
40
- }
41
- if (schema.type === 'any' || schema.type === 'unknown') {
42
- return true;
43
- }
44
- if (schema.type === 'function') {
45
- return typeof value === 'function';
46
- }
47
- if (schema.type === 'number') {
48
- return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value);
49
- }
50
- if (schema.type === 'string') {
51
- return typeof value === 'string';
52
- }
53
- if (schema.type === 'boolean') {
54
- return typeof value === 'boolean';
55
- }
56
- if (schema.type === 'nullable') {
57
- return value === null;
58
- }
59
- if (schema.type === 'notDefined') {
60
- return value === undefined;
61
- }
62
- if (schema.type === 'array') {
63
- return Array.isArray(value) && value.every((item) => validate(schema.items, item));
64
- }
65
- if (schema.type === 'record') {
66
- if (!isObject(value)) {
67
- return false;
68
- }
69
- const keys = Object.keys(value);
70
- return keys.every((key) => validate(schema.key, key) && validate(schema.value, value[key]));
71
- }
72
- if (schema.type === 'object') {
73
- if (!isObject(value)) {
74
- return false;
75
- }
76
- const schemaKeys = Object.keys(schema.properties);
77
- return schemaKeys.every((key) => validate(schema.properties[key], value[key]));
78
- }
79
- if (schema.type === 'optional') {
80
- return value === undefined || validate(schema.schema, value);
81
- }
82
- if (schema.type === 'union') {
83
- return schema.schemas.some((schema) => validate(schema, value));
84
- }
85
- if (schema.type === 'intersection') {
86
- if (schema.schemas.length === 0) {
87
- // Vacuous: no constraints (matches `Array.prototype.every` on an empty list).
88
- return true;
89
- }
90
- if (!isObject(value)) {
91
- return false;
92
- }
93
- return schema.schemas.every((subSchema) => validate(subSchema, value));
94
- }
95
- if (schema.type === 'literal') {
96
- return value === schema.value;
97
- }
98
- if (schema.type === 'lazy') {
99
- return validate(schema.schema(), value);
100
- }
101
- if (schema.type === 'evaluate') {
102
- return validate(schema.schema, schema.expression(value));
103
- }
104
- // We need to assert here that schema has the type never so we know we handle all cases
105
- const _exhaustive = schema;
106
- console.warn('Unknown schema type:', _exhaustive);
107
- return false;
108
- };
151
+ export const validate = (schema, value, cache = new WeakMap()) => validateInner(schema, value, cache);
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "coerce",
16
16
  "scalar"
17
17
  ],
18
- "version": "0.5.0",
18
+ "version": "0.6.0",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },