@orkestrel/contract 0.0.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/src/core/combinators.d.ts +384 -0
- package/dist/src/core/compilers.d.ts +119 -0
- package/dist/src/core/constants.d.ts +20 -0
- package/dist/src/core/helpers.d.ts +112 -0
- package/dist/src/core/index.d.ts +8 -0
- package/dist/src/core/index.js +2026 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/parsers.d.ts +230 -0
- package/dist/src/core/shapers.d.ts +178 -0
- package/dist/src/core/types.d.ts +372 -0
- package/dist/src/core/validators.d.ts +250 -0
- package/package.json +69 -0
|
@@ -0,0 +1,2026 @@
|
|
|
1
|
+
//#region src/core/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* The seven standard JSON Schema `type` names, frozen.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The runtime source of truth for the {@link JSONSchemaType} vocabulary. Compose
|
|
7
|
+
* it with the shipped primitives instead of reaching for a bespoke guard:
|
|
8
|
+
* `literalOf(...JSON_SCHEMA_TYPES)` is the guard, and
|
|
9
|
+
* `parseEnum(value, JSON_SCHEMA_TYPES)` / `parseEnumField(record, path, JSON_SCHEMA_TYPES)`
|
|
10
|
+
* is the parser.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { JSON_SCHEMA_TYPES, literalOf, parseEnumField } from '@src/core'
|
|
15
|
+
*
|
|
16
|
+
* const isSchemaType = literalOf(...JSON_SCHEMA_TYPES) // Guard<JSONSchemaType>
|
|
17
|
+
* parseEnumField(schema, 'type', JSON_SCHEMA_TYPES) // JSONSchemaType | undefined
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
var JSON_SCHEMA_TYPES = Object.freeze([
|
|
21
|
+
"null",
|
|
22
|
+
"boolean",
|
|
23
|
+
"object",
|
|
24
|
+
"array",
|
|
25
|
+
"number",
|
|
26
|
+
"integer",
|
|
27
|
+
"string"
|
|
28
|
+
]);
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/core/validators.ts
|
|
31
|
+
/** Determine whether a value is `null`. */
|
|
32
|
+
function isNull(value) {
|
|
33
|
+
return value === null;
|
|
34
|
+
}
|
|
35
|
+
/** Determine whether a value is `undefined`. */
|
|
36
|
+
function isUndefined(value) {
|
|
37
|
+
return value === void 0;
|
|
38
|
+
}
|
|
39
|
+
/** Determine whether a value is defined (neither `null` nor `undefined`). */
|
|
40
|
+
function isDefined(value) {
|
|
41
|
+
return value !== null && value !== void 0;
|
|
42
|
+
}
|
|
43
|
+
/** Determine whether a value is a string. */
|
|
44
|
+
function isString(value) {
|
|
45
|
+
return typeof value === "string";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Determine whether a value is a number.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* Includes `NaN` and `±Infinity` — use {@link isFiniteNumber} to exclude them.
|
|
52
|
+
*/
|
|
53
|
+
function isNumber(value) {
|
|
54
|
+
return typeof value === "number";
|
|
55
|
+
}
|
|
56
|
+
/** Determine whether a value is a finite number (excludes `NaN` and `±Infinity`). */
|
|
57
|
+
function isFiniteNumber(value) {
|
|
58
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
59
|
+
}
|
|
60
|
+
/** Determine whether a value is a finite integer (excludes `NaN`, `±Infinity`, and fractional numbers). */
|
|
61
|
+
function isInteger(value) {
|
|
62
|
+
return Number.isInteger(value);
|
|
63
|
+
}
|
|
64
|
+
/** Determine whether a value is a boolean. */
|
|
65
|
+
function isBoolean(value) {
|
|
66
|
+
return typeof value === "boolean";
|
|
67
|
+
}
|
|
68
|
+
/** Determine whether a value is exactly `true`. */
|
|
69
|
+
function isTrue(value) {
|
|
70
|
+
return value === true;
|
|
71
|
+
}
|
|
72
|
+
/** Determine whether a value is exactly `false`. */
|
|
73
|
+
function isFalse(value) {
|
|
74
|
+
return value === false;
|
|
75
|
+
}
|
|
76
|
+
/** Determine whether a value is a bigint. */
|
|
77
|
+
function isBigInt(value) {
|
|
78
|
+
return typeof value === "bigint";
|
|
79
|
+
}
|
|
80
|
+
/** Determine whether a value is a symbol. */
|
|
81
|
+
function isSymbol(value) {
|
|
82
|
+
return typeof value === "symbol";
|
|
83
|
+
}
|
|
84
|
+
/** Determine whether a value is callable. */
|
|
85
|
+
function isFunction(value) {
|
|
86
|
+
return typeof value === "function";
|
|
87
|
+
}
|
|
88
|
+
/** Determine whether a value is a string or `null`. */
|
|
89
|
+
function isNullableString(value) {
|
|
90
|
+
return value === null || isString(value);
|
|
91
|
+
}
|
|
92
|
+
/** Determine whether a value is a number or `null` (the number may be `NaN` / `±Infinity`). */
|
|
93
|
+
function isNullableNumber(value) {
|
|
94
|
+
return value === null || isNumber(value);
|
|
95
|
+
}
|
|
96
|
+
/** Determine whether a value is a boolean or `null`. */
|
|
97
|
+
function isNullableBoolean(value) {
|
|
98
|
+
return value === null || isBoolean(value);
|
|
99
|
+
}
|
|
100
|
+
/** Determine whether a value is a `Date`. */
|
|
101
|
+
function isDate(value) {
|
|
102
|
+
return value instanceof Date;
|
|
103
|
+
}
|
|
104
|
+
/** Determine whether a value is a `RegExp`. */
|
|
105
|
+
function isRegExp(value) {
|
|
106
|
+
return value instanceof RegExp;
|
|
107
|
+
}
|
|
108
|
+
/** Determine whether a value is an `Error`. */
|
|
109
|
+
function isError(value) {
|
|
110
|
+
return value instanceof Error;
|
|
111
|
+
}
|
|
112
|
+
/** Determine whether a value is a native `Promise` (use {@link isPromiseLike} for any thenable). */
|
|
113
|
+
function isPromise(value) {
|
|
114
|
+
return value instanceof Promise;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Determine whether a value is promise-like — an object exposing callable
|
|
118
|
+
* `then`, `catch`, and `finally` methods.
|
|
119
|
+
*
|
|
120
|
+
* @remarks
|
|
121
|
+
* Accepts any object with all three methods, not only native `Promise`
|
|
122
|
+
* instances. Use {@link isPromise} when you specifically need `instanceof Promise`.
|
|
123
|
+
*/
|
|
124
|
+
function isPromiseLike(value) {
|
|
125
|
+
if (!isObject(value)) return false;
|
|
126
|
+
const outcome = attempt(() => {
|
|
127
|
+
const thenValue = Reflect.get(value, "then");
|
|
128
|
+
const catchValue = Reflect.get(value, "catch");
|
|
129
|
+
const finallyValue = Reflect.get(value, "finally");
|
|
130
|
+
return isFunction(thenValue) && isFunction(catchValue) && isFunction(finallyValue);
|
|
131
|
+
});
|
|
132
|
+
return outcome.success && outcome.value;
|
|
133
|
+
}
|
|
134
|
+
/** Determine whether a value is an `ArrayBuffer`. */
|
|
135
|
+
function isArrayBuffer(value) {
|
|
136
|
+
return value instanceof ArrayBuffer;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Determine whether a value is a `SharedArrayBuffer`.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* Guards the global existence of `SharedArrayBuffer` first — safe where it is
|
|
143
|
+
* absent or disabled (e.g. a context that is not cross-origin isolated).
|
|
144
|
+
*/
|
|
145
|
+
function isSharedArrayBuffer(value) {
|
|
146
|
+
return typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Determine whether a value implements the iterable protocol (`Symbol.iterator`).
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* Strings are explicitly included: a string has a callable `Symbol.iterator`
|
|
153
|
+
* but is not an object, so the generic object path alone would miss it.
|
|
154
|
+
*/
|
|
155
|
+
function isIterable(value) {
|
|
156
|
+
if (isString(value)) return true;
|
|
157
|
+
if (!isObject(value)) return false;
|
|
158
|
+
const outcome = attempt(() => isFunction(Reflect.get(value, Symbol.iterator)));
|
|
159
|
+
return outcome.success && outcome.value;
|
|
160
|
+
}
|
|
161
|
+
/** Determine whether a value implements the async iterable protocol (`Symbol.asyncIterator`). */
|
|
162
|
+
function isAsyncIterable(value) {
|
|
163
|
+
if (!isObject(value)) return false;
|
|
164
|
+
const outcome = attempt(() => isFunction(Reflect.get(value, Symbol.asyncIterator)));
|
|
165
|
+
return outcome.success && outcome.value;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Determine whether a value is a non-null object.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* `true` for arrays, class instances, plain objects, `Map`, `Set`, etc. — use
|
|
172
|
+
* {@link isRecord} when you need a plain-record check.
|
|
173
|
+
*/
|
|
174
|
+
function isObject(value) {
|
|
175
|
+
return typeof value === "object" && value !== null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Determine whether a value is a plain record (object literal or null-prototype),
|
|
179
|
+
* not an array or class instance.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* Use instead of {@link isObject} to distinguish a plain `{}` /
|
|
183
|
+
* `Object.create(null)` from arrays, `Date`, `Map`, etc. The prototype-chain
|
|
184
|
+
* test is realm-agnostic: rather than comparing against the current realm's
|
|
185
|
+
* `Object.prototype` (which a plain object from another `vm.Context`, iframe,
|
|
186
|
+
* or worker would fail), it accepts any value whose prototype is `null`, OR
|
|
187
|
+
* whose prototype's own prototype is `null` — the shape every plain object
|
|
188
|
+
* has in every realm, since `Object.prototype` itself always sits one step
|
|
189
|
+
* above `null`. Arrays and class instances are still rejected: an array's
|
|
190
|
+
* prototype chain runs through `Array.prototype` before `null`, and a class
|
|
191
|
+
* instance's runs through the class's own prototype. The whole body runs
|
|
192
|
+
* inside `attempt` (AGENTS §14) so a revoked `Proxy` or a hostile
|
|
193
|
+
* `getPrototypeOf` trap cannot escape as a thrown error.
|
|
194
|
+
*/
|
|
195
|
+
function isRecord(value) {
|
|
196
|
+
const outcome = attempt(() => {
|
|
197
|
+
if (!isObject(value) || isArray(value)) return false;
|
|
198
|
+
const prototype = Object.getPrototypeOf(value);
|
|
199
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
200
|
+
});
|
|
201
|
+
return outcome.success && outcome.value;
|
|
202
|
+
}
|
|
203
|
+
/** Determine whether a value is a `Map`. */
|
|
204
|
+
function isMap(value) {
|
|
205
|
+
return value instanceof Map;
|
|
206
|
+
}
|
|
207
|
+
/** Determine whether a value is a `Set`. */
|
|
208
|
+
function isSet(value) {
|
|
209
|
+
return value instanceof Set;
|
|
210
|
+
}
|
|
211
|
+
/** Determine whether a value is a `WeakMap`. */
|
|
212
|
+
function isWeakMap(value) {
|
|
213
|
+
return value instanceof WeakMap;
|
|
214
|
+
}
|
|
215
|
+
/** Determine whether a value is a `WeakSet`. */
|
|
216
|
+
function isWeakSet(value) {
|
|
217
|
+
return value instanceof WeakSet;
|
|
218
|
+
}
|
|
219
|
+
/** Determine whether a value is an array. */
|
|
220
|
+
function isArray(value) {
|
|
221
|
+
return Array.isArray(value);
|
|
222
|
+
}
|
|
223
|
+
/** Determine whether a value is a `DataView`. */
|
|
224
|
+
function isDataView(value) {
|
|
225
|
+
return value instanceof DataView;
|
|
226
|
+
}
|
|
227
|
+
/** Determine whether a value is an `ArrayBufferView` (any typed array or `DataView`). */
|
|
228
|
+
function isArrayBufferView(value) {
|
|
229
|
+
return ArrayBuffer.isView(value);
|
|
230
|
+
}
|
|
231
|
+
/** Determine whether a value is an `Int8Array`. */
|
|
232
|
+
function isInt8Array(value) {
|
|
233
|
+
return value instanceof Int8Array;
|
|
234
|
+
}
|
|
235
|
+
/** Determine whether a value is a `Uint8Array`. */
|
|
236
|
+
function isUint8Array(value) {
|
|
237
|
+
return value instanceof Uint8Array;
|
|
238
|
+
}
|
|
239
|
+
/** Determine whether a value is a `Uint8ClampedArray`. */
|
|
240
|
+
function isUint8ClampedArray(value) {
|
|
241
|
+
return value instanceof Uint8ClampedArray;
|
|
242
|
+
}
|
|
243
|
+
/** Determine whether a value is an `Int16Array`. */
|
|
244
|
+
function isInt16Array(value) {
|
|
245
|
+
return value instanceof Int16Array;
|
|
246
|
+
}
|
|
247
|
+
/** Determine whether a value is a `Uint16Array`. */
|
|
248
|
+
function isUint16Array(value) {
|
|
249
|
+
return value instanceof Uint16Array;
|
|
250
|
+
}
|
|
251
|
+
/** Determine whether a value is an `Int32Array`. */
|
|
252
|
+
function isInt32Array(value) {
|
|
253
|
+
return value instanceof Int32Array;
|
|
254
|
+
}
|
|
255
|
+
/** Determine whether a value is a `Uint32Array`. */
|
|
256
|
+
function isUint32Array(value) {
|
|
257
|
+
return value instanceof Uint32Array;
|
|
258
|
+
}
|
|
259
|
+
/** Determine whether a value is a `Float32Array`. */
|
|
260
|
+
function isFloat32Array(value) {
|
|
261
|
+
return value instanceof Float32Array;
|
|
262
|
+
}
|
|
263
|
+
/** Determine whether a value is a `Float64Array`. */
|
|
264
|
+
function isFloat64Array(value) {
|
|
265
|
+
return value instanceof Float64Array;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Determine whether a value is a `BigInt64Array`.
|
|
269
|
+
*
|
|
270
|
+
* @remarks
|
|
271
|
+
* Guards the global existence of `BigInt64Array` first — safe in environments
|
|
272
|
+
* that pre-date the BigInt typed-array additions.
|
|
273
|
+
*/
|
|
274
|
+
function isBigInt64Array(value) {
|
|
275
|
+
return typeof BigInt64Array !== "undefined" && value instanceof BigInt64Array;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Determine whether a value is a `BigUint64Array`.
|
|
279
|
+
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* Guards the global existence of `BigUint64Array` first — safe in environments
|
|
282
|
+
* that pre-date the BigInt typed-array additions.
|
|
283
|
+
*/
|
|
284
|
+
function isBigUint64Array(value) {
|
|
285
|
+
return typeof BigUint64Array !== "undefined" && value instanceof BigUint64Array;
|
|
286
|
+
}
|
|
287
|
+
/** Determine whether a value is the empty string `''`. */
|
|
288
|
+
function isEmptyString(value) {
|
|
289
|
+
return isString(value) && value.length === 0;
|
|
290
|
+
}
|
|
291
|
+
/** Determine whether a value is an empty array. */
|
|
292
|
+
function isEmptyArray(value) {
|
|
293
|
+
return isArray(value) && value.length === 0;
|
|
294
|
+
}
|
|
295
|
+
/** Determine whether a value is an empty plain object (no own string or enumerable symbol keys). */
|
|
296
|
+
function isEmptyObject(value) {
|
|
297
|
+
if (!isRecord(value)) return false;
|
|
298
|
+
return Object.keys(value).length === 0 && enumerableSymbolCount(value) === 0;
|
|
299
|
+
}
|
|
300
|
+
/** Determine whether a value is an empty `Map`. */
|
|
301
|
+
function isEmptyMap(value) {
|
|
302
|
+
return value instanceof Map && value.size === 0;
|
|
303
|
+
}
|
|
304
|
+
/** Determine whether a value is an empty `Set`. */
|
|
305
|
+
function isEmptySet(value) {
|
|
306
|
+
return value instanceof Set && value.size === 0;
|
|
307
|
+
}
|
|
308
|
+
/** Determine whether a value is a non-empty string (at least one character). */
|
|
309
|
+
function isNonEmptyString(value) {
|
|
310
|
+
return isString(value) && value.length > 0;
|
|
311
|
+
}
|
|
312
|
+
/** Determine whether a value is a non-empty array (at least one element). */
|
|
313
|
+
function isNonEmptyArray(value) {
|
|
314
|
+
return isArray(value) && value.length > 0;
|
|
315
|
+
}
|
|
316
|
+
/** Determine whether a value is a non-empty plain object (at least one own string or enumerable symbol key). */
|
|
317
|
+
function isNonEmptyObject(value) {
|
|
318
|
+
if (!isRecord(value)) return false;
|
|
319
|
+
return Object.keys(value).length > 0 || enumerableSymbolCount(value) > 0;
|
|
320
|
+
}
|
|
321
|
+
/** Determine whether a value is a non-empty `Map` (at least one entry). */
|
|
322
|
+
function isNonEmptyMap(value) {
|
|
323
|
+
return value instanceof Map && value.size > 0;
|
|
324
|
+
}
|
|
325
|
+
/** Determine whether a value is a non-empty `Set` (at least one element). */
|
|
326
|
+
function isNonEmptySet(value) {
|
|
327
|
+
return value instanceof Set && value.size > 0;
|
|
328
|
+
}
|
|
329
|
+
/** Determine whether a value is a function that declares zero parameters (`Function.length === 0`). */
|
|
330
|
+
function isZeroArg(value) {
|
|
331
|
+
return isFunction(value) && value.length === 0;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Determine whether a value is a native `async function`.
|
|
335
|
+
*
|
|
336
|
+
* @remarks
|
|
337
|
+
* Uses `constructor.name === 'AsyncFunction'` — not `instanceof`, which is
|
|
338
|
+
* unreliable across realms. The `?.` keeps the guard total (§14): a function
|
|
339
|
+
* whose `constructor` was nulled yields `undefined`, never a thrown `null.name`.
|
|
340
|
+
*/
|
|
341
|
+
function isAsyncFunction(value) {
|
|
342
|
+
return isFunction(value) && value.constructor?.name === "AsyncFunction";
|
|
343
|
+
}
|
|
344
|
+
/** Determine whether a value is a generator function (`function*`). */
|
|
345
|
+
function isGeneratorFunction(value) {
|
|
346
|
+
return isFunction(value) && value.constructor?.name === "GeneratorFunction";
|
|
347
|
+
}
|
|
348
|
+
/** Determine whether a value is an async generator function (`async function*`). */
|
|
349
|
+
function isAsyncGeneratorFunction(value) {
|
|
350
|
+
return isFunction(value) && value.constructor?.name === "AsyncGeneratorFunction";
|
|
351
|
+
}
|
|
352
|
+
/** Determine whether a value is a zero-argument async function. */
|
|
353
|
+
function isZeroArgAsync(value) {
|
|
354
|
+
return isZeroArg(value) && isAsyncFunction(value);
|
|
355
|
+
}
|
|
356
|
+
/** Determine whether a value is a zero-argument generator function. */
|
|
357
|
+
function isZeroArgGenerator(value) {
|
|
358
|
+
return isZeroArg(value) && isGeneratorFunction(value);
|
|
359
|
+
}
|
|
360
|
+
/** Determine whether a value is a zero-argument async generator function. */
|
|
361
|
+
function isZeroArgAsyncGenerator(value) {
|
|
362
|
+
return isZeroArg(value) && isAsyncGeneratorFunction(value);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Determine whether a value can be used as a `new`-target constructor.
|
|
366
|
+
*
|
|
367
|
+
* @remarks
|
|
368
|
+
* Probes with `Reflect.construct(String, [], value)`: a real constructor
|
|
369
|
+
* succeeds, while arrow functions, plain functions, and non-functions throw
|
|
370
|
+
* and yield `false`. Never throws. Backs the `instanceOf` combinator.
|
|
371
|
+
*/
|
|
372
|
+
function isConstructor(value) {
|
|
373
|
+
if (!isFunction(value)) return false;
|
|
374
|
+
try {
|
|
375
|
+
Reflect.construct(String, [], value);
|
|
376
|
+
return true;
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Determine whether a value is a cycle-safe JSON value.
|
|
383
|
+
*
|
|
384
|
+
* @remarks
|
|
385
|
+
* Total guard: never throws, returns `false` for cycles, functions, `Date`
|
|
386
|
+
* instances, class instances, `NaN`, and `±Infinity`. Arrays and plain records
|
|
387
|
+
* are walked with an ancestor set so recursive input fails instead of hanging.
|
|
388
|
+
* The whole walk runs inside `attempt` (AGENTS §14): a hostile getter on a
|
|
389
|
+
* record property, or a revoked `Proxy` anywhere in the structure, is caught
|
|
390
|
+
* and yields `false` instead of escaping as a thrown error.
|
|
391
|
+
*
|
|
392
|
+
* @param value - The value to test
|
|
393
|
+
* @returns `true` when the value has a JSON representation
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* isJSONValue({ nested: [1, 'x', null] }) // true
|
|
398
|
+
* isJSONValue(Number.NaN) // false
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
function isJSONValue(value) {
|
|
402
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
403
|
+
const check = (entry) => {
|
|
404
|
+
if (entry === null || isString(entry) || isBoolean(entry) || isFiniteNumber(entry)) return true;
|
|
405
|
+
if (Array.isArray(entry)) {
|
|
406
|
+
if (ancestors.has(entry)) return false;
|
|
407
|
+
ancestors.add(entry);
|
|
408
|
+
const valid = entry.every(check);
|
|
409
|
+
ancestors.delete(entry);
|
|
410
|
+
return valid;
|
|
411
|
+
}
|
|
412
|
+
if (!isRecord(entry)) return false;
|
|
413
|
+
if (ancestors.has(entry)) return false;
|
|
414
|
+
ancestors.add(entry);
|
|
415
|
+
const valid = Object.values(entry).every(check);
|
|
416
|
+
ancestors.delete(entry);
|
|
417
|
+
return valid;
|
|
418
|
+
};
|
|
419
|
+
const outcome = attempt(() => check(value));
|
|
420
|
+
return outcome.success && outcome.value;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Determine whether a value is a primitive JSON value.
|
|
424
|
+
*
|
|
425
|
+
* @remarks
|
|
426
|
+
* The flat leaf of any JSON document: `null`, a string, a **finite** number, or
|
|
427
|
+
* a boolean. Uses {@link isFiniteNumber} (not {@link isNumber}) because real JSON
|
|
428
|
+
* carries no `NaN` / `±Infinity` — `JSON.stringify(NaN)` is `'null'`.
|
|
429
|
+
*
|
|
430
|
+
* The recursive {@link isJSONValue} guard is shipped and stays total with
|
|
431
|
+
* cycle-safe walking. Dedicated `isJSONObject` / `isJSONSchema` validators and
|
|
432
|
+
* the broad `JSONSchemaDefinition` remain omitted; compose narrower shapes with
|
|
433
|
+
* the combinators and gate untrusted strings with `parseJSON` / `parseJSONAs`.
|
|
434
|
+
*
|
|
435
|
+
* @param value - The value to test
|
|
436
|
+
* @returns `true` when `value` is `null`, a string, a finite number, or a boolean
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```ts
|
|
440
|
+
* isJSONPrimitive(null) // true
|
|
441
|
+
* isJSONPrimitive('hi') // true
|
|
442
|
+
* isJSONPrimitive(42) // true
|
|
443
|
+
* isJSONPrimitive(Number.NaN) // false — not representable in JSON
|
|
444
|
+
* isJSONPrimitive({}) // false
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
function isJSONPrimitive(value) {
|
|
448
|
+
return isNull(value) || isString(value) || isFiniteNumber(value) || isBoolean(value);
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/core/helpers.ts
|
|
452
|
+
/**
|
|
453
|
+
* Invoke a callback and capture its outcome as a {@link Result}, never letting
|
|
454
|
+
* a throw escape.
|
|
455
|
+
*
|
|
456
|
+
* @remarks
|
|
457
|
+
* The single sanctioned never-throw boundary for the guards (AGENTS §14). The
|
|
458
|
+
* `whereOf`, `lazyOf`, and `transformOf` combinators invoke caller-supplied
|
|
459
|
+
* callbacks *inside* a guard body, yet a guard must NEVER throw — it returns a
|
|
460
|
+
* `boolean`. This converts a throwing callback into a `Failure` so the
|
|
461
|
+
* surrounding guard can treat it as a non-match instead of propagating the
|
|
462
|
+
* exception, written once and shared rather than copy-pasted as ad-hoc
|
|
463
|
+
* `try`/`catch`.
|
|
464
|
+
*
|
|
465
|
+
* @param callback - The callback to invoke with no arguments
|
|
466
|
+
* @returns A `Success` carrying the return value, or a `Failure` carrying the
|
|
467
|
+
* thrown reason normalised to an `Error`
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```ts
|
|
471
|
+
* const outcome = attempt(() => predicate(value))
|
|
472
|
+
* return outcome.success && outcome.value
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
function attempt(callback) {
|
|
476
|
+
try {
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
value: callback()
|
|
480
|
+
};
|
|
481
|
+
} catch (reason) {
|
|
482
|
+
if (reason instanceof Error) return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: reason
|
|
485
|
+
};
|
|
486
|
+
let message = "Unknown thrown value";
|
|
487
|
+
try {
|
|
488
|
+
message = String(reason);
|
|
489
|
+
} catch {}
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
error: new Error(message)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Resolve a (possibly nested) field value from a record by a key or key path.
|
|
498
|
+
*
|
|
499
|
+
* @remarks
|
|
500
|
+
* A single `string` is ONE key (never split on `.`, so dotted keys are safe); a
|
|
501
|
+
* string array descends left-to-right through nested objects. Intermediates may
|
|
502
|
+
* be any object — records, class instances, or arrays indexed by string. Returns
|
|
503
|
+
* `undefined` the moment a segment is missing or lands on a non-object, so the
|
|
504
|
+
* lookup is total — even against a hostile getter or Proxy trap that throws on
|
|
505
|
+
* read, contained via {@link attempt} so the throw never escapes.
|
|
506
|
+
*
|
|
507
|
+
* @param record - The source record
|
|
508
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
509
|
+
* @returns The resolved value, or `undefined`
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```ts
|
|
513
|
+
* resolveField({ user: { name: 'Ada' } }, ['user', 'name']) // 'Ada'
|
|
514
|
+
* resolveField({ 'a.b': 1 }, 'a.b') // 1 (one key)
|
|
515
|
+
* resolveField({ a: 1 }, ['a', 'b']) // undefined
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
function resolveField(record, path) {
|
|
519
|
+
const keys = isString(path) ? [path] : path;
|
|
520
|
+
let current = record;
|
|
521
|
+
for (const key of keys) {
|
|
522
|
+
if (!isObject(current)) return void 0;
|
|
523
|
+
const container = current;
|
|
524
|
+
const outcome = attempt(() => Reflect.get(container, key));
|
|
525
|
+
if (!outcome.success) return void 0;
|
|
526
|
+
current = outcome.value;
|
|
527
|
+
}
|
|
528
|
+
return current;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Build a deterministic pseudo-random source seeded from a single number.
|
|
532
|
+
*
|
|
533
|
+
* @remarks
|
|
534
|
+
* A mulberry32 generator — the same seed always yields the same sequence, so
|
|
535
|
+
* generated seed data is reproducible across runs. Used as the default random
|
|
536
|
+
* source for {@link compileGenerator}, seeded from the wall clock so casual
|
|
537
|
+
* callers still get varied output without passing a source themselves.
|
|
538
|
+
*
|
|
539
|
+
* @param seed - The seed for the sequence
|
|
540
|
+
* @returns A {@link RandomFunction} returning values in `[0, 1)`
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* const random = seededRandom(42)
|
|
545
|
+
* random() // always the same first value for seed 42
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
function seededRandom(seed) {
|
|
549
|
+
let state = seed >>> 0;
|
|
550
|
+
return () => {
|
|
551
|
+
state = state + 1831565813 >>> 0;
|
|
552
|
+
let t = state;
|
|
553
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
554
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
555
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Count the enumerable own-symbol keys on a value.
|
|
560
|
+
*
|
|
561
|
+
* @remarks
|
|
562
|
+
* String keys are ignored — only `Object.getOwnPropertySymbols` entries whose
|
|
563
|
+
* descriptor is `enumerable` are counted. Backs the object-emptiness guards
|
|
564
|
+
* (`isEmptyObject` / `isNonEmptyObject`) so a record keyed only by an
|
|
565
|
+
* enumerable symbol is not mistaken for empty.
|
|
566
|
+
*
|
|
567
|
+
* @param value - The object to inspect
|
|
568
|
+
* @returns The number of enumerable own-symbol keys
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```ts
|
|
572
|
+
* const flag = Symbol('flag')
|
|
573
|
+
* enumerableSymbolCount(Object.defineProperty({}, flag, { value: 1, enumerable: true })) // 1
|
|
574
|
+
* enumerableSymbolCount({}) // 0
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
function enumerableSymbolCount(value) {
|
|
578
|
+
let count = 0;
|
|
579
|
+
for (const symbol of Object.getOwnPropertySymbols(value)) if (Object.getOwnPropertyDescriptor(value, symbol)?.enumerable) count += 1;
|
|
580
|
+
return count;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Narrow a compiled {@link JSONSchema} down to the open `Readonly<Record<string, unknown>>` shape
|
|
584
|
+
* tool definitions advertise as `parameters` — through the {@link isRecord} boundary guard, never
|
|
585
|
+
* an assertion (AGENTS §14).
|
|
586
|
+
*
|
|
587
|
+
* @remarks
|
|
588
|
+
* A `JSONSchema` is the closed contract-compiler fragment (it has no index signature), whereas a
|
|
589
|
+
* tool advertises its `parameters` as an open record. The two are structurally compatible but not
|
|
590
|
+
* assignable, so the schema crosses that boundary through `isRecord` — a compiled contract schema
|
|
591
|
+
* is always a record, so the guard passes; the `undefined` fallback only satisfies the type's
|
|
592
|
+
* optionality. This is the single sanctioned narrowing from a compiled contract schema to the open
|
|
593
|
+
* tool-parameters record, so the crossing lives once rather than being copy-pasted per call site.
|
|
594
|
+
*
|
|
595
|
+
* @param schema - The compiled JSON Schema (a contract's `schema`)
|
|
596
|
+
* @returns The schema as the open tool-parameters record, or `undefined` when it is not a record
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* import { createContract, schemaToParameters } from '@src/core'
|
|
601
|
+
*
|
|
602
|
+
* const contract = createContract(shape)
|
|
603
|
+
* const parameters = schemaToParameters(contract.schema) // the open record a tool advertises
|
|
604
|
+
* ```
|
|
605
|
+
*/
|
|
606
|
+
function schemaToParameters(schema) {
|
|
607
|
+
return isRecord(schema) ? schema : void 0;
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/core/combinators.ts
|
|
611
|
+
function arrayOf(elementGuard) {
|
|
612
|
+
return (value) => {
|
|
613
|
+
if (!isArray(value)) return false;
|
|
614
|
+
const outcome = attempt(() => value.every(elementGuard));
|
|
615
|
+
return outcome.success && outcome.value;
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function tupleOf(...guards) {
|
|
619
|
+
return (value) => {
|
|
620
|
+
if (!isArray(value)) return false;
|
|
621
|
+
const outcome = attempt(() => {
|
|
622
|
+
if (value.length !== guards.length) return false;
|
|
623
|
+
for (let index = 0; index < guards.length; index += 1) {
|
|
624
|
+
const guard = guards[index];
|
|
625
|
+
if (!guard?.(value[index])) return false;
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
});
|
|
629
|
+
return outcome.success && outcome.value;
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Build a guard that accepts values identical (via `Object.is`) to one of the
|
|
634
|
+
* provided literal primitives.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```ts
|
|
638
|
+
* const isRole = literalOf('admin', 'member', 'guest')
|
|
639
|
+
* isRole('admin') // true
|
|
640
|
+
* isRole('owner') // false
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
function literalOf(...literals) {
|
|
644
|
+
return (value) => literals.some((literal) => Object.is(literal, value));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Build a guard that accepts instances of the provided constructor.
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* Verifies that `ctor` is a real constructor (via {@link isConstructor}) first,
|
|
651
|
+
* so passing an arrow function does not silently produce a broken guard.
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* const isDateValue = instanceOf(Date)
|
|
656
|
+
* isDateValue(new Date()) // true
|
|
657
|
+
* isDateValue({}) // false
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
function instanceOf(ctor) {
|
|
661
|
+
return (value) => isConstructor(ctor) && isObject(value) && value instanceof ctor;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Build a guard from a native `enum` or any object whose values are strings or
|
|
665
|
+
* numbers.
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```ts
|
|
669
|
+
* enum Direction { Up = 'up', Down = 'down' }
|
|
670
|
+
* const isDirection = enumOf(Direction)
|
|
671
|
+
* isDirection('up') // true
|
|
672
|
+
* isDirection('left') // false
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
function enumOf(enumeration) {
|
|
676
|
+
const values = new Set(Object.values(enumeration));
|
|
677
|
+
return (value) => (isString(value) || isNumber(value)) && values.has(value);
|
|
678
|
+
}
|
|
679
|
+
function setOf(elementGuard) {
|
|
680
|
+
return (value) => {
|
|
681
|
+
if (!isSet(value)) return false;
|
|
682
|
+
const outcome = attempt(() => {
|
|
683
|
+
for (const entry of value) if (!elementGuard(entry)) return false;
|
|
684
|
+
return true;
|
|
685
|
+
});
|
|
686
|
+
return outcome.success && outcome.value;
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function mapOf(keyGuard, valueGuard) {
|
|
690
|
+
return (value) => {
|
|
691
|
+
if (!isMap(value)) return false;
|
|
692
|
+
const outcome = attempt(() => {
|
|
693
|
+
for (const [key, entryValue] of value) if (!keyGuard(key) || !valueGuard(entryValue)) return false;
|
|
694
|
+
return true;
|
|
695
|
+
});
|
|
696
|
+
return outcome.success && outcome.value;
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Build a guard that accepts plain records matching a guard shape.
|
|
701
|
+
*
|
|
702
|
+
* @remarks
|
|
703
|
+
* Three calling modes depending on the `optional` argument:
|
|
704
|
+
* - **No `optional`** — all shape keys required; extra keys rejected.
|
|
705
|
+
* - **`optional: K[]`** — the listed keys are optional; all others required.
|
|
706
|
+
* - **`optional: true`** — every shape key is optional.
|
|
707
|
+
*
|
|
708
|
+
* Key presence is tested with `Object.hasOwn`, so a shape key satisfied only by
|
|
709
|
+
* an inherited prototype member (`toString`, `constructor`, …) counts as absent.
|
|
710
|
+
* A non-object / `null` / array input returns `false` rather than throwing. The
|
|
711
|
+
* extra-key check only inspects `Object.keys` (string keys), so an extra
|
|
712
|
+
* enumerable SYMBOL key is never rejected — intentional, for JSON fidelity, and
|
|
713
|
+
* matches the compiled guard.
|
|
714
|
+
*
|
|
715
|
+
* @example
|
|
716
|
+
* ```ts
|
|
717
|
+
* const isUser = recordOf({ name: isString, age: isNumber })
|
|
718
|
+
* isUser({ name: 'Ada', age: 36 }) // true
|
|
719
|
+
* isUser({ name: 'Ada' }) // false — age missing
|
|
720
|
+
*
|
|
721
|
+
* const isPartial = recordOf({ name: isString, age: isNumber }, ['age'])
|
|
722
|
+
* isPartial({ name: 'Ada' }) // true
|
|
723
|
+
* ```
|
|
724
|
+
*/
|
|
725
|
+
function recordOf(shape, optional) {
|
|
726
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
727
|
+
for (const key in shape) if (Object.prototype.hasOwnProperty.call(shape, key)) allowed.add(key);
|
|
728
|
+
const optionalSet = new Set(optional === true ? [...allowed] : isArray(optional) ? optional.map((key) => String(key)) : []);
|
|
729
|
+
return (value) => {
|
|
730
|
+
if (!isRecord(value)) return false;
|
|
731
|
+
const outcome = attempt(() => {
|
|
732
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
|
|
733
|
+
for (const key in shape) {
|
|
734
|
+
if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
735
|
+
const present = Object.hasOwn(value, key);
|
|
736
|
+
if (!optionalSet.has(key) && !present) return false;
|
|
737
|
+
if (present) {
|
|
738
|
+
const guard = shape[key];
|
|
739
|
+
if (!guard(value[key])) return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
});
|
|
744
|
+
return outcome.success && outcome.value;
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function iterableOf(elementGuard) {
|
|
748
|
+
return (value) => {
|
|
749
|
+
if (!isIterable(value)) return false;
|
|
750
|
+
const outcome = attempt(() => {
|
|
751
|
+
for (const entry of value) if (!elementGuard(entry)) return false;
|
|
752
|
+
return true;
|
|
753
|
+
});
|
|
754
|
+
return outcome.success && outcome.value;
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Build a guard that accepts values that are own keys of the provided object.
|
|
759
|
+
*
|
|
760
|
+
* @remarks
|
|
761
|
+
* Membership is tested with `Object.hasOwn`, so inherited prototype-chain keys
|
|
762
|
+
* (`toString`, `constructor`, …) are rejected. An own property that shadows a
|
|
763
|
+
* prototype name is accepted.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* const COLORS = { red: '#f00', green: '#0f0', blue: '#00f' } as const
|
|
768
|
+
* const isColorKey = keyOf(COLORS)
|
|
769
|
+
* isColorKey('red') // true
|
|
770
|
+
* isColorKey('purple') // false
|
|
771
|
+
* isColorKey('toString') // false — inherited, not an own key
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
774
|
+
function keyOf(value) {
|
|
775
|
+
return (entry) => (isString(entry) || isSymbol(entry) || isNumber(entry)) && Object.hasOwn(value, entry);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Build a new guard shape by keeping only the listed keys — the structural
|
|
779
|
+
* equivalent of `Pick<T, K>`. Produces a shape for {@link recordOf}, not a guard.
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```ts
|
|
783
|
+
* const full = { name: isString, age: isNumber, role: isString }
|
|
784
|
+
* const isName = recordOf(pickOf(full, ['name']))
|
|
785
|
+
* isName({ name: 'Ada' }) // true
|
|
786
|
+
* ```
|
|
787
|
+
*/
|
|
788
|
+
function pickOf(shape, keys) {
|
|
789
|
+
const result = Object.create(null);
|
|
790
|
+
for (const key of keys) if (Object.prototype.hasOwnProperty.call(shape, key)) result[key] = shape[key];
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Build a new guard shape by removing the listed keys — the structural
|
|
795
|
+
* equivalent of `Omit<T, K>`. Produces a shape for {@link recordOf}, not a guard.
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* ```ts
|
|
799
|
+
* const full = { name: isString, age: isNumber, role: isString }
|
|
800
|
+
* const isPublic = recordOf(omitOf(full, ['role']))
|
|
801
|
+
* isPublic({ name: 'Ada', age: 36 }) // true
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
function omitOf(shape, keys) {
|
|
805
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const key of keys) skipped.add(key);
|
|
807
|
+
const result = Object.create(null);
|
|
808
|
+
for (const key in shape) {
|
|
809
|
+
if (!Object.prototype.hasOwnProperty.call(shape, key)) continue;
|
|
810
|
+
if (!skipped.has(key)) result[key] = shape[key];
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
}
|
|
814
|
+
function andOf(left, right) {
|
|
815
|
+
return (value) => left(value) && right(value);
|
|
816
|
+
}
|
|
817
|
+
function orOf(left, right) {
|
|
818
|
+
return (value) => left(value) || right(value);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Negate a guard or predicate — passes when `guard` returns `false`.
|
|
822
|
+
*
|
|
823
|
+
* @remarks
|
|
824
|
+
* Typed as `Guard<unknown>` because `Exclude<unknown, T>` is not useful; use
|
|
825
|
+
* {@link complementOf} when you need the narrowed `Exclude<TBase, TExcluded>`.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```ts
|
|
829
|
+
* const isNotNull = notOf(isNull)
|
|
830
|
+
* ```
|
|
831
|
+
*/
|
|
832
|
+
function notOf(guard) {
|
|
833
|
+
return (value) => !guard(value);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Build a guard for `Exclude<TBase, TExcluded>` — accepts values that pass
|
|
837
|
+
* `base` but not `excluded`.
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```ts
|
|
841
|
+
* const isNonEmpty = complementOf(isString, isEmptyString)
|
|
842
|
+
* isNonEmpty('hi') // true
|
|
843
|
+
* isNonEmpty('') // false
|
|
844
|
+
* ```
|
|
845
|
+
*/
|
|
846
|
+
function complementOf(base, excluded) {
|
|
847
|
+
return (value) => {
|
|
848
|
+
if (!base(value)) return false;
|
|
849
|
+
return !excluded(value);
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function unionOf(...guards) {
|
|
853
|
+
return (value) => guards.some((guard) => guard(value));
|
|
854
|
+
}
|
|
855
|
+
function intersectionOf(...guards) {
|
|
856
|
+
return (value) => guards.every((guard) => guard(value));
|
|
857
|
+
}
|
|
858
|
+
function whereOf(base, predicate) {
|
|
859
|
+
return (value) => {
|
|
860
|
+
if (!base(value)) return false;
|
|
861
|
+
const outcome = attempt(() => predicate(value));
|
|
862
|
+
return outcome.success && outcome.value;
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Defer guard creation until first use by calling `thunk()` on every
|
|
867
|
+
* invocation.
|
|
868
|
+
*
|
|
869
|
+
* @remarks
|
|
870
|
+
* `thunk` is called on every guard call, not cached — this lets it close over a
|
|
871
|
+
* binding assigned *after* `lazyOf` is called, the primary use case for
|
|
872
|
+
* self-referential recursive guards. Per §14 a throw from `thunk` (or the guard
|
|
873
|
+
* it resolves to) is contained and reported as a non-match.
|
|
874
|
+
*
|
|
875
|
+
* A recursive guard built this way has no cycle/depth detection: a cyclic or
|
|
876
|
+
* pathologically deep input is stack-bounded — the overflow is contained and the
|
|
877
|
+
* guard returns `false` rather than throwing, but it is not validated correctly
|
|
878
|
+
* past that bound.
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```ts
|
|
882
|
+
* type Tree = { value: number; children: Tree[] }
|
|
883
|
+
* let isTree: Guard<Tree>
|
|
884
|
+
* isTree = recordOf({ value: isNumber, children: arrayOf(lazyOf(() => isTree)) })
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
function lazyOf(thunk) {
|
|
888
|
+
return (value) => {
|
|
889
|
+
const outcome = attempt(() => thunk()(value));
|
|
890
|
+
return outcome.success && outcome.value;
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function transformOf(base, project, target) {
|
|
894
|
+
return (value) => {
|
|
895
|
+
if (!base(value)) return false;
|
|
896
|
+
const outcome = attempt(() => project(value));
|
|
897
|
+
return outcome.success && target(outcome.value);
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Build a guard that accepts finite numbers within an inclusive `[min, max]`
|
|
902
|
+
* range.
|
|
903
|
+
*
|
|
904
|
+
* @remarks
|
|
905
|
+
* Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
|
|
906
|
+
* `±Infinity` are rejected before any comparison runs. An absent bound never
|
|
907
|
+
* constrains that side. Reused for a number's own value AND, applied to a
|
|
908
|
+
* `.length`, for string and array length refinements — the single source of the
|
|
909
|
+
* bound logic shared by the compiled guard and parser (compilers.ts).
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```ts
|
|
913
|
+
* const inRange = boundsOf(1, 5)
|
|
914
|
+
* inRange(3) // true
|
|
915
|
+
* inRange(0) // false — below min
|
|
916
|
+
* inRange(6) // false — above max
|
|
917
|
+
*
|
|
918
|
+
* const atLeastTwo = boundsOf(2)
|
|
919
|
+
* atLeastTwo(2) // true — unbounded above
|
|
920
|
+
* ```
|
|
921
|
+
*/
|
|
922
|
+
function boundsOf(min, max) {
|
|
923
|
+
return whereOf(isFiniteNumber, (value) => (min === void 0 || value >= min) && (max === void 0 || value <= max));
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Build a guard that accepts strings matching a regular expression.
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```ts
|
|
930
|
+
* const isHex = matchOf(/^[0-9a-f]+$/)
|
|
931
|
+
* isHex('1a2f') // true
|
|
932
|
+
* isHex('xyz') // false
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
function matchOf(pattern) {
|
|
936
|
+
return whereOf(isString, (value) => pattern.test(value));
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Build a guard that accepts strings satisfying optional length and pattern
|
|
940
|
+
* refinements — `min` / `max` length and a `pattern`.
|
|
941
|
+
*
|
|
942
|
+
* @remarks
|
|
943
|
+
* Composes {@link isString} with {@link boundsOf} on the string's `.length` and
|
|
944
|
+
* an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
|
|
945
|
+
* the bare {@link isString} guard (the unconstrained fast path), so an
|
|
946
|
+
* unrefined string leaf pays no wrapping cost. The single source of the string
|
|
947
|
+
* refinement shared by the compiled guard and parser (compilers.ts).
|
|
948
|
+
*
|
|
949
|
+
* @example
|
|
950
|
+
* ```ts
|
|
951
|
+
* const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
|
|
952
|
+
* isSlug('hello-world') // true
|
|
953
|
+
* isSlug('') // false — below min
|
|
954
|
+
* isSlug('Hello') // false — pattern miss
|
|
955
|
+
*
|
|
956
|
+
* stringOf() // identical to isString
|
|
957
|
+
* ```
|
|
958
|
+
*/
|
|
959
|
+
function stringOf(options) {
|
|
960
|
+
const min = options?.min;
|
|
961
|
+
const max = options?.max;
|
|
962
|
+
const pattern = options?.pattern;
|
|
963
|
+
if (min === void 0 && max === void 0 && pattern === void 0) return isString;
|
|
964
|
+
const withinLength = boundsOf(min, max);
|
|
965
|
+
return whereOf(isString, (value) => withinLength(value.length) && (pattern === void 0 || pattern.test(value)));
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Extend a guard to also allow `null`.
|
|
969
|
+
*
|
|
970
|
+
* @example
|
|
971
|
+
* ```ts
|
|
972
|
+
* const isNullableString = nullableOf(isString)
|
|
973
|
+
* isNullableString('hi') // true
|
|
974
|
+
* isNullableString(null) // true
|
|
975
|
+
* isNullableString(42) // false
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
978
|
+
function nullableOf(guard) {
|
|
979
|
+
return (value) => value === null || guard(value);
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Extend a guard to also allow `undefined` — the optional counterpart of
|
|
983
|
+
* {@link nullableOf}.
|
|
984
|
+
*
|
|
985
|
+
* @example
|
|
986
|
+
* ```ts
|
|
987
|
+
* const isOptionalString = optionalOf(isString)
|
|
988
|
+
* isOptionalString('hi') // true
|
|
989
|
+
* isOptionalString(undefined) // true
|
|
990
|
+
* isOptionalString(null) // false
|
|
991
|
+
* ```
|
|
992
|
+
*/
|
|
993
|
+
function optionalOf(guard) {
|
|
994
|
+
return (value) => value === void 0 || guard(value);
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/core/parsers.ts
|
|
998
|
+
/**
|
|
999
|
+
* Parse an unknown value to a string.
|
|
1000
|
+
*
|
|
1001
|
+
* @remarks
|
|
1002
|
+
* A string is returned unchanged; a finite number is coerced to its decimal
|
|
1003
|
+
* string (`42` → `'42'`). `NaN`, `±Infinity`, and every other type → `undefined`.
|
|
1004
|
+
*
|
|
1005
|
+
* @param value - The value to parse
|
|
1006
|
+
* @returns A string, or `undefined`
|
|
1007
|
+
*/
|
|
1008
|
+
function parseString(value) {
|
|
1009
|
+
if (isString(value)) return value;
|
|
1010
|
+
if (isFiniteNumber(value)) return String(value);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Parse an unknown value to a finite number.
|
|
1014
|
+
*
|
|
1015
|
+
* @remarks
|
|
1016
|
+
* A finite number is returned unchanged; a non-blank numeric string is parsed
|
|
1017
|
+
* via `Number(...)`. `NaN`, `±Infinity`, blank/non-numeric strings, and every
|
|
1018
|
+
* other type → `undefined`.
|
|
1019
|
+
*
|
|
1020
|
+
* @param value - The value to parse
|
|
1021
|
+
* @returns A finite number, or `undefined`
|
|
1022
|
+
*/
|
|
1023
|
+
function parseNumber(value) {
|
|
1024
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1025
|
+
if (isString(value)) {
|
|
1026
|
+
if (value.trim() === "") return void 0;
|
|
1027
|
+
const parsed = Number(value);
|
|
1028
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Parse an unknown value to a finite integer.
|
|
1033
|
+
*
|
|
1034
|
+
* @remarks
|
|
1035
|
+
* Accepts whatever {@link parseNumber} accepts, then requires the result to have
|
|
1036
|
+
* no fractional part. `3.14` / `'3.14'` → `undefined`.
|
|
1037
|
+
*
|
|
1038
|
+
* @param value - The value to parse
|
|
1039
|
+
* @returns A finite integer, or `undefined`
|
|
1040
|
+
*/
|
|
1041
|
+
function parseInteger(value) {
|
|
1042
|
+
const parsed = parseNumber(value);
|
|
1043
|
+
if (parsed === void 0) return void 0;
|
|
1044
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Parse an unknown value to a boolean.
|
|
1048
|
+
*
|
|
1049
|
+
* @remarks
|
|
1050
|
+
* A boolean is returned unchanged. The strings `'true'` / `'false'` / `'1'` /
|
|
1051
|
+
* `'0'` and the numbers `1` / `0` coerce to the matching boolean. Everything
|
|
1052
|
+
* else → `undefined`.
|
|
1053
|
+
*
|
|
1054
|
+
* @param value - The value to parse
|
|
1055
|
+
* @returns A boolean, or `undefined`
|
|
1056
|
+
*/
|
|
1057
|
+
function parseBoolean(value) {
|
|
1058
|
+
if (typeof value === "boolean") return value;
|
|
1059
|
+
if (value === "true" || value === "1" || value === 1) return true;
|
|
1060
|
+
if (value === "false" || value === "0" || value === 0) return false;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Parse an unknown value to `null`.
|
|
1064
|
+
*
|
|
1065
|
+
* @remarks
|
|
1066
|
+
* A successful parse returns `null` itself — distinct from the `undefined`
|
|
1067
|
+
* failure sentinel every other parser in this file uses. Only `null` passes;
|
|
1068
|
+
* every other value (including `undefined`) → `undefined`.
|
|
1069
|
+
*
|
|
1070
|
+
* @param value - The value to parse
|
|
1071
|
+
* @returns `null` on a successful parse, or `undefined`
|
|
1072
|
+
*/
|
|
1073
|
+
function parseNull(value) {
|
|
1074
|
+
return isNull(value) ? value : void 0;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Parse an unknown value to a plain record — the input reference, never cloned.
|
|
1078
|
+
*
|
|
1079
|
+
* @param value - The value to parse
|
|
1080
|
+
* @returns The record, or `undefined`
|
|
1081
|
+
*/
|
|
1082
|
+
function parseRecord(value) {
|
|
1083
|
+
return isRecord(value) ? value : void 0;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Parse an unknown value to an array — the input reference, never cloned —
|
|
1087
|
+
* optionally guarding every element.
|
|
1088
|
+
*
|
|
1089
|
+
* @remarks
|
|
1090
|
+
* Without a `guard`, element types are NOT verified; let `T` default to
|
|
1091
|
+
* `unknown` rather than asserting a specific element type.
|
|
1092
|
+
*
|
|
1093
|
+
* @param value - The value to parse
|
|
1094
|
+
* @param guard - Optional element guard
|
|
1095
|
+
* @returns The array, or `undefined`
|
|
1096
|
+
*/
|
|
1097
|
+
function parseArray(value, guard) {
|
|
1098
|
+
if (!isArray(value)) return void 0;
|
|
1099
|
+
if (guard !== void 0 && !value.every(guard)) return void 0;
|
|
1100
|
+
return value;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Parse an unknown value to a cycle-safe JSON value — the input reference,
|
|
1104
|
+
* never cloned.
|
|
1105
|
+
*
|
|
1106
|
+
* @remarks
|
|
1107
|
+
* Unlike {@link parseRecord} / {@link parseArray}, this is a DEEP gate: it
|
|
1108
|
+
* walks the entire tree via {@link isJSONValue} rather than checking only the
|
|
1109
|
+
* top-level shape. That walk is cycle-safe and total (never throws) because
|
|
1110
|
+
* `isJSONValue` runs its own probe inside a guard, so an adversarial
|
|
1111
|
+
* structure (a cycle, a hostile getter) yields `undefined` instead of hanging
|
|
1112
|
+
* or throwing.
|
|
1113
|
+
*
|
|
1114
|
+
* @param value - The value to parse
|
|
1115
|
+
* @returns The value, or `undefined` when it is not a valid JSON value
|
|
1116
|
+
*/
|
|
1117
|
+
function parseJSONValue(value) {
|
|
1118
|
+
return isJSONValue(value) ? value : void 0;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Parse an unknown value as one of the allowed literal primitives.
|
|
1122
|
+
*
|
|
1123
|
+
* @remarks
|
|
1124
|
+
* Pairs with {@link literalOf} — both match by `Object.is`, so the
|
|
1125
|
+
* `parseEnum ↔ literalOf(...allowed)` pairing covers every literal primitive
|
|
1126
|
+
* (string, number, or boolean), not only strings. Matching is identity, never
|
|
1127
|
+
* cross-type coercion: `parseEnum('1', [1])` stays `undefined`.
|
|
1128
|
+
*
|
|
1129
|
+
* @param value - The value to parse
|
|
1130
|
+
* @param allowed - The permitted literal values
|
|
1131
|
+
* @returns The matched literal (by identity), or `undefined`
|
|
1132
|
+
*/
|
|
1133
|
+
function parseEnum(value, allowed) {
|
|
1134
|
+
for (const option of allowed) if (Object.is(value, option)) return option;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Read and parse a string field from a record by key or nested key path.
|
|
1138
|
+
*
|
|
1139
|
+
* @param record - The source record
|
|
1140
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1141
|
+
* @returns A string, or `undefined`
|
|
1142
|
+
*/
|
|
1143
|
+
function parseStringField(record, path) {
|
|
1144
|
+
return parseString(resolveField(record, path));
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Read and parse a finite-number field from a record by key or nested key path.
|
|
1148
|
+
*
|
|
1149
|
+
* @param record - The source record
|
|
1150
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1151
|
+
* @returns A finite number, or `undefined`
|
|
1152
|
+
*/
|
|
1153
|
+
function parseNumberField(record, path) {
|
|
1154
|
+
return parseNumber(resolveField(record, path));
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Read and parse a finite-integer field from a record by key or nested key path.
|
|
1158
|
+
*
|
|
1159
|
+
* @param record - The source record
|
|
1160
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1161
|
+
* @returns A finite integer, or `undefined`
|
|
1162
|
+
*/
|
|
1163
|
+
function parseIntegerField(record, path) {
|
|
1164
|
+
return parseInteger(resolveField(record, path));
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Read and parse a boolean field from a record by key or nested key path.
|
|
1168
|
+
*
|
|
1169
|
+
* @param record - The source record
|
|
1170
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1171
|
+
* @returns A boolean, or `undefined`
|
|
1172
|
+
*/
|
|
1173
|
+
function parseBooleanField(record, path) {
|
|
1174
|
+
return parseBoolean(resolveField(record, path));
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Read and parse a `null` field from a record by key or nested key path.
|
|
1178
|
+
*
|
|
1179
|
+
* @remarks
|
|
1180
|
+
* A successful parse returns `null` itself — distinct from the `undefined`
|
|
1181
|
+
* failure sentinel, which also covers a missing field.
|
|
1182
|
+
*
|
|
1183
|
+
* @param record - The source record
|
|
1184
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1185
|
+
* @returns `null` on a successful parse, or `undefined`
|
|
1186
|
+
*/
|
|
1187
|
+
function parseNullField(record, path) {
|
|
1188
|
+
return parseNull(resolveField(record, path));
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Read and parse a nested record field from a record by key or nested key path.
|
|
1192
|
+
*
|
|
1193
|
+
* @param record - The source record
|
|
1194
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1195
|
+
* @returns A plain record, or `undefined`
|
|
1196
|
+
*/
|
|
1197
|
+
function parseRecordField(record, path) {
|
|
1198
|
+
return parseRecord(resolveField(record, path));
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Read and parse an array field from a record by key or nested key path,
|
|
1202
|
+
* optionally guarding elements.
|
|
1203
|
+
*
|
|
1204
|
+
* @param record - The source record
|
|
1205
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1206
|
+
* @param guard - Optional element guard
|
|
1207
|
+
* @returns An array, or `undefined`
|
|
1208
|
+
*/
|
|
1209
|
+
function parseArrayField(record, path, guard) {
|
|
1210
|
+
return parseArray(resolveField(record, path), guard);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Read and parse an enum field from a record by key or nested key path.
|
|
1214
|
+
*
|
|
1215
|
+
* @param record - The source record
|
|
1216
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1217
|
+
* @param allowed - The permitted literal values
|
|
1218
|
+
* @returns The matched literal, or `undefined`
|
|
1219
|
+
*/
|
|
1220
|
+
function parseEnumField(record, path, allowed) {
|
|
1221
|
+
return parseEnum(resolveField(record, path), allowed);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Read and parse a JSON-value field from a record by key or nested key path.
|
|
1225
|
+
*
|
|
1226
|
+
* @remarks
|
|
1227
|
+
* Deep-gates the field's whole subtree via {@link parseJSONValue} — see that
|
|
1228
|
+
* function's remarks for why this differs from the shallow
|
|
1229
|
+
* {@link parseRecordField} / {@link parseArrayField}.
|
|
1230
|
+
*
|
|
1231
|
+
* @param record - The source record
|
|
1232
|
+
* @param path - A property key, or a key path descending into nested objects
|
|
1233
|
+
* @returns The value, or `undefined`
|
|
1234
|
+
*/
|
|
1235
|
+
function parseJSONValueField(record, path) {
|
|
1236
|
+
return parseJSONValue(resolveField(record, path));
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Parse a JSON string, returning `undefined` instead of throwing.
|
|
1240
|
+
*
|
|
1241
|
+
* @remarks
|
|
1242
|
+
* The safe boundary for untrusted JSON text: a malformed string yields
|
|
1243
|
+
* `undefined`, never an exception. Returns `unknown` — a successful parse proves
|
|
1244
|
+
* nothing about shape, so narrow the result with a guard (or use
|
|
1245
|
+
* {@link parseJSONAs}). A large document is not walked here; parsing is shallow
|
|
1246
|
+
* and lazy validation is the caller's to compose.
|
|
1247
|
+
*
|
|
1248
|
+
* @param value - The JSON string to parse
|
|
1249
|
+
* @returns The parsed value, or `undefined` when `value` is not valid JSON
|
|
1250
|
+
*/
|
|
1251
|
+
function parseJSON(value) {
|
|
1252
|
+
try {
|
|
1253
|
+
return JSON.parse(value);
|
|
1254
|
+
} catch {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Parse a JSON string and validate the result against a guard.
|
|
1260
|
+
*
|
|
1261
|
+
* @remarks
|
|
1262
|
+
* The lazy, safe path from an untrusted string to a typed `T`: parse, then check
|
|
1263
|
+
* the parsed value with the guard you bring — typically one composed from the
|
|
1264
|
+
* combinators (`recordOf`, `arrayOf`, …). Only the shape the guard inspects is
|
|
1265
|
+
* validated, so a large document is never walked in full unless the guard does.
|
|
1266
|
+
*
|
|
1267
|
+
* @param value - The JSON string to parse
|
|
1268
|
+
* @param guard - The guard for the expected shape
|
|
1269
|
+
* @returns The parsed value when it satisfies `guard`, otherwise `undefined`
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* const isConfig = recordOf({ host: isString, tags: arrayOf(isString) })
|
|
1274
|
+
* parseJSONAs('{"host":"localhost","tags":["a"]}', isConfig) // { host: 'localhost', tags: ['a'] }
|
|
1275
|
+
* parseJSONAs('{"host":"localhost"}', isConfig) // undefined — guard fails
|
|
1276
|
+
* parseJSONAs('not json', isConfig) // undefined — never throws
|
|
1277
|
+
* ```
|
|
1278
|
+
*/
|
|
1279
|
+
function parseJSONAs(value, guard) {
|
|
1280
|
+
const parsed = parseJSON(value);
|
|
1281
|
+
if (parsed === void 0) return void 0;
|
|
1282
|
+
return guard(parsed) ? parsed : void 0;
|
|
1283
|
+
}
|
|
1284
|
+
//#endregion
|
|
1285
|
+
//#region src/core/compilers.ts
|
|
1286
|
+
/**
|
|
1287
|
+
* Validate that a {@link ContractShape} tree is well-formed — a pure recursive
|
|
1288
|
+
* prepass run before compilation.
|
|
1289
|
+
*
|
|
1290
|
+
* @remarks
|
|
1291
|
+
* Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
|
|
1292
|
+
* throws a plain `Error` immediately rather than surfacing as a silently-wrong
|
|
1293
|
+
* guard, parser, schema, or generator later. Checks, recursively:
|
|
1294
|
+
*
|
|
1295
|
+
* - An {@link OptionalShape} is only legal as a direct object-property value —
|
|
1296
|
+
* `optionalShape` wrapping an array item, a union variant, another
|
|
1297
|
+
* optional/nullable's inner shape, `additionalProperties`, or the top-level
|
|
1298
|
+
* shape all throw. An object property IS the one legal placement: its value
|
|
1299
|
+
* is unwrapped to `.inner` before recursing, so `.inner` itself is validated
|
|
1300
|
+
* as a normal (non-optional-wrapping) shape.
|
|
1301
|
+
* - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
|
|
1302
|
+
* needs at least one value and rejects non-finite (`NaN` / `Infinity` /
|
|
1303
|
+
* `-Infinity`) number values.
|
|
1304
|
+
* - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
|
|
1305
|
+
* needs `min <= max` when both are set.
|
|
1306
|
+
* - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
|
|
1307
|
+
* range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
|
|
1308
|
+
* - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
|
|
1309
|
+
* continues into array items, object properties (and `additionalProperties`
|
|
1310
|
+
* when it is a shape), union variants, and optional/nullable inner shapes.
|
|
1311
|
+
*
|
|
1312
|
+
* @param shape - The shape to validate
|
|
1313
|
+
* @throws {Error} When the shape is malformed
|
|
1314
|
+
*/
|
|
1315
|
+
function validateShape(shape) {
|
|
1316
|
+
switch (shape.type) {
|
|
1317
|
+
case "string":
|
|
1318
|
+
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a string shape has min greater than max");
|
|
1319
|
+
return;
|
|
1320
|
+
case "number":
|
|
1321
|
+
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: a number shape has min greater than max");
|
|
1322
|
+
if (shape.integer === true) {
|
|
1323
|
+
if (Math.ceil(shape.min ?? Number.NEGATIVE_INFINITY) > Math.floor(shape.max ?? Number.POSITIVE_INFINITY)) throw new Error("validateShape: an integer number shape has an empty integer range");
|
|
1324
|
+
}
|
|
1325
|
+
return;
|
|
1326
|
+
case "boolean":
|
|
1327
|
+
case "null":
|
|
1328
|
+
case "json":
|
|
1329
|
+
case "raw": return;
|
|
1330
|
+
case "literal":
|
|
1331
|
+
if (shape.values.length === 0) throw new Error("validateShape: a literal shape needs at least one value");
|
|
1332
|
+
for (const value of shape.values) if (typeof value === "number" && !Number.isFinite(value)) throw new Error("validateShape: a literal shape may not contain non-finite number values");
|
|
1333
|
+
return;
|
|
1334
|
+
case "array":
|
|
1335
|
+
if (shape.min !== void 0 && shape.max !== void 0 && shape.min > shape.max) throw new Error("validateShape: an array shape has min greater than max");
|
|
1336
|
+
validateShape(shape.items);
|
|
1337
|
+
return;
|
|
1338
|
+
case "object": {
|
|
1339
|
+
for (const key of Object.keys(shape.properties)) {
|
|
1340
|
+
const child = shape.properties[key];
|
|
1341
|
+
if (child === void 0) continue;
|
|
1342
|
+
validateShape(child.type === "optional" ? child.inner : child);
|
|
1343
|
+
}
|
|
1344
|
+
const extra = shape.additionalProperties;
|
|
1345
|
+
if (extra !== void 0 && extra !== true && extra !== false) validateShape(extra);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
case "union":
|
|
1349
|
+
if (shape.variants.length === 0) throw new Error("validateShape: a union shape needs at least one variant");
|
|
1350
|
+
for (const variant of shape.variants) validateShape(variant);
|
|
1351
|
+
return;
|
|
1352
|
+
case "optional": throw new Error("validateShape: an optional shape may only appear as a direct object-property value");
|
|
1353
|
+
case "nullable":
|
|
1354
|
+
validateShape(shape.inner);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Compile a {@link ContractShape} into a JSON Schema document.
|
|
1360
|
+
*
|
|
1361
|
+
* @remarks
|
|
1362
|
+
* Object shapes emit `additionalProperties: false` (unless opened) and list only
|
|
1363
|
+
* required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
|
|
1364
|
+
* 'null' }`. Emission only — it never inspects a runtime value.
|
|
1365
|
+
*
|
|
1366
|
+
* @param shape - The shape to compile
|
|
1367
|
+
* @returns The emitted JSON Schema
|
|
1368
|
+
*/
|
|
1369
|
+
function compileSchema(shape) {
|
|
1370
|
+
switch (shape.type) {
|
|
1371
|
+
case "string": return {
|
|
1372
|
+
type: "string",
|
|
1373
|
+
...shape.min !== void 0 ? { minLength: shape.min } : {},
|
|
1374
|
+
...shape.max !== void 0 ? { maxLength: shape.max } : {},
|
|
1375
|
+
...shape.pattern !== void 0 ? { pattern: shape.pattern.source } : {},
|
|
1376
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1377
|
+
};
|
|
1378
|
+
case "number": return {
|
|
1379
|
+
type: shape.integer === true ? "integer" : "number",
|
|
1380
|
+
...shape.min !== void 0 ? { minimum: shape.min } : {},
|
|
1381
|
+
...shape.max !== void 0 ? { maximum: shape.max } : {},
|
|
1382
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1383
|
+
};
|
|
1384
|
+
case "boolean": return {
|
|
1385
|
+
type: "boolean",
|
|
1386
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1387
|
+
};
|
|
1388
|
+
case "null": return {
|
|
1389
|
+
type: "null",
|
|
1390
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1391
|
+
};
|
|
1392
|
+
case "json": return { ...shape.description !== void 0 ? { description: shape.description } : {} };
|
|
1393
|
+
case "literal": return {
|
|
1394
|
+
enum: [...shape.values],
|
|
1395
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1396
|
+
};
|
|
1397
|
+
case "array": return {
|
|
1398
|
+
type: "array",
|
|
1399
|
+
items: compileSchema(shape.items),
|
|
1400
|
+
...shape.min !== void 0 ? { minItems: shape.min } : {},
|
|
1401
|
+
...shape.max !== void 0 ? { maxItems: shape.max } : {},
|
|
1402
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1403
|
+
};
|
|
1404
|
+
case "object": {
|
|
1405
|
+
const properties = {};
|
|
1406
|
+
const required = [];
|
|
1407
|
+
for (const key of Object.keys(shape.properties)) {
|
|
1408
|
+
const child = shape.properties[key];
|
|
1409
|
+
if (child === void 0) continue;
|
|
1410
|
+
properties[key] = compileSchema(child);
|
|
1411
|
+
if (child.type !== "optional") required.push(key);
|
|
1412
|
+
}
|
|
1413
|
+
const extra = shape.additionalProperties;
|
|
1414
|
+
const additionalProperties = extra === true ? true : extra !== void 0 && extra !== false ? compileSchema(extra) : false;
|
|
1415
|
+
return {
|
|
1416
|
+
type: "object",
|
|
1417
|
+
...Object.keys(properties).length > 0 ? { properties } : {},
|
|
1418
|
+
...required.length > 0 ? { required } : {},
|
|
1419
|
+
additionalProperties,
|
|
1420
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
case "union": return {
|
|
1424
|
+
...shape.mode === "oneOf" ? { oneOf: shape.variants.map((variant) => compileSchema(variant)) } : { anyOf: shape.variants.map((variant) => compileSchema(variant)) },
|
|
1425
|
+
...shape.description !== void 0 ? { description: shape.description } : {}
|
|
1426
|
+
};
|
|
1427
|
+
case "optional": return compileSchema(shape.inner);
|
|
1428
|
+
case "nullable": return { anyOf: [compileSchema(shape.inner), { type: "null" }] };
|
|
1429
|
+
case "raw": return shape.schema;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Compile a {@link ContractShape} into a runtime type guard.
|
|
1434
|
+
*
|
|
1435
|
+
* @remarks
|
|
1436
|
+
* Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
|
|
1437
|
+
* `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
|
|
1438
|
+
* and `whereOf` for constraint refinement. Like every guard it is total — it
|
|
1439
|
+
* never throws (AGENTS §14).
|
|
1440
|
+
*
|
|
1441
|
+
* @param shape - The shape to compile
|
|
1442
|
+
* @returns A guard narrowing to the shape's inferred type
|
|
1443
|
+
*/
|
|
1444
|
+
function compileGuard(shape) {
|
|
1445
|
+
switch (shape.type) {
|
|
1446
|
+
case "string": return stringOf({
|
|
1447
|
+
min: shape.min,
|
|
1448
|
+
max: shape.max,
|
|
1449
|
+
pattern: shape.pattern
|
|
1450
|
+
});
|
|
1451
|
+
case "number": {
|
|
1452
|
+
const base = shape.integer === true ? isInteger : isFiniteNumber;
|
|
1453
|
+
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
1454
|
+
return shape.integer === true ? intersectionOf(isInteger, boundsOf(shape.min, shape.max)) : boundsOf(shape.min, shape.max);
|
|
1455
|
+
}
|
|
1456
|
+
case "boolean": return isBoolean;
|
|
1457
|
+
case "null": return isNull;
|
|
1458
|
+
case "json": return isJSONValue;
|
|
1459
|
+
case "literal": return literalOf(...shape.values);
|
|
1460
|
+
case "array": {
|
|
1461
|
+
const base = arrayOf(compileGuard(shape.items));
|
|
1462
|
+
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
1463
|
+
const withinLength = boundsOf(shape.min, shape.max);
|
|
1464
|
+
return whereOf(base, (value) => withinLength(value.length));
|
|
1465
|
+
}
|
|
1466
|
+
case "object": {
|
|
1467
|
+
const map = Object.create(null);
|
|
1468
|
+
const optionalKeys = [];
|
|
1469
|
+
for (const key of Object.keys(shape.properties)) {
|
|
1470
|
+
const child = shape.properties[key];
|
|
1471
|
+
if (child === void 0) continue;
|
|
1472
|
+
if (child.type === "optional") {
|
|
1473
|
+
map[key] = compileGuard(child.inner);
|
|
1474
|
+
optionalKeys.push(key);
|
|
1475
|
+
} else map[key] = compileGuard(child);
|
|
1476
|
+
}
|
|
1477
|
+
const extra = shape.additionalProperties;
|
|
1478
|
+
if (extra === void 0 || extra === false) return optionalKeys.length > 0 ? recordOf(map, optionalKeys) : recordOf(map);
|
|
1479
|
+
const additional = extra === true ? void 0 : compileGuard(extra);
|
|
1480
|
+
const required = Object.keys(map).filter((key) => !optionalKeys.includes(key));
|
|
1481
|
+
return (value) => {
|
|
1482
|
+
if (!isRecord(value)) return false;
|
|
1483
|
+
for (const key of required) if (!Object.hasOwn(value, key)) return false;
|
|
1484
|
+
const outcome = attempt(() => {
|
|
1485
|
+
for (const key of Object.keys(value)) {
|
|
1486
|
+
const guard = Object.hasOwn(map, key) ? map[key] : void 0;
|
|
1487
|
+
if (guard !== void 0) {
|
|
1488
|
+
if (!guard(value[key])) return false;
|
|
1489
|
+
} else if (additional !== void 0 && !additional(value[key])) return false;
|
|
1490
|
+
}
|
|
1491
|
+
return true;
|
|
1492
|
+
});
|
|
1493
|
+
return outcome.success && outcome.value;
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
case "union": return unionOf(...shape.variants.map((variant) => compileGuard(variant)));
|
|
1497
|
+
case "optional": return orOf(isUndefined, compileGuard(shape.inner));
|
|
1498
|
+
case "nullable": return nullableOf(compileGuard(shape.inner));
|
|
1499
|
+
case "raw": return (_value) => true;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Compile a {@link ContractShape} into an input parser.
|
|
1504
|
+
*
|
|
1505
|
+
* @remarks
|
|
1506
|
+
* Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
|
|
1507
|
+
* `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
|
|
1508
|
+
* whole on any required-field failure; a union returns a guard-valid value
|
|
1509
|
+
* unchanged, otherwise the first variant that both parses and guards wins.
|
|
1510
|
+
*
|
|
1511
|
+
* After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
|
|
1512
|
+
* combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
|
|
1513
|
+
* `boundsOf` for a number's value and an array's length — so a value that coerces
|
|
1514
|
+
* but violates a bound parses to `undefined`. The result is full parse↔guard
|
|
1515
|
+
* soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
|
|
1516
|
+
* `is`, refinements included.
|
|
1517
|
+
*
|
|
1518
|
+
* @param shape - The shape to compile
|
|
1519
|
+
* @returns A parser yielding the shape's inferred type or `undefined`
|
|
1520
|
+
*/
|
|
1521
|
+
function compileParser(shape) {
|
|
1522
|
+
switch (shape.type) {
|
|
1523
|
+
case "string": {
|
|
1524
|
+
if (shape.min === void 0 && shape.max === void 0 && shape.pattern === void 0) return parseString;
|
|
1525
|
+
const guard = stringOf({
|
|
1526
|
+
min: shape.min,
|
|
1527
|
+
max: shape.max,
|
|
1528
|
+
pattern: shape.pattern
|
|
1529
|
+
});
|
|
1530
|
+
return (value) => {
|
|
1531
|
+
const parsed = parseString(value);
|
|
1532
|
+
return parsed !== void 0 && guard(parsed) ? parsed : void 0;
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
case "number": {
|
|
1536
|
+
const base = shape.integer === true ? parseInteger : parseNumber;
|
|
1537
|
+
if (shape.min === void 0 && shape.max === void 0) return base;
|
|
1538
|
+
const within = boundsOf(shape.min, shape.max);
|
|
1539
|
+
return (value) => {
|
|
1540
|
+
const parsed = base(value);
|
|
1541
|
+
return parsed !== void 0 && within(parsed) ? parsed : void 0;
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
case "boolean": return parseBoolean;
|
|
1545
|
+
case "null": return (value) => value === null ? null : void 0;
|
|
1546
|
+
case "json": return (value) => isJSONValue(value) ? value : void 0;
|
|
1547
|
+
case "literal": {
|
|
1548
|
+
const allowed = new Set(shape.values);
|
|
1549
|
+
return (value) => {
|
|
1550
|
+
if (allowed.has(value)) return value;
|
|
1551
|
+
if (isString(value)) {
|
|
1552
|
+
const trimmed = value.trim();
|
|
1553
|
+
if (allowed.has(trimmed)) return trimmed;
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
case "array": {
|
|
1558
|
+
const item = compileParser(shape.items);
|
|
1559
|
+
const unbounded = shape.min === void 0 && shape.max === void 0;
|
|
1560
|
+
const withinLength = boundsOf(shape.min, shape.max);
|
|
1561
|
+
return (value) => {
|
|
1562
|
+
if (!isArray(value)) return void 0;
|
|
1563
|
+
const result = [];
|
|
1564
|
+
for (const entry of value) {
|
|
1565
|
+
const parsed = item(entry);
|
|
1566
|
+
if (parsed === void 0) return void 0;
|
|
1567
|
+
result.push(parsed);
|
|
1568
|
+
}
|
|
1569
|
+
return unbounded || withinLength(result.length) ? result : void 0;
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
case "object": {
|
|
1573
|
+
const entries = [];
|
|
1574
|
+
for (const key of Object.keys(shape.properties)) {
|
|
1575
|
+
const child = shape.properties[key];
|
|
1576
|
+
if (child === void 0) continue;
|
|
1577
|
+
const optional = child.type === "optional";
|
|
1578
|
+
entries.push({
|
|
1579
|
+
key,
|
|
1580
|
+
parse: compileParser(optional ? child.inner : child),
|
|
1581
|
+
optional
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
const known = new Set(entries.map((entry) => entry.key));
|
|
1585
|
+
const extra = shape.additionalProperties;
|
|
1586
|
+
const additional = extra === void 0 || extra === false || extra === true ? void 0 : compileParser(extra);
|
|
1587
|
+
const open = extra === true || additional !== void 0;
|
|
1588
|
+
return (value) => {
|
|
1589
|
+
const record = parseRecord(value);
|
|
1590
|
+
if (record === void 0) return void 0;
|
|
1591
|
+
const outcome = attempt(() => {
|
|
1592
|
+
const result = Object.create(null);
|
|
1593
|
+
for (const entry of entries) {
|
|
1594
|
+
const raw = record[entry.key];
|
|
1595
|
+
if (raw === void 0) {
|
|
1596
|
+
if (entry.optional) continue;
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const parsed = entry.parse(raw);
|
|
1600
|
+
if (parsed === void 0) return void 0;
|
|
1601
|
+
result[entry.key] = parsed;
|
|
1602
|
+
}
|
|
1603
|
+
if (open) for (const key of Object.keys(record)) {
|
|
1604
|
+
if (known.has(key)) continue;
|
|
1605
|
+
if (additional === void 0) result[key] = record[key];
|
|
1606
|
+
else {
|
|
1607
|
+
const parsed = additional(record[key]);
|
|
1608
|
+
if (parsed === void 0) return void 0;
|
|
1609
|
+
result[key] = parsed;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return result;
|
|
1613
|
+
});
|
|
1614
|
+
return outcome.success ? outcome.value : void 0;
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
case "union": {
|
|
1618
|
+
const variants = shape.variants.map((variant) => ({
|
|
1619
|
+
parse: compileParser(variant),
|
|
1620
|
+
guard: compileGuard(variant)
|
|
1621
|
+
}));
|
|
1622
|
+
return (value) => {
|
|
1623
|
+
for (const variant of variants) if (variant.guard(value)) return value;
|
|
1624
|
+
for (const variant of variants) {
|
|
1625
|
+
const parsed = variant.parse(value);
|
|
1626
|
+
if (parsed !== void 0 && variant.guard(parsed)) return parsed;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
case "optional": {
|
|
1631
|
+
const inner = compileParser(shape.inner);
|
|
1632
|
+
return (value) => value === void 0 ? void 0 : inner(value);
|
|
1633
|
+
}
|
|
1634
|
+
case "nullable": {
|
|
1635
|
+
const inner = compileParser(shape.inner);
|
|
1636
|
+
return (value) => value === null ? null : inner(value);
|
|
1637
|
+
}
|
|
1638
|
+
case "raw": return (value) => value;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Compile a {@link ContractShape} into a deterministic seed value.
|
|
1643
|
+
*
|
|
1644
|
+
* @remarks
|
|
1645
|
+
* The same shape and the same `random` source always produce the same value, so
|
|
1646
|
+
* seed data is reproducible. Defaults to a {@link seededRandom} source seeded
|
|
1647
|
+
* from the wall clock when none is supplied. Throws on a degenerate empty
|
|
1648
|
+
* `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
|
|
1649
|
+
* generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
|
|
1650
|
+
* schema is arbitrary and cannot be auto-generated) — a programmer error that
|
|
1651
|
+
* cannot generate a value (AGENTS §12). `createContract` runs
|
|
1652
|
+
* {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
|
|
1653
|
+
* bounded shape is normally caught there; these throws remain here as defense
|
|
1654
|
+
* for standalone `compileGenerator` use.
|
|
1655
|
+
*
|
|
1656
|
+
* @param shape - The shape to generate from
|
|
1657
|
+
* @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
|
|
1658
|
+
* @returns A value matching the shape
|
|
1659
|
+
*/
|
|
1660
|
+
function compileGenerator(shape, random = seededRandom(Date.now())) {
|
|
1661
|
+
switch (shape.type) {
|
|
1662
|
+
case "string": {
|
|
1663
|
+
const min = shape.min ?? 0;
|
|
1664
|
+
const max = shape.max ?? Math.max(min, 12);
|
|
1665
|
+
const length = Math.max(min, Math.min(max, 8));
|
|
1666
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1667
|
+
let value = "";
|
|
1668
|
+
for (let index = 0; index < length; index += 1) value += alphabet[Math.floor(random() * 36)];
|
|
1669
|
+
if (shape.pattern !== void 0 && !shape.pattern.test(value)) throw new Error("compileGenerator: a pattern-constrained string shape cannot be auto-generated — supply or verify values another way");
|
|
1670
|
+
return value;
|
|
1671
|
+
}
|
|
1672
|
+
case "number": {
|
|
1673
|
+
const min = shape.min ?? 0;
|
|
1674
|
+
const max = shape.max ?? 100;
|
|
1675
|
+
if (shape.integer === true) {
|
|
1676
|
+
const lo = Math.ceil(min);
|
|
1677
|
+
const hi = Math.floor(max);
|
|
1678
|
+
return Math.floor(random() * (hi - lo + 1)) + lo;
|
|
1679
|
+
}
|
|
1680
|
+
return random() * (max - min) + min;
|
|
1681
|
+
}
|
|
1682
|
+
case "boolean": return random() >= .5;
|
|
1683
|
+
case "null": return null;
|
|
1684
|
+
case "json": {
|
|
1685
|
+
const pick = Math.floor(random() * 5);
|
|
1686
|
+
if (pick === 0) return null;
|
|
1687
|
+
if (pick === 1) return random() >= .5;
|
|
1688
|
+
if (pick === 2) return Math.floor(random() * 1e3);
|
|
1689
|
+
if (pick === 3) {
|
|
1690
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
1691
|
+
let value = "";
|
|
1692
|
+
for (let index = 0; index < 6; index += 1) value += alphabet[Math.floor(random() * 26)];
|
|
1693
|
+
return value;
|
|
1694
|
+
}
|
|
1695
|
+
return { value: Math.floor(random() * 1e3) };
|
|
1696
|
+
}
|
|
1697
|
+
case "literal":
|
|
1698
|
+
if (shape.values.length === 0) throw new Error("compileGenerator: a literal shape needs at least one value");
|
|
1699
|
+
return shape.values[Math.floor(random() * shape.values.length)];
|
|
1700
|
+
case "array": {
|
|
1701
|
+
const lo = shape.min ?? Math.min(1, shape.max ?? 1);
|
|
1702
|
+
const hi = shape.max ?? Math.max(lo, 3);
|
|
1703
|
+
const length = Math.floor(random() * (hi - lo + 1)) + lo;
|
|
1704
|
+
const result = [];
|
|
1705
|
+
for (let index = 0; index < length; index += 1) result.push(compileGenerator(shape.items, random));
|
|
1706
|
+
return result;
|
|
1707
|
+
}
|
|
1708
|
+
case "object": {
|
|
1709
|
+
const result = {};
|
|
1710
|
+
for (const key of Object.keys(shape.properties)) {
|
|
1711
|
+
const child = shape.properties[key];
|
|
1712
|
+
if (child === void 0) continue;
|
|
1713
|
+
if (child.type === "optional" && random() < .3) continue;
|
|
1714
|
+
result[key] = compileGenerator(child, random);
|
|
1715
|
+
}
|
|
1716
|
+
const extra = shape.additionalProperties;
|
|
1717
|
+
if (extra !== void 0 && extra !== true && extra !== false) {
|
|
1718
|
+
const count = 1 + Math.floor(random() * 2);
|
|
1719
|
+
for (let index = 0; index < count; index += 1) {
|
|
1720
|
+
const key = `key${index}`;
|
|
1721
|
+
if (Object.hasOwn(result, key)) continue;
|
|
1722
|
+
result[key] = compileGenerator(extra, random);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return result;
|
|
1726
|
+
}
|
|
1727
|
+
case "union":
|
|
1728
|
+
if (shape.variants.length === 0) throw new Error("compileGenerator: a union shape needs at least one variant");
|
|
1729
|
+
return compileGenerator(shape.variants[Math.floor(random() * shape.variants.length)], random);
|
|
1730
|
+
case "optional": return compileGenerator(shape.inner, random);
|
|
1731
|
+
case "nullable": return random() < .2 ? null : compileGenerator(shape.inner, random);
|
|
1732
|
+
case "raw": throw new Error("compileGenerator: a raw shape embeds an arbitrary JSON Schema and cannot be auto-generated — supply values another way");
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function createContract(shape) {
|
|
1736
|
+
validateShape(shape);
|
|
1737
|
+
const schema = compileSchema(shape);
|
|
1738
|
+
const guard = compileGuard(shape);
|
|
1739
|
+
const parser = compileParser(shape);
|
|
1740
|
+
return {
|
|
1741
|
+
schema,
|
|
1742
|
+
is: guard,
|
|
1743
|
+
parse(value) {
|
|
1744
|
+
return parser(value);
|
|
1745
|
+
},
|
|
1746
|
+
generate(random) {
|
|
1747
|
+
return compileGenerator(shape, random);
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
//#endregion
|
|
1752
|
+
//#region src/core/shapers.ts
|
|
1753
|
+
/**
|
|
1754
|
+
* Build a string {@link StringShape}.
|
|
1755
|
+
*
|
|
1756
|
+
* @param options - Optional length (`min` / `max`), `pattern`, and `description`
|
|
1757
|
+
* @returns A string shape
|
|
1758
|
+
*
|
|
1759
|
+
* @example
|
|
1760
|
+
* ```ts
|
|
1761
|
+
* const name = stringShape({ min: 1, max: 80, description: 'Display name' })
|
|
1762
|
+
* ```
|
|
1763
|
+
*/
|
|
1764
|
+
function stringShape(options) {
|
|
1765
|
+
return {
|
|
1766
|
+
type: "string",
|
|
1767
|
+
min: options?.min,
|
|
1768
|
+
max: options?.max,
|
|
1769
|
+
pattern: options?.pattern,
|
|
1770
|
+
description: options?.description
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Build a numeric {@link NumberShape}.
|
|
1775
|
+
*
|
|
1776
|
+
* @param options - Optional bounds (`min` / `max`), `integer`, and `description`
|
|
1777
|
+
* @returns A number shape
|
|
1778
|
+
*/
|
|
1779
|
+
function numberShape(options) {
|
|
1780
|
+
return {
|
|
1781
|
+
type: "number",
|
|
1782
|
+
min: options?.min,
|
|
1783
|
+
max: options?.max,
|
|
1784
|
+
integer: options?.integer,
|
|
1785
|
+
description: options?.description
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Build an integer {@link NumberShape} — forces `integer: true`.
|
|
1790
|
+
*
|
|
1791
|
+
* @remarks
|
|
1792
|
+
* The emitted JSON Schema uses `"type": "integer"` and the guard rejects
|
|
1793
|
+
* fractional numbers.
|
|
1794
|
+
*
|
|
1795
|
+
* @param options - Optional bounds and `description` (no `integer` key)
|
|
1796
|
+
* @returns An integer number shape
|
|
1797
|
+
*/
|
|
1798
|
+
function integerShape(options) {
|
|
1799
|
+
return {
|
|
1800
|
+
type: "number",
|
|
1801
|
+
integer: true,
|
|
1802
|
+
min: options?.min,
|
|
1803
|
+
max: options?.max,
|
|
1804
|
+
description: options?.description
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Build a {@link BooleanShape}.
|
|
1809
|
+
*
|
|
1810
|
+
* @param options - Optional `description`
|
|
1811
|
+
* @returns A boolean shape
|
|
1812
|
+
*/
|
|
1813
|
+
function booleanShape(options) {
|
|
1814
|
+
return {
|
|
1815
|
+
type: "boolean",
|
|
1816
|
+
description: options?.description
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Build a {@link NullShape}.
|
|
1821
|
+
*
|
|
1822
|
+
* @param options - Optional `description`
|
|
1823
|
+
* @returns A null shape
|
|
1824
|
+
*/
|
|
1825
|
+
function nullShape(options) {
|
|
1826
|
+
return {
|
|
1827
|
+
type: "null",
|
|
1828
|
+
description: options?.description
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Build a literal shape from a fixed set of primitive values.
|
|
1833
|
+
*
|
|
1834
|
+
* @param values - The permitted literals
|
|
1835
|
+
* @param options - Optional `description`
|
|
1836
|
+
* @returns A literal shape whose `Infer` is the union of `values`
|
|
1837
|
+
*
|
|
1838
|
+
* @example
|
|
1839
|
+
* ```ts
|
|
1840
|
+
* const role = literalShape(['admin', 'member', 'guest'])
|
|
1841
|
+
* // Infer<typeof role> = 'admin' | 'member' | 'guest'
|
|
1842
|
+
*
|
|
1843
|
+
* const via = literalShape(['function', 'tool', 'agent'], { description: 'How to run the step.' })
|
|
1844
|
+
* ```
|
|
1845
|
+
*/
|
|
1846
|
+
function literalShape(values, options) {
|
|
1847
|
+
return {
|
|
1848
|
+
type: "literal",
|
|
1849
|
+
values,
|
|
1850
|
+
description: options?.description
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Build an {@link ArrayShape} from an element shape.
|
|
1855
|
+
*
|
|
1856
|
+
* @param items - The element shape
|
|
1857
|
+
* @param options - Optional length bounds and `description`
|
|
1858
|
+
* @returns An array shape
|
|
1859
|
+
*
|
|
1860
|
+
* @example
|
|
1861
|
+
* ```ts
|
|
1862
|
+
* const tags = arrayShape(stringShape(), { max: 10 })
|
|
1863
|
+
* ```
|
|
1864
|
+
*/
|
|
1865
|
+
function arrayShape(items, options) {
|
|
1866
|
+
return {
|
|
1867
|
+
type: "array",
|
|
1868
|
+
items,
|
|
1869
|
+
min: options?.min,
|
|
1870
|
+
max: options?.max,
|
|
1871
|
+
description: options?.description
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Build an {@link ObjectShape} from a property map.
|
|
1876
|
+
*
|
|
1877
|
+
* @remarks
|
|
1878
|
+
* Wrap any property in {@link optionalShape} to allow its absence. By default
|
|
1879
|
+
* the compiled guard rejects unknown keys; pass `additionalProperties` to open
|
|
1880
|
+
* the object.
|
|
1881
|
+
*
|
|
1882
|
+
* @param properties - Map of property names to child shapes
|
|
1883
|
+
* @param options - Optional `additionalProperties` and `description`
|
|
1884
|
+
* @returns An object shape
|
|
1885
|
+
*
|
|
1886
|
+
* @example
|
|
1887
|
+
* ```ts
|
|
1888
|
+
* const user = objectShape({
|
|
1889
|
+
* name: stringShape({ min: 1 }),
|
|
1890
|
+
* age: integerShape({ min: 0, max: 120 }),
|
|
1891
|
+
* bio: optionalShape(stringShape()),
|
|
1892
|
+
* })
|
|
1893
|
+
* ```
|
|
1894
|
+
*/
|
|
1895
|
+
function objectShape(properties, options) {
|
|
1896
|
+
return {
|
|
1897
|
+
type: "object",
|
|
1898
|
+
properties,
|
|
1899
|
+
additionalProperties: options?.additionalProperties,
|
|
1900
|
+
description: options?.description
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Build an open {@link ObjectShape} with no fixed properties — a dictionary.
|
|
1905
|
+
*
|
|
1906
|
+
* @remarks
|
|
1907
|
+
* Every value is validated against `values`; keys are unconstrained. Equivalent
|
|
1908
|
+
* to `objectShape({}, { additionalProperties: values })`.
|
|
1909
|
+
*
|
|
1910
|
+
* @param values - The shape every value must match
|
|
1911
|
+
* @param options - Optional `description`
|
|
1912
|
+
* @returns An open object shape
|
|
1913
|
+
*
|
|
1914
|
+
* @example
|
|
1915
|
+
* ```ts
|
|
1916
|
+
* const bindings = recordShape(numberShape()) // ~ Record<string, number>
|
|
1917
|
+
* ```
|
|
1918
|
+
*/
|
|
1919
|
+
function recordShape(values, options) {
|
|
1920
|
+
return {
|
|
1921
|
+
type: "object",
|
|
1922
|
+
properties: {},
|
|
1923
|
+
additionalProperties: values,
|
|
1924
|
+
description: options?.description
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Build a {@link UnionShape} from a list of variant shapes (`anyOf` in JSON Schema).
|
|
1929
|
+
*
|
|
1930
|
+
* @param variants - The candidate shapes; the first match wins at runtime
|
|
1931
|
+
* @returns A union shape whose `Infer` is the union of the variants
|
|
1932
|
+
*
|
|
1933
|
+
* @example
|
|
1934
|
+
* ```ts
|
|
1935
|
+
* const id = unionShape(stringShape(), integerShape())
|
|
1936
|
+
* // Infer<typeof id> = string | number
|
|
1937
|
+
* ```
|
|
1938
|
+
*/
|
|
1939
|
+
function unionShape(...variants) {
|
|
1940
|
+
return {
|
|
1941
|
+
type: "union",
|
|
1942
|
+
variants
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Build a {@link UnionShape} that emits `oneOf` (exactly one match) in JSON Schema.
|
|
1947
|
+
*
|
|
1948
|
+
* @remarks
|
|
1949
|
+
* Runtime behavior is identical to {@link unionShape} — only the emitted schema
|
|
1950
|
+
* keyword differs (`oneOf` vs `anyOf`).
|
|
1951
|
+
*
|
|
1952
|
+
* @param variants - The candidate shapes
|
|
1953
|
+
* @returns A union shape with `mode: 'oneOf'`
|
|
1954
|
+
*/
|
|
1955
|
+
function oneOfShape(...variants) {
|
|
1956
|
+
return {
|
|
1957
|
+
type: "union",
|
|
1958
|
+
variants,
|
|
1959
|
+
mode: "oneOf"
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Wrap a shape so it may be absent (`undefined`).
|
|
1964
|
+
*
|
|
1965
|
+
* @remarks
|
|
1966
|
+
* As an {@link objectShape} property, the field becomes a true optional property
|
|
1967
|
+
* in the inferred type.
|
|
1968
|
+
*
|
|
1969
|
+
* @param inner - The wrapped shape
|
|
1970
|
+
* @returns An optional shape
|
|
1971
|
+
*/
|
|
1972
|
+
function optionalShape(inner) {
|
|
1973
|
+
return {
|
|
1974
|
+
type: "optional",
|
|
1975
|
+
inner
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Wrap a shape so it may be `null`.
|
|
1980
|
+
*
|
|
1981
|
+
* @param inner - The wrapped shape
|
|
1982
|
+
* @returns A nullable shape
|
|
1983
|
+
*/
|
|
1984
|
+
function nullableShape(inner) {
|
|
1985
|
+
return {
|
|
1986
|
+
type: "nullable",
|
|
1987
|
+
inner
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Build a {@link JSONShape}.
|
|
1992
|
+
*
|
|
1993
|
+
* @remarks
|
|
1994
|
+
* The sound counterpart of {@link rawShape}: `rawShape` embeds an arbitrary
|
|
1995
|
+
* schema fragment and accepts anything at runtime, while `jsonShape` validates
|
|
1996
|
+
* that a value is real JSON (via {@link isJSONValue}).
|
|
1997
|
+
*
|
|
1998
|
+
* @param options - Optional `description`
|
|
1999
|
+
* @returns A JSON passthrough shape
|
|
2000
|
+
*/
|
|
2001
|
+
function jsonShape(options) {
|
|
2002
|
+
return {
|
|
2003
|
+
type: "json",
|
|
2004
|
+
description: options?.description
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Build a {@link RawShape} from a JSON Schema fragment.
|
|
2009
|
+
*
|
|
2010
|
+
* @remarks
|
|
2011
|
+
* For values the shape DSL can't express. The compiled guard accepts any value;
|
|
2012
|
+
* the parser passes it through; the schema is emitted verbatim.
|
|
2013
|
+
*
|
|
2014
|
+
* @param schema - The JSON Schema fragment to embed
|
|
2015
|
+
* @returns A raw shape
|
|
2016
|
+
*/
|
|
2017
|
+
function rawShape(schema) {
|
|
2018
|
+
return {
|
|
2019
|
+
type: "raw",
|
|
2020
|
+
schema
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
//#endregion
|
|
2024
|
+
export { JSON_SCHEMA_TYPES, andOf, arrayOf, arrayShape, attempt, booleanShape, boundsOf, compileGenerator, compileGuard, compileParser, compileSchema, complementOf, createContract, enumOf, enumerableSymbolCount, instanceOf, integerShape, intersectionOf, isArray, isArrayBuffer, isArrayBufferView, isAsyncFunction, isAsyncGeneratorFunction, isAsyncIterable, isBigInt, isBigInt64Array, isBigUint64Array, isBoolean, isConstructor, isDataView, isDate, isDefined, isEmptyArray, isEmptyMap, isEmptyObject, isEmptySet, isEmptyString, isError, isFalse, isFiniteNumber, isFloat32Array, isFloat64Array, isFunction, isGeneratorFunction, isInt16Array, isInt32Array, isInt8Array, isInteger, isIterable, isJSONPrimitive, isJSONValue, isMap, isNonEmptyArray, isNonEmptyMap, isNonEmptyObject, isNonEmptySet, isNonEmptyString, isNull, isNullableBoolean, isNullableNumber, isNullableString, isNumber, isObject, isPromise, isPromiseLike, isRecord, isRegExp, isSet, isSharedArrayBuffer, isString, isSymbol, isTrue, isUint16Array, isUint32Array, isUint8Array, isUint8ClampedArray, isUndefined, isWeakMap, isWeakSet, isZeroArg, isZeroArgAsync, isZeroArgAsyncGenerator, isZeroArgGenerator, iterableOf, jsonShape, keyOf, lazyOf, literalOf, literalShape, mapOf, matchOf, notOf, nullShape, nullableOf, nullableShape, numberShape, objectShape, omitOf, oneOfShape, optionalOf, optionalShape, orOf, parseArray, parseArrayField, parseBoolean, parseBooleanField, parseEnum, parseEnumField, parseInteger, parseIntegerField, parseJSON, parseJSONAs, parseJSONValue, parseJSONValueField, parseNull, parseNullField, parseNumber, parseNumberField, parseRecord, parseRecordField, parseString, parseStringField, pickOf, rawShape, recordOf, recordShape, resolveField, schemaToParameters, seededRandom, setOf, stringOf, stringShape, transformOf, tupleOf, unionOf, unionShape, validateShape, whereOf };
|
|
2025
|
+
|
|
2026
|
+
//# sourceMappingURL=index.js.map
|