@oh-my-pi/omptype 17.2.6 → 17.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -2
- package/README.md +83 -14
- package/dist/js/ark.js +16 -0
- package/dist/js/compile.js +747 -0
- package/dist/js/errors.js +199 -0
- package/dist/js/index.js +12 -0
- package/dist/js/infer.js +2 -0
- package/dist/js/interp.js +476 -0
- package/dist/js/ir.js +930 -0
- package/dist/js/json-schema.js +273 -0
- package/dist/js/keywords.js +233 -0
- package/dist/js/type.js +1078 -0
- package/dist/js/typebox.js +346 -0
- package/dist/js/zod.js +269 -0
- package/dist/types/ark.d.ts +3 -11
- package/dist/types/compile.d.ts +2 -0
- package/dist/types/errors.d.ts +51 -15
- package/dist/types/index.d.ts +3 -4
- package/dist/types/infer.d.ts +107 -30
- package/dist/types/interp.d.ts +5 -3
- package/dist/types/ir.d.ts +76 -7
- package/dist/types/json-schema.d.ts +6 -1
- package/dist/types/keywords.d.ts +7 -0
- package/dist/types/type.d.ts +238 -49
- package/dist/types/zod.d.ts +1 -1
- package/package.json +20 -8
- package/src/ark.ts +4 -14
- package/src/compile.ts +432 -54
- package/src/errors.ts +146 -34
- package/src/index.ts +3 -4
- package/src/infer.ts +284 -77
- package/src/interp.ts +153 -9
- package/src/ir.ts +636 -89
- package/src/json-schema.ts +109 -14
- package/src/keywords.ts +270 -0
- package/src/type.ts +1302 -147
- package/src/typebox.ts +1 -1
- package/src/zod.ts +40 -27
package/src/type.ts
CHANGED
|
@@ -10,12 +10,25 @@
|
|
|
10
10
|
* A schema is a callable: `schema(data)` returns the (possibly morphed)
|
|
11
11
|
* output, or an `OmpErrors` on failure (`result instanceof type.errors`).
|
|
12
12
|
*/
|
|
13
|
-
import { compile } from "./compile";
|
|
14
|
-
import { OmpErrors, OmpTypeError, TraversalError } from "./errors";
|
|
15
|
-
import type { InferDef, InferObjectLiteral, InferString } from "./infer";
|
|
13
|
+
import { compile, compileAllows } from "./compile";
|
|
14
|
+
import { type ErrorConfig, OmpErrors, OmpTypeError, TraversalError } from "./errors";
|
|
15
|
+
import type { InferDef, InferDefIn, InferObjectLiteral, InferString } from "./infer";
|
|
16
16
|
import { walk } from "./interp";
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
type AliasResolver,
|
|
19
|
+
type Constructor,
|
|
20
|
+
type Def,
|
|
21
|
+
embed,
|
|
22
|
+
expectedOf,
|
|
23
|
+
hasMorph,
|
|
24
|
+
type IR,
|
|
25
|
+
IR_BRAND,
|
|
26
|
+
keyOf,
|
|
27
|
+
type PropIR,
|
|
28
|
+
parseDef,
|
|
29
|
+
} from "./ir";
|
|
30
|
+
import { irToJsonSchema, type JsonSchemaOptions } from "./json-schema";
|
|
31
|
+
import { keywordIR, patternIR } from "./keywords";
|
|
19
32
|
|
|
20
33
|
/** Context passed to `.narrow()` / `.pipe()` callbacks. */
|
|
21
34
|
export interface NarrowContext {
|
|
@@ -25,20 +38,43 @@ export interface NarrowContext {
|
|
|
25
38
|
reject(problem: string): false;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
/**
|
|
29
|
-
export interface
|
|
30
|
-
|
|
31
|
-
dialect?: string;
|
|
32
|
-
fallback?: (ctx: { base: Record<string, unknown> }) => unknown;
|
|
41
|
+
/** Schema metadata and validation-message overrides accepted by `.configure()`. */
|
|
42
|
+
export interface SchemaConfig extends ErrorConfig {
|
|
43
|
+
readonly description?: string;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
/** Options accepted by `Type.toJsonSchema`. */
|
|
47
|
+
export interface ToJsonSchemaOptions extends JsonSchemaOptions {}
|
|
48
|
+
|
|
49
|
+
declare const brand: unique symbol;
|
|
50
|
+
|
|
51
|
+
/** Inference-only nominal brand attached by `.brand(name)`. */
|
|
52
|
+
export type Brand<t, name extends string> = t & { readonly [brand]: name };
|
|
53
|
+
|
|
54
|
+
interface SchemaInference<out t, i = t> {
|
|
36
55
|
readonly [IR_BRAND]: true;
|
|
37
56
|
readonly infer: t;
|
|
57
|
+
readonly inferIn: i;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Property descriptor exposed by object schemas and consumed by `.map()`. */
|
|
61
|
+
export interface TypeProperty {
|
|
62
|
+
readonly kind: "required" | "optional";
|
|
63
|
+
readonly key: PropertyKey;
|
|
64
|
+
readonly value: FluentType<unknown>;
|
|
65
|
+
readonly default?: unknown;
|
|
66
|
+
readonly meta: Readonly<Record<string, unknown>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Structural node returned by `.select()`. */
|
|
70
|
+
export interface SelectedNode {
|
|
71
|
+
readonly kind: string;
|
|
72
|
+
readonly node: IR;
|
|
73
|
+
readonly unit?: unknown;
|
|
38
74
|
}
|
|
39
75
|
|
|
40
76
|
/** A compiled schema: callable validator plus composition methods. */
|
|
41
|
-
export interface Type<out t = unknown> {
|
|
77
|
+
export interface Type<out t = unknown, i = t> {
|
|
42
78
|
(data: unknown): t | OmpErrors;
|
|
43
79
|
readonly [IR_BRAND]: true;
|
|
44
80
|
/** Structural IR (base type; runtime steps live in `steps`). */
|
|
@@ -54,37 +90,120 @@ export interface Type<out t = unknown> {
|
|
|
54
90
|
/** Inference-only output type (no runtime value). */
|
|
55
91
|
readonly infer: t;
|
|
56
92
|
/** Inference-only input type (no runtime value). */
|
|
57
|
-
readonly inferIn:
|
|
93
|
+
readonly inferIn: i;
|
|
58
94
|
|
|
59
95
|
/** Structural + narrow check without running pipes. */
|
|
60
|
-
allows(data: unknown): data is
|
|
96
|
+
allows(data: unknown): data is i;
|
|
61
97
|
/** Validate and return output, throwing `TraversalError` on failure. */
|
|
62
98
|
assert(data: unknown): t;
|
|
63
|
-
/**
|
|
99
|
+
/** Validate a statically typed input and return its output. */
|
|
100
|
+
from(data: i): t;
|
|
101
|
+
/** JSON Schema for this schema's structural base. */
|
|
64
102
|
toJsonSchema(options?: ToJsonSchemaOptions): Record<string, unknown>;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
type MergeTypes<left, right> = left extends object
|
|
106
|
+
? right extends object
|
|
107
|
+
? Omit<left, keyof right> & right
|
|
108
|
+
: right
|
|
109
|
+
: right;
|
|
110
|
+
|
|
111
|
+
interface FluentMethods<t, i> {
|
|
112
|
+
describe(description: string): FluentType<t, i>;
|
|
113
|
+
configure(config: SchemaConfig): FluentType<t, i>;
|
|
114
|
+
default(value: t | (() => t)): FluentType<t, i>;
|
|
115
|
+
optional(): readonly [SchemaInference<t, i>, "?"];
|
|
116
|
+
or<r, ri>(def: SchemaInference<r, ri>): FluentType<t | r, i | ri>;
|
|
117
|
+
or<const def extends string>(def: def): FluentType<t | InferString<def>, i | InferString<def>>;
|
|
118
|
+
or<const def extends Record<string, unknown>>(
|
|
119
|
+
def: def,
|
|
120
|
+
): FluentType<t | InferObjectLiteral<def>, i | InferObjectLiteral<def>>;
|
|
74
121
|
or(def: Def): FluentType<unknown>;
|
|
75
|
-
and<r>(def: SchemaInference<r>): FluentType<t & r>;
|
|
76
|
-
and<const def extends Record<string, unknown>>(
|
|
122
|
+
and<r, ri>(def: SchemaInference<r, ri>): FluentType<t & r, i & ri>;
|
|
123
|
+
and<const def extends Record<string, unknown>>(
|
|
124
|
+
def: def,
|
|
125
|
+
): FluentType<t & InferObjectLiteral<def>, i & InferObjectLiteral<def>>;
|
|
77
126
|
and(def: Def): FluentType<unknown>;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
equals(def: Def): boolean;
|
|
128
|
+
ifEquals(def: Def): FluentType<t, i> | undefined;
|
|
129
|
+
extends(def: Def): boolean;
|
|
130
|
+
overlaps(def: Def): boolean;
|
|
131
|
+
distribute<r>(mapper: (branch: FluentType<unknown>) => SchemaInference<r>): FluentType<r>;
|
|
132
|
+
select(kind: string): readonly SelectedNode[];
|
|
133
|
+
array(): FluentType<t[], i[]>;
|
|
134
|
+
atLeastLength(bound: number): FluentType<t, i>;
|
|
135
|
+
atMostLength(bound: number): FluentType<t, i>;
|
|
136
|
+
moreThanLength(bound: number): FluentType<t, i>;
|
|
137
|
+
lessThanLength(bound: number): FluentType<t, i>;
|
|
138
|
+
exactlyLength(bound: number): FluentType<t, i>;
|
|
139
|
+
atLeast(bound: number): FluentType<t, i>;
|
|
140
|
+
atMost(bound: number): FluentType<t, i>;
|
|
141
|
+
moreThan(bound: number): FluentType<t, i>;
|
|
142
|
+
lessThan(bound: number): FluentType<t, i>;
|
|
143
|
+
divisibleBy(divisor: number): FluentType<t, i>;
|
|
144
|
+
positive(): FluentType<t, i>;
|
|
145
|
+
negative(): FluentType<t, i>;
|
|
146
|
+
nonNegative(): FluentType<t, i>;
|
|
147
|
+
nonPositive(): FluentType<t, i>;
|
|
148
|
+
matching(pattern: RegExp): FluentType<t, i>;
|
|
149
|
+
atOrAfter(bound: Date): FluentType<t, i>;
|
|
150
|
+
atOrBefore(bound: Date): FluentType<t, i>;
|
|
151
|
+
laterThan(bound: Date): FluentType<t, i>;
|
|
152
|
+
earlierThan(bound: Date): FluentType<t, i>;
|
|
153
|
+
pipe<r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>, i>;
|
|
154
|
+
to<const def>(def: def): FluentType<InferDef<def>, i>;
|
|
155
|
+
filter<narrowed extends i>(fn: (data: i, ctx: NarrowContext) => data is narrowed): FluentType<t, narrowed>;
|
|
156
|
+
filter(fn: (data: i, ctx: NarrowContext) => boolean): FluentType<t, i>;
|
|
157
|
+
narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed, i>;
|
|
158
|
+
narrow(fn: (data: t, ctx: NarrowContext) => boolean): FluentType<t, i>;
|
|
159
|
+
brand<const name extends string>(name: name): FluentType<Brand<t, name>, i>;
|
|
160
|
+
as<castTo>(): FluentType<castTo, i>;
|
|
161
|
+
extract<r, ri>(def: SchemaInference<r, ri>): FluentType<Extract<t, r>, Extract<i, ri>>;
|
|
162
|
+
extract<const def extends string>(def: def): FluentType<Extract<t, InferString<def>>, Extract<i, InferString<def>>>;
|
|
163
|
+
extract(def: Def): FluentType<unknown>;
|
|
164
|
+
exclude<r, ri>(def: SchemaInference<r, ri>): FluentType<Exclude<t, r>, Exclude<i, ri>>;
|
|
165
|
+
exclude<const def extends string>(def: def): FluentType<Exclude<t, InferString<def>>, Exclude<i, InferString<def>>>;
|
|
166
|
+
exclude(def: Def): FluentType<unknown>;
|
|
167
|
+
onUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
|
|
168
|
+
onDeepUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type InputObject<i> = i extends object ? i : object;
|
|
172
|
+
|
|
173
|
+
interface ObjectMethods<t extends object, i> {
|
|
174
|
+
readonly props: readonly TypeProperty[];
|
|
175
|
+
map(
|
|
176
|
+
mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[],
|
|
177
|
+
): FluentType<Record<PropertyKey, unknown>>;
|
|
178
|
+
keyof(): FluentType<Extract<keyof t, PropertyKey>, Extract<keyof InputObject<i>, PropertyKey>>;
|
|
179
|
+
get<key extends keyof t>(
|
|
180
|
+
key: key,
|
|
181
|
+
): FluentType<t[key], key extends keyof InputObject<i> ? InputObject<i>[key] : unknown>;
|
|
182
|
+
pick<const keys extends readonly (keyof t)[]>(
|
|
183
|
+
...keys: keys
|
|
184
|
+
): FluentType<Pick<t, keys[number]>, Pick<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
|
|
185
|
+
omit<const keys extends readonly (keyof t)[]>(
|
|
186
|
+
...keys: keys
|
|
187
|
+
): FluentType<Omit<t, keys[number]>, Omit<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
|
|
188
|
+
partial(): FluentType<Partial<t>, Partial<InputObject<i>>>;
|
|
189
|
+
required(): FluentType<Required<t>, Required<InputObject<i>>>;
|
|
190
|
+
merge<r, ri>(def: SchemaInference<r, ri>): FluentType<MergeTypes<t, r>, MergeTypes<i, ri>>;
|
|
191
|
+
merge<const def extends Record<string, unknown>>(
|
|
192
|
+
def: def,
|
|
193
|
+
): FluentType<MergeTypes<t, InferObjectLiteral<def>>, MergeTypes<i, InferObjectLiteral<def>>>;
|
|
194
|
+
merge(def: Def): FluentType<unknown>;
|
|
86
195
|
}
|
|
87
196
|
|
|
197
|
+
type ObjectMethodsFor<t, i> = [t] extends [never]
|
|
198
|
+
? unknown
|
|
199
|
+
: [t] extends [readonly unknown[]]
|
|
200
|
+
? unknown
|
|
201
|
+
: [t] extends [object]
|
|
202
|
+
? ObjectMethods<t & object, i>
|
|
203
|
+
: unknown;
|
|
204
|
+
/** Callable schema with fluent methods specialized to its output and input. */
|
|
205
|
+
export type FluentType<t = unknown, i = t> = Type<t, i> & FluentMethods<t, i> & ObjectMethodsFor<t, i>;
|
|
206
|
+
|
|
88
207
|
/** Runtime constructor-like value used by ArkType-compatible `instanceof Type` checks. */
|
|
89
208
|
export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasInstance, {
|
|
90
209
|
value: (value: unknown): boolean =>
|
|
@@ -92,14 +211,15 @@ export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasIn
|
|
|
92
211
|
});
|
|
93
212
|
|
|
94
213
|
interface Step {
|
|
95
|
-
kind: "pipe" | "narrow";
|
|
96
|
-
fn: (data:
|
|
214
|
+
kind: "pipe" | "narrow" | "filter";
|
|
215
|
+
fn: (data: unknown, ctx: NarrowContext) => unknown;
|
|
97
216
|
}
|
|
98
217
|
|
|
99
218
|
interface TypeMeta {
|
|
100
219
|
description?: string;
|
|
101
220
|
defaultValue?: unknown;
|
|
102
221
|
hasDefault?: boolean;
|
|
222
|
+
errorConfig?: ErrorConfig;
|
|
103
223
|
}
|
|
104
224
|
|
|
105
225
|
class Ctx implements NarrowContext {
|
|
@@ -116,12 +236,385 @@ class Ctx implements NarrowContext {
|
|
|
116
236
|
}
|
|
117
237
|
}
|
|
118
238
|
|
|
239
|
+
type Validator = (data: unknown) => unknown;
|
|
240
|
+
type Allows = (data: unknown) => boolean;
|
|
241
|
+
|
|
242
|
+
const kBase = Symbol("omptype.base");
|
|
243
|
+
const kSteps = Symbol("omptype.steps");
|
|
244
|
+
|
|
245
|
+
const EMPTY_STEPS: Step[] = [];
|
|
246
|
+
const EMPTY_META: TypeMeta = {};
|
|
247
|
+
|
|
248
|
+
interface InternalType {
|
|
249
|
+
(data: unknown): unknown;
|
|
250
|
+
[IR_BRAND]: true;
|
|
251
|
+
[kBase]: Validator;
|
|
252
|
+
[kSteps]: Step[];
|
|
253
|
+
allows: Allows;
|
|
254
|
+
ir: IR;
|
|
255
|
+
hasSteps: boolean;
|
|
256
|
+
hasDefault: boolean;
|
|
257
|
+
defaultValue?: unknown;
|
|
258
|
+
description?: string;
|
|
259
|
+
errorConfig?: ErrorConfig;
|
|
260
|
+
run: Validator;
|
|
261
|
+
}
|
|
262
|
+
|
|
119
263
|
/** Calls before the JIT compiles a schema (first two run the interpreter). */
|
|
120
264
|
const JIT_THRESHOLD = 3;
|
|
121
265
|
|
|
122
|
-
function
|
|
266
|
+
function metaOf(schema: InternalType): TypeMeta {
|
|
267
|
+
return {
|
|
268
|
+
description: schema.description,
|
|
269
|
+
defaultValue: schema.defaultValue,
|
|
270
|
+
hasDefault: schema.hasDefault,
|
|
271
|
+
errorConfig: schema.errorConfig,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const typeMethods = {
|
|
276
|
+
describe(this: InternalType, description: string): InternalType {
|
|
277
|
+
return makeType({ ...this.ir, desc: description }, this[kSteps], { ...metaOf(this), description });
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
configure(this: InternalType, config: SchemaConfig): InternalType {
|
|
281
|
+
const errorConfig: ErrorConfig = {
|
|
282
|
+
...this.errorConfig,
|
|
283
|
+
...(config.expected === undefined ? {} : { expected: config.expected }),
|
|
284
|
+
...(config.actual === undefined ? {} : { actual: config.actual }),
|
|
285
|
+
...(config.problem === undefined ? {} : { problem: config.problem }),
|
|
286
|
+
...(config.message === undefined ? {} : { message: config.message }),
|
|
287
|
+
};
|
|
288
|
+
const meta = {
|
|
289
|
+
...metaOf(this),
|
|
290
|
+
errorConfig,
|
|
291
|
+
...(config.description === undefined ? {} : { description: config.description }),
|
|
292
|
+
};
|
|
293
|
+
return config.description === undefined
|
|
294
|
+
? makeType(this.ir, this[kSteps], meta)
|
|
295
|
+
: makeType({ ...this.ir, desc: config.description }, this[kSteps], meta);
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
default(this: InternalType, value: unknown): InternalType {
|
|
299
|
+
return makeType(this.ir, this[kSteps], { ...metaOf(this), defaultValue: value, hasDefault: true });
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
optional(this: InternalType): readonly [InternalType, "?"] {
|
|
303
|
+
return [this, "?"];
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
or(this: InternalType, def: Def): InternalType {
|
|
307
|
+
const other = parseDef(def);
|
|
308
|
+
const a = embed(this);
|
|
309
|
+
const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
|
|
310
|
+
return makeType({ k: "union", members }, [], {});
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
equals(this: InternalType, def: Def): boolean {
|
|
314
|
+
return irEquals(embed(this), parseDef(def));
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
ifEquals(this: InternalType, def: Def): InternalType | undefined {
|
|
318
|
+
return irEquals(embed(this), parseDef(def)) ? this : undefined;
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
extends(this: InternalType, def: Def): boolean {
|
|
322
|
+
return isSubtype(embed(this), parseDef(def));
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
overlaps(this: InternalType, def: Def): boolean {
|
|
326
|
+
try {
|
|
327
|
+
intersect(embed(this), parseDef(def));
|
|
328
|
+
return true;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error instanceof OmpTypeError) return false;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
distribute(this: InternalType, mapper: (branch: BaseType) => InternalType): InternalType {
|
|
336
|
+
const branches = this.ir.k === "union" ? this.ir.members : [embed(this)];
|
|
337
|
+
const members = branches.map(branch => embed(mapper(makeType(branch, [], {}) as unknown as BaseType)));
|
|
338
|
+
return makeType(members.length === 1 ? members[0] : { k: "union", members }, [], {});
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
select(this: InternalType, kind: string): readonly SelectedNode[] {
|
|
342
|
+
return selectNodes(this.ir, kind);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
and(this: InternalType, def: Def): InternalType {
|
|
346
|
+
return makeType(intersect(embed(this), parseDef(def)), [], {});
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
array(this: InternalType): InternalType {
|
|
350
|
+
return makeType({ k: "array", el: embed(this) }, [], {});
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
atLeastLength(this: InternalType, bound: number): InternalType {
|
|
354
|
+
return makeType(withLengthBound(this.ir, "min", bound), this[kSteps], metaOf(this));
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
atMostLength(this: InternalType, bound: number): InternalType {
|
|
358
|
+
return makeType(withLengthBound(this.ir, "max", bound), this[kSteps], metaOf(this));
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
moreThanLength(this: InternalType, bound: number): InternalType {
|
|
362
|
+
return makeType(withLengthBound(this.ir, "min", bound + 1), this[kSteps], metaOf(this));
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
lessThanLength(this: InternalType, bound: number): InternalType {
|
|
366
|
+
return makeType(withLengthBound(this.ir, "max", bound - 1), this[kSteps], metaOf(this));
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
exactlyLength(this: InternalType, bound: number): InternalType {
|
|
370
|
+
const bounded = withLengthBound(withLengthBound(this.ir, "min", bound), "max", bound);
|
|
371
|
+
return makeType(bounded, this[kSteps], metaOf(this));
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
atLeast(this: InternalType, bound: number): InternalType {
|
|
375
|
+
return makeType(withNumericBound(this.ir, "min", bound), this[kSteps], metaOf(this));
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
atMost(this: InternalType, bound: number): InternalType {
|
|
379
|
+
return makeType(withNumericBound(this.ir, "max", bound), this[kSteps], metaOf(this));
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
moreThan(this: InternalType, bound: number): InternalType {
|
|
383
|
+
return makeType(withNumericBound(this.ir, "min", bound, true), this[kSteps], metaOf(this));
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
lessThan(this: InternalType, bound: number): InternalType {
|
|
387
|
+
return makeType(withNumericBound(this.ir, "max", bound, true), this[kSteps], metaOf(this));
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
divisibleBy(this: InternalType, divisor: number): InternalType {
|
|
391
|
+
if (this.ir.k !== "number") throw new OmpTypeError(`cannot apply divisibility to ${this.ir.k}`);
|
|
392
|
+
if (!Number.isFinite(divisor) || divisor === 0) throw new OmpTypeError("divisor must be non-zero");
|
|
393
|
+
return makeType({ ...this.ir, divisor }, this[kSteps], metaOf(this));
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
positive(this: InternalType): InternalType {
|
|
397
|
+
return makeType(withNumericBound(this.ir, "min", 0, true), this[kSteps], metaOf(this));
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
negative(this: InternalType): InternalType {
|
|
401
|
+
return makeType(withNumericBound(this.ir, "max", 0, true), this[kSteps], metaOf(this));
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
nonNegative(this: InternalType): InternalType {
|
|
405
|
+
return makeType(withNumericBound(this.ir, "min", 0), this[kSteps], metaOf(this));
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
nonPositive(this: InternalType): InternalType {
|
|
409
|
+
return makeType(withNumericBound(this.ir, "max", 0), this[kSteps], metaOf(this));
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
matching(this: InternalType, pattern: RegExp): InternalType {
|
|
413
|
+
return makeType(intersect(this.ir, patternIR(pattern)), this[kSteps], metaOf(this));
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
atOrAfter(this: InternalType, bound: Date): InternalType {
|
|
417
|
+
return dateRefinement(this, bound, "at or after", value => value >= bound.valueOf());
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
atOrBefore(this: InternalType, bound: Date): InternalType {
|
|
421
|
+
return dateRefinement(this, bound, "at or before", value => value <= bound.valueOf());
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
laterThan(this: InternalType, bound: Date): InternalType {
|
|
425
|
+
return dateRefinement(this, bound, "later than", value => value > bound.valueOf());
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
earlierThan(this: InternalType, bound: Date): InternalType {
|
|
429
|
+
return dateRefinement(this, bound, "earlier than", value => value < bound.valueOf());
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
pipe(this: InternalType, fn: Step["fn"]): InternalType {
|
|
433
|
+
return makeType(this.ir, [...this[kSteps], { kind: "pipe", fn }], metaOf(this));
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
to(this: InternalType, def: unknown): InternalType {
|
|
437
|
+
const output = makeType(parseDef(def), [], {});
|
|
438
|
+
return makeType(
|
|
439
|
+
this.ir,
|
|
440
|
+
[
|
|
441
|
+
...this[kSteps],
|
|
442
|
+
{
|
|
443
|
+
kind: "pipe",
|
|
444
|
+
fn: value => output(value),
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
metaOf(this),
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
filter(this: InternalType, fn: Step["fn"]): InternalType {
|
|
452
|
+
return makeType(this.ir, [{ kind: "filter", fn }, ...this[kSteps]], metaOf(this));
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
narrow(this: InternalType, fn: Step["fn"]): InternalType {
|
|
456
|
+
return makeType(this.ir, [...this[kSteps], { kind: "narrow", fn }], metaOf(this));
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
brand(this: InternalType): InternalType {
|
|
460
|
+
return makeType(this.ir, this[kSteps], metaOf(this));
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
as(this: InternalType): InternalType {
|
|
464
|
+
return makeType(this.ir, this[kSteps], metaOf(this));
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
keyof(this: InternalType): InternalType {
|
|
468
|
+
return makeType(keyOf(this.ir), [], {});
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
get(this: InternalType, key: PropertyKey): InternalType {
|
|
472
|
+
const object = requireObject(this.ir, "get");
|
|
473
|
+
const prop = object.props.find(candidate => candidate.key === String(key));
|
|
474
|
+
if (!prop) throw new OmpTypeError(`key ${String(key)} is not declared`);
|
|
475
|
+
const ir: IR = prop.opt ? { k: "union", members: [prop.val, { k: "undefined" }] } : prop.val;
|
|
476
|
+
return makeType(ir, [], {});
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
pick(this: InternalType, ...keys: PropertyKey[]): InternalType {
|
|
480
|
+
const object = requireObject(this.ir, "pick");
|
|
481
|
+
const selected = new Set(keys.map(String));
|
|
482
|
+
return makeType({ ...object, props: object.props.filter(prop => selected.has(prop.key)) }, [], {});
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
omit(this: InternalType, ...keys: PropertyKey[]): InternalType {
|
|
486
|
+
const object = requireObject(this.ir, "omit");
|
|
487
|
+
const omitted = new Set(keys.map(String));
|
|
488
|
+
return makeType({ ...object, props: object.props.filter(prop => !omitted.has(prop.key)) }, [], {});
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
partial(this: InternalType): InternalType {
|
|
492
|
+
const object = requireObject(this.ir, "partial");
|
|
493
|
+
return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: true })) }, [], {});
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
required(this: InternalType): InternalType {
|
|
497
|
+
const object = requireObject(this.ir, "required");
|
|
498
|
+
return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: false })) }, [], {});
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
map(this: InternalType, mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[]): InternalType {
|
|
502
|
+
const object = requireObject(this.ir, "map");
|
|
503
|
+
const props = object.props.flatMap(prop => {
|
|
504
|
+
const mapped = mapper(propertyFromIR(prop));
|
|
505
|
+
return (Array.isArray(mapped) ? mapped : [mapped]).map(propertyToIR);
|
|
506
|
+
});
|
|
507
|
+
return makeType({ ...object, props }, [], {});
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
merge(this: InternalType, def: unknown): InternalType {
|
|
511
|
+
return makeType(mergeObjects(requireObject(this.ir, "merge"), requireObject(parseDef(def), "merge")), [], {});
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
extract(this: InternalType, def: unknown): InternalType {
|
|
515
|
+
const other = parseDef(def);
|
|
516
|
+
return makeType(
|
|
517
|
+
{
|
|
518
|
+
k: "refine",
|
|
519
|
+
base: embed(this),
|
|
520
|
+
pred: value => !(walk(other, value) instanceof OmpErrors),
|
|
521
|
+
expected: "a value included by the extracted type",
|
|
522
|
+
},
|
|
523
|
+
[],
|
|
524
|
+
{},
|
|
525
|
+
);
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
exclude(this: InternalType, def: unknown): InternalType {
|
|
529
|
+
const other = parseDef(def);
|
|
530
|
+
return makeType(
|
|
531
|
+
{
|
|
532
|
+
k: "refine",
|
|
533
|
+
base: embed(this),
|
|
534
|
+
pred: value => walk(other, value) instanceof OmpErrors,
|
|
535
|
+
expected: "a value not excluded by the type",
|
|
536
|
+
},
|
|
537
|
+
[],
|
|
538
|
+
{},
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
onUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
|
|
543
|
+
const object = requireObject(this.ir, "onUndeclaredKey");
|
|
544
|
+
return makeType({ ...object, extras: behavior === "ignore" ? "keep" : behavior }, this[kSteps], metaOf(this));
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
onDeepUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
|
|
548
|
+
return makeType(withDeepExtras(this.ir, behavior === "ignore" ? "keep" : behavior), this[kSteps], metaOf(this));
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
allows(this: InternalType, data: unknown): boolean {
|
|
552
|
+
const steps = this[kSteps];
|
|
553
|
+
let needsPredicates = false;
|
|
554
|
+
for (const step of steps) {
|
|
555
|
+
if (step.kind !== "pipe") {
|
|
556
|
+
needsPredicates = true;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (!needsPredicates) {
|
|
561
|
+
const allows = compileAllows(this.ir);
|
|
562
|
+
// Shadow the shared dispatcher once this schema has its specialized check.
|
|
563
|
+
this.allows = allows;
|
|
564
|
+
return allows(data);
|
|
565
|
+
}
|
|
566
|
+
for (const step of steps) {
|
|
567
|
+
if (step.kind === "filter" && !step.fn(data, new Ctx())) return false;
|
|
568
|
+
}
|
|
569
|
+
const out = this[kBase](data);
|
|
570
|
+
if (out instanceof OmpErrors) return false;
|
|
571
|
+
for (const step of steps) {
|
|
572
|
+
if (step.kind === "narrow" && !step.fn(out, new Ctx())) return false;
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
assert(this: InternalType, data: unknown): unknown {
|
|
578
|
+
const out = this.run(data);
|
|
579
|
+
if (out instanceof OmpErrors) throw new TraversalError(out);
|
|
580
|
+
return out;
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
from(this: InternalType, data: unknown): unknown {
|
|
584
|
+
const out = this.run(data);
|
|
585
|
+
if (out instanceof OmpErrors) throw new TraversalError(out);
|
|
586
|
+
return out;
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
toJsonSchema(this: InternalType, options?: ToJsonSchemaOptions): Record<string, unknown> {
|
|
590
|
+
const description = options?.description ?? this.description;
|
|
591
|
+
return irToJsonSchema(
|
|
592
|
+
this.ir,
|
|
593
|
+
options === undefined && description === undefined
|
|
594
|
+
? undefined
|
|
595
|
+
: { ...options, ...(description === undefined ? {} : { description }) },
|
|
596
|
+
);
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
Object.defineProperty(typeMethods, "props", {
|
|
601
|
+
get(this: InternalType): readonly TypeProperty[] {
|
|
602
|
+
const object = requireObject(this.ir, "props");
|
|
603
|
+
return object.props.map(prop => propertyFromIR(prop));
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Share the fluent surface without per-schema method allocations or copies.
|
|
608
|
+
// Function.prototype remains in the chain, except bind is intentionally hidden
|
|
609
|
+
// so generic tool wrappers recognize callable schemas rather than rebinding them.
|
|
610
|
+
Object.setPrototypeOf(typeMethods, Function.prototype);
|
|
611
|
+
Object.defineProperty(typeMethods, "bind", { value: undefined });
|
|
612
|
+
|
|
613
|
+
function makeType(ir: IR, steps: Step[], meta: TypeMeta): InternalType;
|
|
614
|
+
function makeType<t = unknown, i = t>(ir: IR, steps: Step[], meta: TypeMeta): FluentType<t, i>;
|
|
615
|
+
function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
|
|
123
616
|
let calls = 0;
|
|
124
|
-
let impl = (data: unknown): unknown => {
|
|
617
|
+
let impl: Validator = (data: unknown): unknown => {
|
|
125
618
|
if (++calls >= JIT_THRESHOLD) {
|
|
126
619
|
impl = compile(ir);
|
|
127
620
|
return impl(data);
|
|
@@ -129,99 +622,256 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): FluentType {
|
|
|
129
622
|
return walk(ir, data);
|
|
130
623
|
};
|
|
131
624
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (step.kind === "narrow") {
|
|
140
|
-
if (!fn(out, ctx)) return OmpErrors.single([], ctx.expectation ?? "valid (narrow predicate failed)", out);
|
|
141
|
-
} else {
|
|
142
|
-
out = fn(out, ctx);
|
|
143
|
-
if (out instanceof OmpErrors) return out;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return out;
|
|
147
|
-
};
|
|
625
|
+
const base: Validator =
|
|
626
|
+
meta.errorConfig === undefined
|
|
627
|
+
? (data: unknown): unknown => impl(data)
|
|
628
|
+
: (data: unknown): unknown => {
|
|
629
|
+
const result = impl(data);
|
|
630
|
+
return result instanceof OmpErrors ? result.configure(meta.errorConfig ?? {}) : result;
|
|
631
|
+
};
|
|
148
632
|
|
|
149
|
-
const callable =
|
|
150
|
-
steps.length === 0
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
633
|
+
const callable: Validator =
|
|
634
|
+
steps.length === 0
|
|
635
|
+
? base
|
|
636
|
+
: (data: unknown): unknown => {
|
|
637
|
+
for (const step of steps) {
|
|
638
|
+
if (step.kind !== "filter") continue;
|
|
639
|
+
const ctx = new Ctx();
|
|
640
|
+
if (!step.fn(data, ctx)) {
|
|
641
|
+
return OmpErrors.single(
|
|
642
|
+
[],
|
|
643
|
+
ctx.expectation ?? "valid (input predicate failed)",
|
|
644
|
+
data,
|
|
645
|
+
meta.errorConfig,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
let out = base(data);
|
|
650
|
+
if (out instanceof OmpErrors) return out;
|
|
651
|
+
for (const step of steps) {
|
|
652
|
+
if (step.kind === "filter") continue;
|
|
653
|
+
const ctx = new Ctx();
|
|
654
|
+
if (step.kind === "narrow") {
|
|
655
|
+
if (!step.fn(out, ctx)) {
|
|
656
|
+
return OmpErrors.single(
|
|
657
|
+
[],
|
|
658
|
+
ctx.expectation ?? "valid (narrow predicate failed)",
|
|
659
|
+
out,
|
|
660
|
+
meta.errorConfig,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
out = step.fn(out, ctx);
|
|
665
|
+
if (out instanceof OmpErrors) {
|
|
666
|
+
return meta.errorConfig === undefined ? out : out.configure(meta.errorConfig);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const self = callable as InternalType;
|
|
161
674
|
self[IR_BRAND] = true;
|
|
675
|
+
self[kBase] = base;
|
|
676
|
+
self[kSteps] = steps;
|
|
162
677
|
self.ir = ir;
|
|
163
678
|
self.hasSteps = steps.length > 0;
|
|
164
679
|
self.hasDefault = meta.hasDefault === true;
|
|
165
680
|
self.defaultValue = meta.defaultValue;
|
|
166
681
|
self.description = meta.description;
|
|
682
|
+
self.errorConfig = meta.errorConfig;
|
|
167
683
|
self.run = callable;
|
|
168
|
-
|
|
169
|
-
// tool wrappers use that distinction to bind executable methods while passing
|
|
170
|
-
// callable parameter schemas through unchanged; exposing bind here would turn
|
|
171
|
-
// the schema into a bare bound function and strip its schema surface.
|
|
172
|
-
Object.defineProperty(self, "bind", { value: undefined });
|
|
173
|
-
|
|
174
|
-
const methods: Omit<FluentType, keyof typeof self | "infer" | "inferIn" | typeof IR_BRAND> = {
|
|
175
|
-
describe: (description: string) => makeType({ ...ir, desc: description }, steps, { ...meta, description }),
|
|
176
|
-
default: (value: unknown) => makeType(ir, steps, { ...meta, defaultValue: value, hasDefault: true }),
|
|
177
|
-
or: (def: unknown) => {
|
|
178
|
-
const other = parseDef(def as Def);
|
|
179
|
-
const a = embed(self);
|
|
180
|
-
const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
|
|
181
|
-
return makeType({ k: "union", members }, [], {});
|
|
182
|
-
},
|
|
183
|
-
and: (def: unknown) => makeType(intersect(embed(self), parseDef(def as Def)), [], {}),
|
|
184
|
-
array: () => makeType({ k: "array", el: embed(self) }, [], {}),
|
|
185
|
-
atLeastLength: (bound: number) => makeType(withLengthBound(ir, "min", bound), steps, meta),
|
|
186
|
-
atMostLength: (bound: number) => makeType(withLengthBound(ir, "max", bound), steps, meta),
|
|
187
|
-
atLeast: (bound: number) => makeType(withNumericBound(ir, "min", bound), steps, meta),
|
|
188
|
-
atMost: (bound: number) => makeType(withNumericBound(ir, "max", bound), steps, meta),
|
|
189
|
-
pipe: (fn: (data: never, ctx: NarrowContext) => unknown) => makeType(ir, [...steps, { kind: "pipe", fn }], meta),
|
|
190
|
-
narrow: (fn: (data: never, ctx: NarrowContext) => boolean) =>
|
|
191
|
-
makeType(ir, [...steps, { kind: "narrow", fn }], meta),
|
|
192
|
-
allows: (data: unknown): boolean => {
|
|
193
|
-
const out = impl(data);
|
|
194
|
-
if (out instanceof OmpErrors) return false;
|
|
195
|
-
for (const step of steps) {
|
|
196
|
-
if (step.kind !== "narrow") continue;
|
|
197
|
-
const fn = step.fn as (data: unknown, ctx: NarrowContext) => unknown;
|
|
198
|
-
if (!fn(out, new Ctx())) return false;
|
|
199
|
-
}
|
|
200
|
-
return true;
|
|
201
|
-
},
|
|
202
|
-
assert: (data: unknown): unknown => {
|
|
203
|
-
const out = callable(data);
|
|
204
|
-
if (out instanceof OmpErrors) throw new TraversalError(out);
|
|
205
|
-
return out;
|
|
206
|
-
},
|
|
207
|
-
toJsonSchema: (_options?: ToJsonSchemaOptions) =>
|
|
208
|
-
irToJsonSchema(ir, meta.description === undefined ? undefined : { description: meta.description }),
|
|
209
|
-
} as never;
|
|
210
|
-
Object.assign(self, methods);
|
|
684
|
+
Object.setPrototypeOf(self, typeMethods);
|
|
211
685
|
return self;
|
|
212
686
|
}
|
|
213
687
|
|
|
214
|
-
|
|
688
|
+
type ObjectIR = Extract<IR, { k: "object" }>;
|
|
689
|
+
|
|
690
|
+
function requireObject(ir: IR, operation: string): ObjectIR {
|
|
691
|
+
if (ir.k !== "object") throw new OmpTypeError(`${operation} requires an object schema`);
|
|
692
|
+
return ir;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function propertyFromIR(prop: PropIR): TypeProperty {
|
|
696
|
+
return {
|
|
697
|
+
kind: prop.opt ? "optional" : "required",
|
|
698
|
+
key: prop.key,
|
|
699
|
+
value: makeType(prop.val, [], {}) as unknown as FluentType<unknown>,
|
|
700
|
+
...(prop.hasDefault ? { default: prop.def } : {}),
|
|
701
|
+
meta: {},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function propertyToIR(property: TypeProperty): PropIR {
|
|
706
|
+
if (property.kind !== "required" && property.kind !== "optional") {
|
|
707
|
+
throw new OmpTypeError(`mapped property ${String(property.key)} has invalid kind`);
|
|
708
|
+
}
|
|
709
|
+
if (!(IR_BRAND in property.value)) {
|
|
710
|
+
throw new OmpTypeError(`mapped property ${String(property.key)} must contain a schema value`);
|
|
711
|
+
}
|
|
712
|
+
const hasDefault = Object.hasOwn(property, "default");
|
|
713
|
+
return {
|
|
714
|
+
key: String(property.key),
|
|
715
|
+
opt: property.kind === "optional",
|
|
716
|
+
val: embed(property.value),
|
|
717
|
+
...(hasDefault
|
|
718
|
+
? { def: property.default, defFactory: typeof property.default === "function", hasDefault: true }
|
|
719
|
+
: {}),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function dateRefinement(
|
|
724
|
+
schema: InternalType,
|
|
725
|
+
bound: Date,
|
|
726
|
+
relation: string,
|
|
727
|
+
predicate: (value: number) => boolean,
|
|
728
|
+
): InternalType {
|
|
729
|
+
if (!Number.isFinite(bound.valueOf())) throw new OmpTypeError("date bound must be valid");
|
|
730
|
+
return makeType(
|
|
731
|
+
{
|
|
732
|
+
k: "refine",
|
|
733
|
+
base: schema.ir,
|
|
734
|
+
pred: value => value instanceof Date && predicate(value.valueOf()),
|
|
735
|
+
expected: `a Date ${relation} ${bound.toISOString()}`,
|
|
736
|
+
json: relation.includes("after") ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
|
|
737
|
+
},
|
|
738
|
+
schema[kSteps],
|
|
739
|
+
metaOf(schema),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function selectNodes(root: IR, kind: string): readonly SelectedNode[] {
|
|
744
|
+
const selected: SelectedNode[] = [];
|
|
745
|
+
const seen = new Set<IR>();
|
|
746
|
+
const visit = (node: IR): void => {
|
|
747
|
+
if (seen.has(node)) return;
|
|
748
|
+
seen.add(node);
|
|
749
|
+
const nodeKind = node.k === "lit" ? "unit" : node.k;
|
|
750
|
+
if (kind === nodeKind || kind === node.k) {
|
|
751
|
+
selected.push(node.k === "lit" ? { kind: nodeKind, node, unit: node.v } : { kind: nodeKind, node });
|
|
752
|
+
}
|
|
753
|
+
switch (node.k) {
|
|
754
|
+
case "alias":
|
|
755
|
+
visit(node.resolve());
|
|
756
|
+
break;
|
|
757
|
+
case "array":
|
|
758
|
+
visit(node.el);
|
|
759
|
+
break;
|
|
760
|
+
case "tuple":
|
|
761
|
+
for (const item of node.prefix) visit(item.val);
|
|
762
|
+
if (node.variadic !== undefined) visit(node.variadic);
|
|
763
|
+
for (const item of node.postfix) visit(item);
|
|
764
|
+
break;
|
|
765
|
+
case "object":
|
|
766
|
+
for (const prop of node.props) visit(prop.val);
|
|
767
|
+
if (node.index !== undefined) visit(node.index);
|
|
768
|
+
break;
|
|
769
|
+
case "union":
|
|
770
|
+
case "intersection":
|
|
771
|
+
for (const member of node.members) visit(member);
|
|
772
|
+
break;
|
|
773
|
+
case "refine":
|
|
774
|
+
visit(node.base);
|
|
775
|
+
break;
|
|
776
|
+
case "morph":
|
|
777
|
+
visit(node.input);
|
|
778
|
+
if (node.out !== undefined) visit(node.out);
|
|
779
|
+
break;
|
|
780
|
+
case "sub":
|
|
781
|
+
visit(node.schema.ir);
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
visit(root);
|
|
786
|
+
return selected;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function mergeObjects(left: ObjectIR, right: ObjectIR): ObjectIR {
|
|
790
|
+
const props = [...left.props];
|
|
791
|
+
for (const prop of right.props) {
|
|
792
|
+
const index = props.findIndex(candidate => candidate.key === prop.key);
|
|
793
|
+
if (index < 0) props.push(prop);
|
|
794
|
+
else props[index] = prop;
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
k: "object",
|
|
798
|
+
props,
|
|
799
|
+
index: right.index ?? left.index,
|
|
800
|
+
extras: right.extras === "keep" ? left.extras : right.extras,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
|
|
805
|
+
switch (ir.k) {
|
|
806
|
+
case "object":
|
|
807
|
+
return {
|
|
808
|
+
...ir,
|
|
809
|
+
extras,
|
|
810
|
+
props: ir.props.map(prop => ({ ...prop, val: withDeepExtras(prop.val, extras) })),
|
|
811
|
+
index: ir.index === undefined ? undefined : withDeepExtras(ir.index, extras),
|
|
812
|
+
};
|
|
813
|
+
case "array":
|
|
814
|
+
return { ...ir, el: withDeepExtras(ir.el, extras) };
|
|
815
|
+
case "tuple":
|
|
816
|
+
return {
|
|
817
|
+
...ir,
|
|
818
|
+
prefix: ir.prefix.map(item => ({ ...item, val: withDeepExtras(item.val, extras) })),
|
|
819
|
+
variadic: ir.variadic === undefined ? undefined : withDeepExtras(ir.variadic, extras),
|
|
820
|
+
postfix: ir.postfix.map(item => withDeepExtras(item, extras)),
|
|
821
|
+
};
|
|
822
|
+
case "union":
|
|
823
|
+
case "intersection":
|
|
824
|
+
return { ...ir, members: ir.members.map(member => withDeepExtras(member, extras)) };
|
|
825
|
+
case "refine":
|
|
826
|
+
return { ...ir, base: withDeepExtras(ir.base, extras) };
|
|
827
|
+
case "morph":
|
|
828
|
+
return {
|
|
829
|
+
...ir,
|
|
830
|
+
input: withDeepExtras(ir.input, extras),
|
|
831
|
+
out: ir.out === undefined ? undefined : withDeepExtras(ir.out, extras),
|
|
832
|
+
};
|
|
833
|
+
default:
|
|
834
|
+
return ir;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Intersect two IR nodes, rejecting statically disjoint domains. */
|
|
215
839
|
function intersect(a: IR, b: IR): IR {
|
|
840
|
+
if (a.k === "alias") return intersect(a.resolve(), b);
|
|
841
|
+
if (b.k === "alias") return intersect(a, b.resolve());
|
|
842
|
+
if (a.k === "never" || b.k === "never") throw new OmpTypeError("intersection with never is unsatisfiable");
|
|
843
|
+
if (a.k === "unknown") return b;
|
|
844
|
+
if (b.k === "unknown") return a;
|
|
845
|
+
if (a === b) return a;
|
|
846
|
+
if (a.k === "union" || b.k === "union") {
|
|
847
|
+
const union = a.k === "union" ? a : b.k === "union" ? b : undefined;
|
|
848
|
+
if (union === undefined) throw new OmpTypeError("union intersection invariant failed");
|
|
849
|
+
const branches = union.members;
|
|
850
|
+
const other = a.k === "union" ? b : a;
|
|
851
|
+
const members: IR[] = [];
|
|
852
|
+
for (const branch of branches) {
|
|
853
|
+
try {
|
|
854
|
+
members.push(intersect(branch, other));
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (!(error instanceof OmpTypeError)) throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (members.length === 0) throw new OmpTypeError("intersection has no satisfiable branches");
|
|
860
|
+
return members.length === 1 ? members[0] : { k: "union", members };
|
|
861
|
+
}
|
|
862
|
+
if (a.k === "lit") {
|
|
863
|
+
if (walk(b, a.v) instanceof OmpErrors) throw new OmpTypeError("literal is excluded by the intersection");
|
|
864
|
+
return a;
|
|
865
|
+
}
|
|
866
|
+
if (b.k === "lit") return intersect(b, a);
|
|
216
867
|
if (a.k === "object" && b.k === "object") {
|
|
217
868
|
const props = [...a.props];
|
|
218
869
|
for (const bp of b.props) {
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
props[i] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
|
|
870
|
+
const index = props.findIndex(prop => prop.key === bp.key);
|
|
871
|
+
if (index < 0) props.push(bp);
|
|
872
|
+
else {
|
|
873
|
+
const ap = props[index];
|
|
874
|
+
props[index] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
|
|
225
875
|
}
|
|
226
876
|
}
|
|
227
877
|
const extras =
|
|
@@ -234,22 +884,213 @@ function intersect(a: IR, b: IR): IR {
|
|
|
234
884
|
return { k: "object", props, index, extras };
|
|
235
885
|
}
|
|
236
886
|
if (a.k === "string" && b.k === "string") {
|
|
237
|
-
|
|
887
|
+
const min = maxOf(a.min, b.min);
|
|
888
|
+
const max = minOf(a.max, b.max);
|
|
889
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
890
|
+
throw new OmpTypeError("string length intersection is unsatisfiable");
|
|
891
|
+
}
|
|
892
|
+
return { k: "string", min, max, url: a.url || b.url };
|
|
238
893
|
}
|
|
239
894
|
if (a.k === "number" && b.k === "number") {
|
|
895
|
+
const min = maxOf(a.min, b.min);
|
|
896
|
+
const max = minOf(a.max, b.max);
|
|
897
|
+
const xmin = min !== undefined && ((a.min === min && a.xmin === true) || (b.min === min && b.xmin === true));
|
|
898
|
+
const xmax = max !== undefined && ((a.max === max && a.xmax === true) || (b.max === max && b.xmax === true));
|
|
899
|
+
if (min !== undefined && max !== undefined && (min > max || (min === max && (xmin || xmax)))) {
|
|
900
|
+
throw new OmpTypeError("numeric range intersection is unsatisfiable");
|
|
901
|
+
}
|
|
902
|
+
if (a.divisor !== undefined && b.divisor !== undefined && a.divisor !== b.divisor) {
|
|
903
|
+
return { k: "intersection", members: [a, b] };
|
|
904
|
+
}
|
|
240
905
|
return {
|
|
241
906
|
k: "number",
|
|
242
907
|
int: a.int || b.int,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
908
|
+
divisor: a.divisor ?? b.divisor,
|
|
909
|
+
min,
|
|
910
|
+
max,
|
|
911
|
+
xmin,
|
|
912
|
+
xmax,
|
|
247
913
|
};
|
|
248
914
|
}
|
|
249
|
-
if (a.k === "
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
915
|
+
if (a.k === "array" && b.k === "array") {
|
|
916
|
+
const min = maxOf(a.min, b.min);
|
|
917
|
+
const max = minOf(a.max, b.max);
|
|
918
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
919
|
+
throw new OmpTypeError("array length intersection is unsatisfiable");
|
|
920
|
+
}
|
|
921
|
+
return { k: "array", el: intersect(a.el, b.el), min, max };
|
|
922
|
+
}
|
|
923
|
+
if (a.k === "instance" && b.k === "instance") {
|
|
924
|
+
if (a.ctor === b.ctor || a.ctor.prototype instanceof b.ctor) return a;
|
|
925
|
+
if (b.ctor.prototype instanceof a.ctor) return b;
|
|
926
|
+
throw new OmpTypeError(`intersection of ${a.expected} and ${b.expected} is unsatisfiable`);
|
|
927
|
+
}
|
|
928
|
+
if (a.k === b.k && ["null", "undefined", "boolean", "bigint", "symbol", "anyobject"].includes(a.k)) return a;
|
|
929
|
+
const leftDomain = domainOf(a);
|
|
930
|
+
const rightDomain = domainOf(b);
|
|
931
|
+
if (leftDomain !== undefined && rightDomain !== undefined && leftDomain !== rightDomain) {
|
|
932
|
+
throw new OmpTypeError(`intersection of ${leftDomain} and ${rightDomain} is unsatisfiable`);
|
|
933
|
+
}
|
|
934
|
+
if (a.k === "anyobject" && rightDomain === "object") return b;
|
|
935
|
+
if (b.k === "anyobject" && leftDomain === "object") return a;
|
|
936
|
+
const members = [...(a.k === "intersection" ? a.members : [a]), ...(b.k === "intersection" ? b.members : [b])];
|
|
937
|
+
return { k: "intersection", members };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function domainOf(ir: IR): string | undefined {
|
|
941
|
+
switch (ir.k) {
|
|
942
|
+
case "null":
|
|
943
|
+
return "null";
|
|
944
|
+
case "undefined":
|
|
945
|
+
case "boolean":
|
|
946
|
+
case "bigint":
|
|
947
|
+
case "symbol":
|
|
948
|
+
case "string":
|
|
949
|
+
case "number":
|
|
950
|
+
return ir.k;
|
|
951
|
+
case "array":
|
|
952
|
+
case "tuple":
|
|
953
|
+
return "array";
|
|
954
|
+
case "object":
|
|
955
|
+
case "anyobject":
|
|
956
|
+
case "instance":
|
|
957
|
+
return "object";
|
|
958
|
+
case "lit":
|
|
959
|
+
return ir.v === null ? "null" : Array.isArray(ir.v) ? "array" : typeof ir.v;
|
|
960
|
+
case "refine":
|
|
961
|
+
return domainOf(ir.base);
|
|
962
|
+
case "morph":
|
|
963
|
+
return domainOf(ir.input);
|
|
964
|
+
case "sub":
|
|
965
|
+
return domainOf(ir.schema.ir);
|
|
966
|
+
case "alias":
|
|
967
|
+
return domainOf(ir.resolve());
|
|
968
|
+
case "union":
|
|
969
|
+
case "intersection": {
|
|
970
|
+
const first = domainOf(ir.members[0] ?? { k: "never" });
|
|
971
|
+
return ir.members.every(member => domainOf(member) === first) ? first : undefined;
|
|
972
|
+
}
|
|
973
|
+
default:
|
|
974
|
+
return undefined;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function lowerBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
|
|
978
|
+
if (target.min === undefined) return true;
|
|
979
|
+
if (source.min === undefined || source.min < target.min) return false;
|
|
980
|
+
return source.min !== target.min || target.xmin !== true || source.xmin === true;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function upperBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
|
|
984
|
+
if (target.max === undefined) return true;
|
|
985
|
+
if (source.max === undefined || source.max > target.max) return false;
|
|
986
|
+
return source.max !== target.max || target.xmax !== true || source.xmax === true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function lengthWithin(
|
|
990
|
+
source: Extract<IR, { k: "string" | "array" }>,
|
|
991
|
+
target: Extract<IR, { k: "string" | "array" }>,
|
|
992
|
+
): boolean {
|
|
993
|
+
return (
|
|
994
|
+
(target.min === undefined || (source.min !== undefined && source.min >= target.min)) &&
|
|
995
|
+
(target.max === undefined || (source.max !== undefined && source.max <= target.max))
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function isSubtype(source: IR, target: IR, seen = new WeakMap<IR, Set<IR>>()): boolean {
|
|
1000
|
+
if (source === target || target.k === "unknown" || source.k === "never") return true;
|
|
1001
|
+
let targets = seen.get(source);
|
|
1002
|
+
if (targets?.has(target)) return true;
|
|
1003
|
+
if (targets === undefined) {
|
|
1004
|
+
targets = new Set();
|
|
1005
|
+
seen.set(source, targets);
|
|
1006
|
+
}
|
|
1007
|
+
targets.add(target);
|
|
1008
|
+
if (source.k === "alias") return isSubtype(source.resolve(), target, seen);
|
|
1009
|
+
if (target.k === "alias") return isSubtype(source, target.resolve(), seen);
|
|
1010
|
+
if (source.k === "union") return source.members.every(member => isSubtype(member, target, seen));
|
|
1011
|
+
if (target.k === "union") return target.members.some(member => isSubtype(source, member, seen));
|
|
1012
|
+
if (target.k === "intersection") return target.members.every(member => isSubtype(source, member, seen));
|
|
1013
|
+
if (source.k === "intersection") return source.members.some(member => isSubtype(member, target, seen));
|
|
1014
|
+
if (source.k === "lit") return !(walk(target, source.v) instanceof OmpErrors);
|
|
1015
|
+
if (source.k === "refine") return isSubtype(source.base, target, seen);
|
|
1016
|
+
if (source.k === "morph") return isSubtype(source.out ?? source.input, target, seen);
|
|
1017
|
+
if (source.k === "sub") return isSubtype(source.schema.ir, target, seen);
|
|
1018
|
+
if (target.k === "refine" || target.k === "morph" || target.k === "sub") return false;
|
|
1019
|
+
if (source.k === "string" && target.k === "string") {
|
|
1020
|
+
return lengthWithin(source, target) && (!target.url || source.url === true);
|
|
1021
|
+
}
|
|
1022
|
+
if (source.k === "number" && target.k === "number") {
|
|
1023
|
+
return (
|
|
1024
|
+
lowerBoundWithin(source, target) &&
|
|
1025
|
+
upperBoundWithin(source, target) &&
|
|
1026
|
+
(!target.int || source.int === true) &&
|
|
1027
|
+
(target.divisor === undefined || (source.divisor !== undefined && source.divisor % target.divisor === 0))
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
if (source.k === "array" && target.k === "array") {
|
|
1031
|
+
return lengthWithin(source, target) && isSubtype(source.el, target.el, seen);
|
|
1032
|
+
}
|
|
1033
|
+
if (source.k === "object" && target.k === "object") {
|
|
1034
|
+
for (const targetProp of target.props) {
|
|
1035
|
+
const sourceProp = source.props.find(prop => prop.key === targetProp.key);
|
|
1036
|
+
if (sourceProp === undefined) {
|
|
1037
|
+
if (!targetProp.opt) return false;
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (!targetProp.opt && sourceProp.opt) return false;
|
|
1041
|
+
if (!isSubtype(sourceProp.val, targetProp.val, seen)) return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (target.extras === "reject") {
|
|
1044
|
+
if (source.extras !== "reject") return false;
|
|
1045
|
+
if (
|
|
1046
|
+
target.index === undefined &&
|
|
1047
|
+
source.props.some(sourceProp => !target.props.some(targetProp => targetProp.key === sourceProp.key))
|
|
1048
|
+
) {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
if (source.k === "instance" && target.k === "instance") {
|
|
1055
|
+
return source.ctor === target.ctor || source.ctor.prototype instanceof target.ctor;
|
|
1056
|
+
}
|
|
1057
|
+
if (source.k === "object" && target.k === "anyobject") return true;
|
|
1058
|
+
if (source.k === "instance" && target.k === "anyobject") return true;
|
|
1059
|
+
if (source.k === "tuple" && target.k === "array") {
|
|
1060
|
+
return (
|
|
1061
|
+
source.prefix.every(item => isSubtype(item.val, target.el, seen)) &&
|
|
1062
|
+
source.postfix.every(item => isSubtype(item, target.el, seen)) &&
|
|
1063
|
+
(source.variadic === undefined || isSubtype(source.variadic, target.el, seen))
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (source.k !== target.k) return false;
|
|
1067
|
+
switch (source.k) {
|
|
1068
|
+
case "null":
|
|
1069
|
+
case "undefined":
|
|
1070
|
+
case "boolean":
|
|
1071
|
+
case "bigint":
|
|
1072
|
+
case "symbol":
|
|
1073
|
+
case "anyobject":
|
|
1074
|
+
return true;
|
|
1075
|
+
case "tuple":
|
|
1076
|
+
return target.k === "tuple" && expectedTuple(source) === expectedTuple(target);
|
|
1077
|
+
case "instance":
|
|
1078
|
+
return target.k === "instance" && source.ctor === target.ctor;
|
|
1079
|
+
default:
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function expectedTuple(tuple: Extract<IR, { k: "tuple" }>): string {
|
|
1085
|
+
return JSON.stringify({
|
|
1086
|
+
prefix: tuple.prefix.map(item => [item.opt, item.hasDefault, expectedOf(item.val)]),
|
|
1087
|
+
variadic: tuple.variadic === undefined ? undefined : expectedOf(tuple.variadic),
|
|
1088
|
+
postfix: tuple.postfix.map(expectedOf),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function irEquals(left: IR, right: IR): boolean {
|
|
1093
|
+
return isSubtype(left, right) && isSubtype(right, left);
|
|
253
1094
|
}
|
|
254
1095
|
|
|
255
1096
|
function maxOf(a: number | undefined, b: number | undefined): number | undefined {
|
|
@@ -270,21 +1111,131 @@ function withLengthBound(ir: IR, side: "min" | "max", bound: number): IR {
|
|
|
270
1111
|
}
|
|
271
1112
|
throw new OmpTypeError(`cannot apply length bound to ${ir.k}`);
|
|
272
1113
|
}
|
|
273
|
-
function withNumericBound(ir: IR, side: "min" | "max", bound: number): IR {
|
|
1114
|
+
function withNumericBound(ir: IR, side: "min" | "max", bound: number, exclusive = false): IR {
|
|
274
1115
|
if (ir.k === "number") {
|
|
275
|
-
return side === "min" ? { ...ir, min: bound, xmin:
|
|
1116
|
+
return side === "min" ? { ...ir, min: bound, xmin: exclusive } : { ...ir, max: bound, xmax: exclusive };
|
|
276
1117
|
}
|
|
277
1118
|
throw new OmpTypeError(`cannot apply numeric bound to ${ir.k}`);
|
|
278
1119
|
}
|
|
279
1120
|
|
|
280
1121
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* The `const def` generic drives static inference (`typeof schema.infer`);
|
|
284
|
-
* the runtime is definition-shape-agnostic, hence the cast.
|
|
1122
|
+
* Parse a definition into a callable schema with distinct input/output inference.
|
|
285
1123
|
*/
|
|
286
|
-
export function type<const def>(def: def): FluentType<InferDef<def>> {
|
|
287
|
-
return makeType(parseDef(def
|
|
1124
|
+
export function type<const def>(def: def): FluentType<InferDef<def>, InferDefIn<def>> {
|
|
1125
|
+
return makeType<InferDef<def>, InferDefIn<def>>(parseDef(def), EMPTY_STEPS, EMPTY_META);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** String keyword with a parser that morphs validated text to another output. */
|
|
1129
|
+
export interface ParsedStringKeyword<parsed> extends FluentType<string> {
|
|
1130
|
+
readonly parse: FluentType<parsed, string>;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** Morphing string keyword paired with its non-morphing preformatted validator. */
|
|
1134
|
+
export interface PreformattedKeyword extends FluentType<string, string> {
|
|
1135
|
+
readonly preformatted: FluentType<string>;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** Base64 keyword with its URL-safe alphabet variant. */
|
|
1139
|
+
export interface Base64Keyword extends FluentType<string> {
|
|
1140
|
+
readonly url: FluentType<string>;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/** Date-string keyword family. */
|
|
1144
|
+
export interface DateStringKeyword extends ParsedStringKeyword<Date> {
|
|
1145
|
+
readonly iso: ParsedStringKeyword<Date>;
|
|
1146
|
+
readonly epoch: ParsedStringKeyword<Date>;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** IP address keyword family. */
|
|
1150
|
+
export interface IpKeyword extends FluentType<string> {
|
|
1151
|
+
readonly v4: FluentType<string>;
|
|
1152
|
+
readonly v6: FluentType<string>;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** UUID keyword family. */
|
|
1156
|
+
export interface UuidKeyword extends FluentType<string> {
|
|
1157
|
+
readonly v1: FluentType<string>;
|
|
1158
|
+
readonly v2: FluentType<string>;
|
|
1159
|
+
readonly v3: FluentType<string>;
|
|
1160
|
+
readonly v4: FluentType<string>;
|
|
1161
|
+
readonly v5: FluentType<string>;
|
|
1162
|
+
readonly v6: FluentType<string>;
|
|
1163
|
+
readonly v7: FluentType<string>;
|
|
1164
|
+
readonly v8: FluentType<string>;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** String normalization keyword family. */
|
|
1168
|
+
export interface NormalizeKeyword extends PreformattedKeyword {
|
|
1169
|
+
readonly NFC: PreformattedKeyword;
|
|
1170
|
+
readonly NFD: PreformattedKeyword;
|
|
1171
|
+
readonly NFKC: PreformattedKeyword;
|
|
1172
|
+
readonly NFKD: PreformattedKeyword;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** Runtime string parsers exposed under `type.parse`. */
|
|
1176
|
+
export interface ParseKeyword {
|
|
1177
|
+
readonly number: FluentType<number, string>;
|
|
1178
|
+
readonly integer: FluentType<number, string>;
|
|
1179
|
+
readonly json: FluentType<unknown, string>;
|
|
1180
|
+
readonly date: FluentType<Date, string>;
|
|
1181
|
+
readonly url: FluentType<URL, string>;
|
|
1182
|
+
readonly boolean: FluentType<boolean, string>;
|
|
1183
|
+
readonly bigint: FluentType<bigint, string>;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/** Full string keyword module attached to `type.string`. */
|
|
1187
|
+
export interface StringKeyword extends FluentType<string> {
|
|
1188
|
+
readonly alpha: FluentType<string>;
|
|
1189
|
+
readonly alphanumeric: FluentType<string>;
|
|
1190
|
+
readonly base64: Base64Keyword;
|
|
1191
|
+
readonly capitalize: PreformattedKeyword;
|
|
1192
|
+
readonly creditCard: FluentType<string>;
|
|
1193
|
+
readonly date: DateStringKeyword;
|
|
1194
|
+
readonly digits: FluentType<string>;
|
|
1195
|
+
readonly email: FluentType<string>;
|
|
1196
|
+
readonly hex: FluentType<string>;
|
|
1197
|
+
readonly integer: ParsedStringKeyword<number>;
|
|
1198
|
+
readonly ip: IpKeyword;
|
|
1199
|
+
readonly json: ParsedStringKeyword<unknown>;
|
|
1200
|
+
readonly lower: PreformattedKeyword;
|
|
1201
|
+
readonly normalize: NormalizeKeyword;
|
|
1202
|
+
readonly numeric: ParsedStringKeyword<number>;
|
|
1203
|
+
readonly regex: FluentType<string>;
|
|
1204
|
+
readonly semver: FluentType<string>;
|
|
1205
|
+
readonly trim: PreformattedKeyword;
|
|
1206
|
+
readonly upper: PreformattedKeyword;
|
|
1207
|
+
readonly url: ParsedStringKeyword<URL>;
|
|
1208
|
+
readonly uuid: UuidKeyword;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** Number keyword module attached to `type.number`. */
|
|
1212
|
+
export interface NumberKeyword extends FluentType<number> {
|
|
1213
|
+
readonly integer: FluentType<number>;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
type Constructed<ctor> = ctor extends abstract new (...args: never[]) => infer instance ? instance : never;
|
|
1217
|
+
|
|
1218
|
+
function keywordSchema<output = string, input = output>(name: string): FluentType<output, input> {
|
|
1219
|
+
const ir = keywordIR(name);
|
|
1220
|
+
if (ir === undefined) throw new OmpTypeError(`missing built-in keyword ${name}`);
|
|
1221
|
+
return makeType<output, input>(ir, [], {});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function parsedKeyword<parsed>(name: string): ParsedStringKeyword<parsed> {
|
|
1225
|
+
return Object.assign(keywordSchema<string>(name), {
|
|
1226
|
+
parse: keywordSchema<parsed, string>(`${name}.parse`),
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function preformattedKeyword(name: string): PreformattedKeyword {
|
|
1231
|
+
return Object.assign(keywordSchema<string, string>(name), {
|
|
1232
|
+
preformatted: keywordSchema(`${name}.preformatted`),
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function caseResolver(value: unknown): (input: unknown) => unknown {
|
|
1237
|
+
if (typeof value !== "function") throw new OmpTypeError("match case values must be functions");
|
|
1238
|
+
return input => Reflect.apply(value, undefined, [input]);
|
|
288
1239
|
}
|
|
289
1240
|
|
|
290
1241
|
export namespace type {
|
|
@@ -292,38 +1243,242 @@ export namespace type {
|
|
|
292
1243
|
export const errors = OmpErrors;
|
|
293
1244
|
export type errors = OmpErrors;
|
|
294
1245
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1246
|
+
const normalize: NormalizeKeyword = Object.assign(keywordSchema<string, string>("string.normalize"), {
|
|
1247
|
+
preformatted: keywordSchema("string.normalize.NFC.preformatted"),
|
|
1248
|
+
NFC: preformattedKeyword("string.normalize.NFC"),
|
|
1249
|
+
NFD: preformattedKeyword("string.normalize.NFD"),
|
|
1250
|
+
NFKC: preformattedKeyword("string.normalize.NFKC"),
|
|
1251
|
+
NFKD: preformattedKeyword("string.normalize.NFKD"),
|
|
1252
|
+
});
|
|
1253
|
+
const base64: Base64Keyword = Object.assign(keywordSchema("string.base64"), {
|
|
1254
|
+
url: keywordSchema("string.base64.url"),
|
|
1255
|
+
});
|
|
1256
|
+
const date: DateStringKeyword = Object.assign(parsedKeyword<Date>("string.date"), {
|
|
1257
|
+
iso: parsedKeyword<Date>("string.date.iso"),
|
|
1258
|
+
epoch: parsedKeyword<Date>("string.date.epoch"),
|
|
1259
|
+
});
|
|
1260
|
+
const ip: IpKeyword = Object.assign(keywordSchema("string.ip"), {
|
|
1261
|
+
v4: keywordSchema("string.ip.v4"),
|
|
1262
|
+
v6: keywordSchema("string.ip.v6"),
|
|
1263
|
+
});
|
|
1264
|
+
const uuid: UuidKeyword = Object.assign(keywordSchema("string.uuid"), {
|
|
1265
|
+
v1: keywordSchema("string.uuid.v1"),
|
|
1266
|
+
v2: keywordSchema("string.uuid.v2"),
|
|
1267
|
+
v3: keywordSchema("string.uuid.v3"),
|
|
1268
|
+
v4: keywordSchema("string.uuid.v4"),
|
|
1269
|
+
v5: keywordSchema("string.uuid.v5"),
|
|
1270
|
+
v6: keywordSchema("string.uuid.v6"),
|
|
1271
|
+
v7: keywordSchema("string.uuid.v7"),
|
|
1272
|
+
v8: keywordSchema("string.uuid.v8"),
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
/** String validator and its refinement/morph keyword module. */
|
|
1276
|
+
export const string: StringKeyword = Object.assign(makeType<string>({ k: "string" }, [], {}), {
|
|
1277
|
+
alpha: keywordSchema("string.alpha"),
|
|
1278
|
+
alphanumeric: keywordSchema("string.alphanumeric"),
|
|
1279
|
+
base64,
|
|
1280
|
+
capitalize: preformattedKeyword("string.capitalize"),
|
|
1281
|
+
creditCard: keywordSchema("string.creditCard"),
|
|
1282
|
+
date,
|
|
1283
|
+
digits: keywordSchema("string.digits"),
|
|
1284
|
+
email: keywordSchema("string.email"),
|
|
1285
|
+
hex: keywordSchema("string.hex"),
|
|
1286
|
+
integer: parsedKeyword<number>("string.integer"),
|
|
1287
|
+
ip,
|
|
1288
|
+
json: parsedKeyword<unknown>("string.json"),
|
|
1289
|
+
lower: preformattedKeyword("string.lower"),
|
|
1290
|
+
normalize,
|
|
1291
|
+
numeric: parsedKeyword<number>("string.numeric"),
|
|
1292
|
+
regex: keywordSchema("string.regex"),
|
|
1293
|
+
semver: keywordSchema("string.semver"),
|
|
1294
|
+
trim: preformattedKeyword("string.trim"),
|
|
1295
|
+
upper: preformattedKeyword("string.upper"),
|
|
1296
|
+
url: parsedKeyword<URL>("string.url"),
|
|
1297
|
+
uuid,
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
/** Runtime parser keyword family. */
|
|
1301
|
+
export const parse: ParseKeyword = {
|
|
1302
|
+
number: keywordSchema<number, string>("parse.number"),
|
|
1303
|
+
integer: keywordSchema<number, string>("parse.integer"),
|
|
1304
|
+
json: keywordSchema<unknown, string>("parse.json"),
|
|
1305
|
+
date: keywordSchema<Date, string>("parse.date"),
|
|
1306
|
+
url: keywordSchema<URL, string>("parse.url"),
|
|
1307
|
+
boolean: keywordSchema<boolean, string>("parse.boolean"),
|
|
1308
|
+
bigint: keywordSchema<bigint, string>("parse.bigint"),
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
/** Number validator with integer refinement. */
|
|
1312
|
+
export const number: NumberKeyword = Object.assign(makeType<number>({ k: "number" }, [], {}), {
|
|
1313
|
+
integer: makeType<number>({ k: "number", int: true }, [], {}),
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
/** Boolean validator. */
|
|
1317
|
+
export const boolean = makeType<boolean>({ k: "boolean" }, [], {});
|
|
1318
|
+
/** Bigint validator. */
|
|
1319
|
+
export const bigint = makeType<bigint>({ k: "bigint" }, [], {});
|
|
1320
|
+
/** Symbol validator. */
|
|
1321
|
+
export const symbol = makeType<symbol>({ k: "symbol" }, [], {});
|
|
1322
|
+
/** Non-null object validator. */
|
|
1323
|
+
export const object = makeType<object>({ k: "anyobject" }, [], {});
|
|
1324
|
+
/** Unknown validator. */
|
|
1325
|
+
export const unknown = makeType<unknown>({ k: "unknown" }, [], {});
|
|
1326
|
+
/** Alias of the unknown validator. */
|
|
1327
|
+
export const any = unknown;
|
|
1328
|
+
/** Validator that rejects every value. */
|
|
1329
|
+
export const never = makeType<never>({ k: "never" }, [], {});
|
|
1330
|
+
/** Date instance validator. */
|
|
1331
|
+
// biome-ignore lint/suspicious/noShadowRestrictedNames: ArkType exposes this exact keyword.
|
|
1332
|
+
export const Date = makeType<globalThis.Date>({ k: "instance", ctor: globalThis.Date, expected: "a Date" }, [], {});
|
|
300
1333
|
|
|
301
|
-
/**
|
|
1334
|
+
/** Validate instances of `ctor`. */
|
|
1335
|
+
export function instanceOf<const ctor extends Constructor>(ctor: ctor): FluentType<Constructed<ctor>> {
|
|
1336
|
+
const name = Reflect.get(ctor, "name");
|
|
1337
|
+
const expected = typeof name === "string" && name.length > 0 ? `an instance of ${name}` : "an instance";
|
|
1338
|
+
return makeType<Constructed<ctor>>({ k: "instance", ctor, expected }, [], {});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/** Validate one exact unit value. */
|
|
1342
|
+
export function unit<const value>(value: value): FluentType<value> {
|
|
1343
|
+
return makeType<value>({ k: "lit", v: value }, [], {});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** Union of literal values from a runtime array. */
|
|
302
1347
|
export function enumerated<const values extends readonly unknown[]>(...values: values): FluentType<values[number]> {
|
|
1348
|
+
const members: IR[] = values.map(value => ({ k: "lit", v: value }));
|
|
303
1349
|
const ir: IR =
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
1350
|
+
members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
|
|
1351
|
+
return makeType<values[number]>(ir, [], {});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** Build a first-match dispatcher from schema-expression keys and a `default` case. */
|
|
1355
|
+
export function match<const cases extends Record<string, unknown>>(cases: cases): (value: unknown) => unknown {
|
|
1356
|
+
const branches: { schema: BaseType; resolve: (value: unknown) => unknown }[] = [];
|
|
1357
|
+
let fallback: ((value: unknown) => unknown) | undefined;
|
|
1358
|
+
for (const definition in cases) {
|
|
1359
|
+
const resolver = caseResolver(cases[definition]);
|
|
1360
|
+
if (definition === "default") fallback = resolver;
|
|
1361
|
+
else branches.push({ schema: raw(definition), resolve: resolver });
|
|
1362
|
+
}
|
|
1363
|
+
return value => {
|
|
1364
|
+
for (const branch of branches) {
|
|
1365
|
+
if (branch.schema.allows(value)) return branch.resolve(value);
|
|
1366
|
+
}
|
|
1367
|
+
if (fallback !== undefined) return fallback(value);
|
|
1368
|
+
throw new OmpTypeError("match requires a matching case or default");
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
/** Preserve a definition's literal type while authoring reusable modules. */
|
|
1372
|
+
export function define<const definition>(definition: definition): definition {
|
|
1373
|
+
return definition;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** Build a lazy named scope from aliases and recursive definitions. */
|
|
1377
|
+
export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
|
|
1378
|
+
return buildScope(aliases, options);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/** Compile a named schema module whose definitions may reference each other. */
|
|
1382
|
+
export function module<const definitions extends Record<string, unknown>>(
|
|
1383
|
+
definitions: definitions,
|
|
1384
|
+
): { [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>> } {
|
|
1385
|
+
return scope(definitions).export() as unknown as {
|
|
1386
|
+
[name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>>;
|
|
1387
|
+
};
|
|
308
1388
|
}
|
|
309
1389
|
|
|
310
|
-
/**
|
|
1390
|
+
/** Build a runtime generic whose parameter names are supplied as `"<t, u>"`. */
|
|
1391
|
+
export function generic<const definition>(
|
|
1392
|
+
parameters: string,
|
|
1393
|
+
definition: definition,
|
|
1394
|
+
): (...arguments_: readonly unknown[]) => BaseType {
|
|
1395
|
+
const names = parameters
|
|
1396
|
+
.replace(/^<|>$/g, "")
|
|
1397
|
+
.split(",")
|
|
1398
|
+
.map(name => name.trim())
|
|
1399
|
+
.filter(Boolean);
|
|
1400
|
+
return (...arguments_) => {
|
|
1401
|
+
if (arguments_.length !== names.length) {
|
|
1402
|
+
throw new OmpTypeError(`generic expects ${names.length} arguments (received ${arguments_.length})`);
|
|
1403
|
+
}
|
|
1404
|
+
const aliases: Record<string, unknown> = {};
|
|
1405
|
+
for (let index = 0; index < names.length; index++) aliases[names[index]] = arguments_[index];
|
|
1406
|
+
return scope(aliases).type(definition) as unknown as BaseType;
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Untyped builder for runtime-assembled definitions. */
|
|
311
1411
|
export function raw(def: unknown): BaseType {
|
|
312
|
-
return makeType(parseDef(def
|
|
1412
|
+
return makeType(parseDef(def), [], {}) as unknown as BaseType;
|
|
313
1413
|
}
|
|
314
1414
|
}
|
|
315
1415
|
|
|
1416
|
+
// Reserved words cannot be declared as namespace bindings, but ArkType exposes
|
|
1417
|
+
// them as runtime keyword properties.
|
|
1418
|
+
Object.assign(type, {
|
|
1419
|
+
null: makeType<null>({ k: "null" }, [], {}),
|
|
1420
|
+
undefined: makeType<undefined>({ k: "undefined" }, [], {}),
|
|
1421
|
+
true: makeType<true>({ k: "lit", v: true }, [], {}),
|
|
1422
|
+
false: makeType<false>({ k: "lit", v: false }, [], {}),
|
|
1423
|
+
});
|
|
1424
|
+
|
|
316
1425
|
export interface ScopeOptions {
|
|
317
1426
|
jitless?: boolean;
|
|
318
1427
|
}
|
|
319
1428
|
|
|
320
|
-
/**
|
|
321
|
-
export
|
|
322
|
-
|
|
1429
|
+
/** Callable builder bound to one alias scope. */
|
|
1430
|
+
export type ScopedBuilder = <const definition>(
|
|
1431
|
+
definition: definition,
|
|
1432
|
+
) => FluentType<InferDef<definition>, InferDefIn<definition>>;
|
|
1433
|
+
|
|
1434
|
+
/** Named schema scope with a scoped builder and compiled module export. */
|
|
1435
|
+
export interface TypeScope {
|
|
1436
|
+
readonly type: ScopedBuilder;
|
|
1437
|
+
export(): Record<string, BaseType>;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/** Build a scope whose aliases resolve lazily, including recursive cycles. */
|
|
1441
|
+
export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
|
|
1442
|
+
return buildScope(aliases, options);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function buildScope(aliases: Record<string, unknown>, _options?: ScopeOptions): TypeScope {
|
|
1446
|
+
const references = new Map<string, IR>();
|
|
1447
|
+
const targets = new Map<string, IR>();
|
|
1448
|
+
const resolve: AliasResolver = name => {
|
|
1449
|
+
if (!Object.hasOwn(aliases, name)) return undefined;
|
|
1450
|
+
const existing = references.get(name);
|
|
1451
|
+
if (existing !== undefined) return existing;
|
|
1452
|
+
const reference: IR = {
|
|
1453
|
+
k: "alias",
|
|
1454
|
+
name,
|
|
1455
|
+
resolve: () => {
|
|
1456
|
+
const target = targets.get(name);
|
|
1457
|
+
if (target !== undefined) return target;
|
|
1458
|
+
const parsed = parseDef(aliases[name], resolve);
|
|
1459
|
+
targets.set(name, parsed);
|
|
1460
|
+
return parsed;
|
|
1461
|
+
},
|
|
1462
|
+
};
|
|
1463
|
+
references.set(name, reference);
|
|
1464
|
+
return reference;
|
|
1465
|
+
};
|
|
1466
|
+
const scoped = Object.assign(
|
|
1467
|
+
(definition: unknown) => makeType(parseDef(definition, resolve), [], {}),
|
|
1468
|
+
type,
|
|
1469
|
+
) as unknown as ScopedBuilder;
|
|
1470
|
+
return {
|
|
1471
|
+
type: scoped,
|
|
1472
|
+
export() {
|
|
1473
|
+
const schemas: Record<string, BaseType> = {};
|
|
1474
|
+
for (const name in aliases) schemas[name] = scoped(name) as unknown as BaseType;
|
|
1475
|
+
return schemas;
|
|
1476
|
+
},
|
|
1477
|
+
};
|
|
323
1478
|
}
|
|
324
1479
|
|
|
325
1480
|
/** A schema whose output type is not statically known (`type.raw` results). */
|
|
326
|
-
export type BaseType = FluentType<unknown>;
|
|
1481
|
+
export type BaseType = FluentType<unknown, unknown>;
|
|
327
1482
|
|
|
328
1483
|
/** `hasMorph` re-export for diagnostics/tooling. */
|
|
329
1484
|
export { hasMorph };
|