@oh-my-pi/omptype 18.16.0 → 18.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +33 -0
- package/dist/js/compile.js +25 -3
- package/dist/js/from-json-schema.js +61 -216
- package/dist/js/interp.js +2 -0
- package/dist/js/jsonschema/codegen.js +514 -0
- package/dist/js/jsonschema/interpret.js +729 -0
- package/dist/js/jsonschema/pattern.js +221 -0
- package/dist/js/jsonschema/plan.js +448 -0
- package/dist/js/jsonschema/resolve.js +321 -0
- package/dist/js/jsonschema/runtime.js +129 -0
- package/dist/js/jsonschema/validator.js +106 -0
- package/dist/js/typebox.js +5 -5
- package/dist/types/from-json-schema.d.ts +8 -3
- package/dist/types/ir.d.ts +2 -0
- package/dist/types/jsonschema/codegen.d.ts +6 -0
- package/dist/types/jsonschema/interpret.d.ts +48 -0
- package/dist/types/jsonschema/pattern.d.ts +17 -0
- package/dist/types/jsonschema/plan.d.ts +128 -0
- package/dist/types/jsonschema/resolve.d.ts +43 -0
- package/dist/types/jsonschema/runtime.d.ts +30 -0
- package/dist/types/jsonschema/validator.d.ts +33 -0
- package/package.json +2 -5
- package/src/compile.ts +24 -3
- package/src/from-json-schema.ts +64 -213
- package/src/interp.ts +1 -0
- package/src/ir.ts +2 -0
- package/src/jsonschema/codegen.ts +495 -0
- package/src/jsonschema/interpret.ts +724 -0
- package/src/jsonschema/pattern.ts +202 -0
- package/src/jsonschema/plan.ts +538 -0
- package/src/jsonschema/resolve.ts +333 -0
- package/src/jsonschema/runtime.ts +117 -0
- package/src/jsonschema/validator.ts +113 -0
- package/src/typebox.ts +9 -5
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema resource registry: base URIs, `$id`/`$anchor`/`$dynamicAnchor`
|
|
3
|
+
* identifiers, dialects, vocabularies, and `$ref` resolution across the root
|
|
4
|
+
* document and caller-supplied documents.
|
|
5
|
+
*
|
|
6
|
+
* Only subschemas under known applicator keywords are indexed, so an `$id`
|
|
7
|
+
* inside `enum` or an unknown keyword is not an identifier. JSON Pointer
|
|
8
|
+
* fragments may still point anywhere; their target inherits the base URI of
|
|
9
|
+
* the nearest indexed ancestor.
|
|
10
|
+
*/
|
|
11
|
+
import { OmpTypeError } from "../errors.js";
|
|
12
|
+
import { parseLocalRefPointer } from "../local-ref.js";
|
|
13
|
+
/** Vocabulary flags gating keyword groups; resources default to all of them. */
|
|
14
|
+
export const VOCAB_APPLICATOR = 1;
|
|
15
|
+
export const VOCAB_VALIDATION = 2;
|
|
16
|
+
export const VOCAB_UNEVALUATED = 4;
|
|
17
|
+
const VOCAB_ALL = VOCAB_APPLICATOR | VOCAB_VALIDATION | VOCAB_UNEVALUATED;
|
|
18
|
+
/** Base URI for documents without an absolute `$id`. */
|
|
19
|
+
const DEFAULT_BASE = "omptype:/schema.json";
|
|
20
|
+
const SCHEMAS = 1;
|
|
21
|
+
const SCHEMA_MAP = 2;
|
|
22
|
+
/**
|
|
23
|
+
* Keywords holding subschemas: a schema or array of schemas, or a map of names
|
|
24
|
+
* to schemas. A `Map`, so `then` is no thenable key and `constructor` no hit.
|
|
25
|
+
*/
|
|
26
|
+
const SUBSCHEMA_KEYWORDS = new Map([
|
|
27
|
+
["items", SCHEMAS],
|
|
28
|
+
["additionalItems", SCHEMAS],
|
|
29
|
+
["prefixItems", SCHEMAS],
|
|
30
|
+
["contains", SCHEMAS],
|
|
31
|
+
["additionalProperties", SCHEMAS],
|
|
32
|
+
["propertyNames", SCHEMAS],
|
|
33
|
+
["unevaluatedItems", SCHEMAS],
|
|
34
|
+
["unevaluatedProperties", SCHEMAS],
|
|
35
|
+
["not", SCHEMAS],
|
|
36
|
+
["if", SCHEMAS],
|
|
37
|
+
["then", SCHEMAS],
|
|
38
|
+
["else", SCHEMAS],
|
|
39
|
+
["allOf", SCHEMAS],
|
|
40
|
+
["anyOf", SCHEMAS],
|
|
41
|
+
["oneOf", SCHEMAS],
|
|
42
|
+
["contentSchema", SCHEMAS],
|
|
43
|
+
["properties", SCHEMA_MAP],
|
|
44
|
+
["patternProperties", SCHEMA_MAP],
|
|
45
|
+
["$defs", SCHEMA_MAP],
|
|
46
|
+
["definitions", SCHEMA_MAP],
|
|
47
|
+
["dependentSchemas", SCHEMA_MAP],
|
|
48
|
+
["dependencies", SCHEMA_MAP],
|
|
49
|
+
]);
|
|
50
|
+
const VOCABULARIES = {
|
|
51
|
+
applicator: VOCAB_APPLICATOR,
|
|
52
|
+
validation: VOCAB_VALIDATION,
|
|
53
|
+
unevaluated: VOCAB_UNEVALUATED,
|
|
54
|
+
};
|
|
55
|
+
/** Registry of the documents one `fromJsonSchema` call can reference. */
|
|
56
|
+
export class Registry {
|
|
57
|
+
#resources = new Map();
|
|
58
|
+
#anchors = new Map();
|
|
59
|
+
/** Per-registry index: one subschema object can sit at different bases in different registries. */
|
|
60
|
+
#locations = new Map();
|
|
61
|
+
/** Caller-supplied documents by fragment-free URI, indexed on first use. */
|
|
62
|
+
#pending = new Map();
|
|
63
|
+
#draft;
|
|
64
|
+
constructor(draft, documents) {
|
|
65
|
+
this.#draft = draft;
|
|
66
|
+
if (documents === undefined)
|
|
67
|
+
return;
|
|
68
|
+
for (const key in documents) {
|
|
69
|
+
const uri = stripFragment(parseUri(key) ?? key);
|
|
70
|
+
this.#pending.set(uri, documents[key]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Index `schema` as the root document and return its target. */
|
|
74
|
+
addRoot(schema) {
|
|
75
|
+
if (typeof schema !== "object" || schema === null) {
|
|
76
|
+
return {
|
|
77
|
+
schema,
|
|
78
|
+
location: { base: DEFAULT_BASE, resource: this.#newResource(DEFAULT_BASE, {}, this.#draft) },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return this.#addDocument(DEFAULT_BASE, schema);
|
|
82
|
+
}
|
|
83
|
+
/** Location recorded for an indexed subschema. */
|
|
84
|
+
location(schema) {
|
|
85
|
+
return this.#locations.get(schema);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve `ref` against `base` to a schema and its location.
|
|
89
|
+
*
|
|
90
|
+
* # Errors
|
|
91
|
+
* Throws {@link OmpTypeError} for malformed URI references or fragments, and
|
|
92
|
+
* for targets no known document contains.
|
|
93
|
+
*/
|
|
94
|
+
resolve(ref, base) {
|
|
95
|
+
let uri;
|
|
96
|
+
let fragment;
|
|
97
|
+
if (ref.charCodeAt(0) === 35) {
|
|
98
|
+
uri = base;
|
|
99
|
+
fragment = ref.slice(1);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const absolute = parseUri(ref, base);
|
|
103
|
+
if (absolute === undefined)
|
|
104
|
+
throw new OmpTypeError(`invalid $ref: ${ref}`);
|
|
105
|
+
const hash = absolute.indexOf("#");
|
|
106
|
+
uri = hash < 0 ? absolute : absolute.slice(0, hash);
|
|
107
|
+
fragment = hash < 0 ? "" : absolute.slice(hash + 1);
|
|
108
|
+
}
|
|
109
|
+
const resource = this.#resource(uri);
|
|
110
|
+
if (resource === undefined)
|
|
111
|
+
throw new OmpTypeError(`unresolved $ref: ${ref}`);
|
|
112
|
+
if (fragment === "")
|
|
113
|
+
return { schema: resource.root, location: this.#locations.get(resource.root) };
|
|
114
|
+
if (fragment.charCodeAt(0) === 47)
|
|
115
|
+
return this.#pointer(resource, fragment, ref);
|
|
116
|
+
let name;
|
|
117
|
+
try {
|
|
118
|
+
name = decodeURIComponent(fragment);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new OmpTypeError(`invalid $ref: ${ref}`);
|
|
122
|
+
}
|
|
123
|
+
const anchor = this.#anchors.get(`${uri}#${name}`) ?? this.#anchorFromPending(`${uri}#${name}`);
|
|
124
|
+
if (anchor === undefined)
|
|
125
|
+
throw new OmpTypeError(`unresolved $ref: ${ref}`);
|
|
126
|
+
return anchor;
|
|
127
|
+
}
|
|
128
|
+
#anchorFromPending(key) {
|
|
129
|
+
if (this.#pending.size === 0)
|
|
130
|
+
return undefined;
|
|
131
|
+
this.#indexPending();
|
|
132
|
+
return this.#anchors.get(key);
|
|
133
|
+
}
|
|
134
|
+
#pointer(resource, fragment, ref) {
|
|
135
|
+
const tokens = parseLocalRefPointer(`#${fragment}`);
|
|
136
|
+
if (tokens === undefined)
|
|
137
|
+
throw new OmpTypeError(`invalid $ref: ${ref}`);
|
|
138
|
+
let node = resource.root;
|
|
139
|
+
let location = this.#locations.get(resource.root);
|
|
140
|
+
for (const token of tokens) {
|
|
141
|
+
if (Array.isArray(node)) {
|
|
142
|
+
if (!/^(?:0|[1-9]\d*)$/.test(token) || Number(token) >= node.length) {
|
|
143
|
+
throw new OmpTypeError(`unresolved $ref: ${ref}`);
|
|
144
|
+
}
|
|
145
|
+
node = node[Number(token)];
|
|
146
|
+
}
|
|
147
|
+
else if (typeof node === "object" && node !== null && Object.hasOwn(node, token)) {
|
|
148
|
+
node = node[token];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new OmpTypeError(`unresolved $ref: ${ref}`);
|
|
152
|
+
}
|
|
153
|
+
if (typeof node === "object" && node !== null)
|
|
154
|
+
location = this.#locations.get(node) ?? location;
|
|
155
|
+
}
|
|
156
|
+
return { schema: node, location };
|
|
157
|
+
}
|
|
158
|
+
#resource(uri) {
|
|
159
|
+
const known = this.#resources.get(uri);
|
|
160
|
+
if (known !== undefined)
|
|
161
|
+
return known;
|
|
162
|
+
const document = this.#pending.get(uri);
|
|
163
|
+
if (document !== undefined) {
|
|
164
|
+
this.#pending.delete(uri);
|
|
165
|
+
if (typeof document === "object" && document !== null)
|
|
166
|
+
this.#addDocument(uri, document);
|
|
167
|
+
return this.#resources.get(uri);
|
|
168
|
+
}
|
|
169
|
+
// The URI may name a resource embedded in a document not yet indexed.
|
|
170
|
+
if (this.#pending.size === 0)
|
|
171
|
+
return undefined;
|
|
172
|
+
this.#indexPending();
|
|
173
|
+
return this.#resources.get(uri);
|
|
174
|
+
}
|
|
175
|
+
#indexPending() {
|
|
176
|
+
for (const [uri, document] of this.#pending) {
|
|
177
|
+
this.#pending.delete(uri);
|
|
178
|
+
if (typeof document === "object" && document !== null && !this.#resources.has(uri)) {
|
|
179
|
+
this.#addDocument(uri, document);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
#addDocument(retrieval, root) {
|
|
184
|
+
const resource = this.#newResource(retrieval, root, this.#draft);
|
|
185
|
+
const location = { base: retrieval, resource };
|
|
186
|
+
this.#index(root, location);
|
|
187
|
+
const own = this.#locations.get(root) ?? location;
|
|
188
|
+
return { schema: root, location: own };
|
|
189
|
+
}
|
|
190
|
+
#newResource(uri, root, inherited) {
|
|
191
|
+
const dialect = root.$schema;
|
|
192
|
+
let draft = inherited;
|
|
193
|
+
let vocab = VOCAB_ALL;
|
|
194
|
+
if (typeof dialect === "string") {
|
|
195
|
+
const resolved = this.#dialect(stripFragment(parseUri(dialect) ?? dialect), 0);
|
|
196
|
+
if (resolved !== undefined) {
|
|
197
|
+
draft = resolved.draft ?? inherited;
|
|
198
|
+
vocab = resolved.vocab;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const resource = { uri, root, draft, vocab, dynamicAnchors: new Map() };
|
|
202
|
+
if (!this.#resources.has(uri))
|
|
203
|
+
this.#resources.set(uri, resource);
|
|
204
|
+
return resource;
|
|
205
|
+
}
|
|
206
|
+
/** Draft and vocabularies a `$schema` URI selects; unknown dialects inherit. */
|
|
207
|
+
#dialect(uri, depth) {
|
|
208
|
+
if (/^https?:\/\/json-schema\.org\/draft-0[3-7]\/schema$/.test(uri))
|
|
209
|
+
return { draft: "7", vocab: VOCAB_ALL };
|
|
210
|
+
if (/^https?:\/\/json-schema\.org\/draft\/2020-12\/schema$/.test(uri))
|
|
211
|
+
return { draft: "2020-12", vocab: VOCAB_ALL };
|
|
212
|
+
if (/^https?:\/\/json-schema\.org\/draft\/2019-09\/schema$/.test(uri))
|
|
213
|
+
return { draft: "2019-09", vocab: VOCAB_ALL };
|
|
214
|
+
if (depth > 8)
|
|
215
|
+
return undefined;
|
|
216
|
+
const meta = this.#resources.get(uri)?.root ?? this.#pending.get(uri);
|
|
217
|
+
if (typeof meta !== "object" || meta === null)
|
|
218
|
+
return undefined;
|
|
219
|
+
const metaSchema = meta;
|
|
220
|
+
const parent = typeof metaSchema.$schema === "string" && stripFragment(metaSchema.$schema) !== uri
|
|
221
|
+
? this.#dialect(stripFragment(parseUri(metaSchema.$schema) ?? metaSchema.$schema), depth + 1)
|
|
222
|
+
: undefined;
|
|
223
|
+
const vocabularies = metaSchema.$vocabulary;
|
|
224
|
+
if (typeof vocabularies !== "object" || vocabularies === null)
|
|
225
|
+
return parent;
|
|
226
|
+
let vocab = 0;
|
|
227
|
+
for (const vocabulary in vocabularies) {
|
|
228
|
+
const name = vocabulary.slice(vocabulary.lastIndexOf("/") + 1);
|
|
229
|
+
vocab |= VOCABULARIES[name] ?? 0;
|
|
230
|
+
// Draft 2019-09 kept the unevaluated keywords in its applicator vocabulary.
|
|
231
|
+
if (name === "applicator" && vocabulary.includes("/2019-09/"))
|
|
232
|
+
vocab |= VOCAB_UNEVALUATED;
|
|
233
|
+
}
|
|
234
|
+
return { draft: parent?.draft ?? "2020-12", vocab };
|
|
235
|
+
}
|
|
236
|
+
#index(node, parent) {
|
|
237
|
+
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
238
|
+
return;
|
|
239
|
+
if (this.#locations.has(node))
|
|
240
|
+
return;
|
|
241
|
+
const schema = node;
|
|
242
|
+
let location = parent;
|
|
243
|
+
const draft7 = parent.resource.draft === "7";
|
|
244
|
+
const id = schema.$id;
|
|
245
|
+
let idAnchor;
|
|
246
|
+
if (typeof id === "string" && !(draft7 && typeof schema.$ref === "string")) {
|
|
247
|
+
if (id.charCodeAt(0) === 35) {
|
|
248
|
+
// Draft 7 location-independent identifier: `"$id": "#name"`.
|
|
249
|
+
idAnchor = id.slice(1);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const absolute = parseUri(id, parent.base);
|
|
253
|
+
if (absolute !== undefined) {
|
|
254
|
+
const hash = absolute.indexOf("#");
|
|
255
|
+
const uri = hash < 0 ? absolute : absolute.slice(0, hash);
|
|
256
|
+
if (hash >= 0 && hash < absolute.length - 1)
|
|
257
|
+
idAnchor = absolute.slice(hash + 1);
|
|
258
|
+
const resource = node === parent.resource.root && parent.base === parent.resource.uri
|
|
259
|
+
? this.#adoptRoot(parent.resource, uri)
|
|
260
|
+
: this.#newResource(uri, schema, parent.resource.draft);
|
|
261
|
+
location = { base: uri, resource };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.#locations.set(schema, location);
|
|
266
|
+
const anchor = schema.$anchor;
|
|
267
|
+
const dynamicAnchor = schema.$dynamicAnchor;
|
|
268
|
+
if (idAnchor !== undefined || typeof anchor === "string" || typeof dynamicAnchor === "string") {
|
|
269
|
+
const target = { schema, location };
|
|
270
|
+
if (idAnchor !== undefined)
|
|
271
|
+
this.#anchors.set(`${location.base}#${idAnchor}`, target);
|
|
272
|
+
if (typeof anchor === "string")
|
|
273
|
+
this.#anchors.set(`${location.base}#${anchor}`, target);
|
|
274
|
+
if (typeof dynamicAnchor === "string") {
|
|
275
|
+
this.#anchors.set(`${location.base}#${dynamicAnchor}`, target);
|
|
276
|
+
location.resource.dynamicAnchors.set(dynamicAnchor, schema);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const keyword in schema) {
|
|
280
|
+
const kind = SUBSCHEMA_KEYWORDS.get(keyword);
|
|
281
|
+
if (kind === undefined)
|
|
282
|
+
continue;
|
|
283
|
+
const value = schema[keyword];
|
|
284
|
+
if (kind === SCHEMAS) {
|
|
285
|
+
if (Array.isArray(value))
|
|
286
|
+
for (const item of value)
|
|
287
|
+
this.#index(item, location);
|
|
288
|
+
else
|
|
289
|
+
this.#index(value, location);
|
|
290
|
+
}
|
|
291
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
292
|
+
for (const key in value)
|
|
293
|
+
this.#index(value[key], location);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** The document root's own `$id` names the same resource as its retrieval URI. */
|
|
298
|
+
#adoptRoot(resource, uri) {
|
|
299
|
+
if (uri === resource.uri)
|
|
300
|
+
return resource;
|
|
301
|
+
const adopted = { ...resource, uri };
|
|
302
|
+
if (!this.#resources.has(uri))
|
|
303
|
+
this.#resources.set(uri, adopted);
|
|
304
|
+
// Retrieval URI keeps resolving to the root, now evaluated under its `$id`.
|
|
305
|
+
this.#resources.set(resource.uri, adopted);
|
|
306
|
+
return adopted;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** Absolute form of `reference` resolved against `base`, or `undefined` when unparsable. */
|
|
310
|
+
function parseUri(reference, base) {
|
|
311
|
+
try {
|
|
312
|
+
return new URL(reference, base).href;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function stripFragment(uri) {
|
|
319
|
+
const hash = uri.indexOf("#");
|
|
320
|
+
return hash < 0 ? uri : uri.slice(0, hash);
|
|
321
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime helpers shared by the closure validators (`closures.ts`) and the
|
|
3
|
+
* generated ones (`codegen.ts`): JSON value equality, `uniqueItems`,
|
|
4
|
+
* `multipleOf`, code-point string length, and the pattern cache.
|
|
5
|
+
*/
|
|
6
|
+
const hasOwn = Object.hasOwn;
|
|
7
|
+
/** JSON equality: numbers by value, arrays by order, objects by key set regardless of order. */
|
|
8
|
+
export function jsonEqual(a, b) {
|
|
9
|
+
if (a === b)
|
|
10
|
+
return true;
|
|
11
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
12
|
+
return false;
|
|
13
|
+
const array = Array.isArray(a);
|
|
14
|
+
if (array !== Array.isArray(b))
|
|
15
|
+
return false;
|
|
16
|
+
if (array) {
|
|
17
|
+
const left = a;
|
|
18
|
+
const right = b;
|
|
19
|
+
if (left.length !== right.length)
|
|
20
|
+
return false;
|
|
21
|
+
for (let index = 0; index < left.length; index++)
|
|
22
|
+
if (!jsonEqual(left[index], right[index]))
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const left = a;
|
|
27
|
+
const right = b;
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const key in left) {
|
|
30
|
+
if (!hasOwn(left, key))
|
|
31
|
+
continue;
|
|
32
|
+
if (!hasOwn(right, key) || !jsonEqual(left[key], right[key]))
|
|
33
|
+
return false;
|
|
34
|
+
count++;
|
|
35
|
+
}
|
|
36
|
+
for (const key in right)
|
|
37
|
+
if (hasOwn(right, key))
|
|
38
|
+
count--;
|
|
39
|
+
return count === 0;
|
|
40
|
+
}
|
|
41
|
+
/** True when `values` contains `value` under {@link jsonEqual}. */
|
|
42
|
+
export function jsonIncludes(values, value) {
|
|
43
|
+
for (const candidate of values)
|
|
44
|
+
if (jsonEqual(candidate, value))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** `uniqueItems`: no two elements are {@link jsonEqual}. */
|
|
49
|
+
export function uniqueItems(items) {
|
|
50
|
+
const n = items.length;
|
|
51
|
+
if (n < 2)
|
|
52
|
+
return true;
|
|
53
|
+
let primitive = true;
|
|
54
|
+
for (let index = 0; index < n; index++) {
|
|
55
|
+
const item = items[index];
|
|
56
|
+
if (typeof item === "object" && item !== null) {
|
|
57
|
+
primitive = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// SameValueZero matches JSON equality for primitives (0 equals -0).
|
|
62
|
+
if (primitive && n > 16)
|
|
63
|
+
return new Set(items).size === n;
|
|
64
|
+
for (let i = 1; i < n; i++) {
|
|
65
|
+
const item = items[i];
|
|
66
|
+
for (let j = 0; j < i; j++) {
|
|
67
|
+
if (primitive ? item === items[j] : jsonEqual(item, items[j]))
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `multipleOf` with float tolerance: `0.0075` is a multiple of `0.0001` even
|
|
75
|
+
* though the IEEE quotient is `74.99999999999999`. An overflowing quotient is
|
|
76
|
+
* a multiple only when `divisor` divides 1 (every integer is a multiple of 0.5).
|
|
77
|
+
*/
|
|
78
|
+
export function isMultipleOf(value, divisor) {
|
|
79
|
+
const quotient = value / divisor;
|
|
80
|
+
if (Number.isInteger(quotient))
|
|
81
|
+
return true;
|
|
82
|
+
if (!Number.isFinite(quotient))
|
|
83
|
+
return Number.isInteger(value) && Number.isInteger(1 / divisor);
|
|
84
|
+
return Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.abs(quotient);
|
|
85
|
+
}
|
|
86
|
+
/** String length in Unicode code points, as `minLength`/`maxLength` count it. */
|
|
87
|
+
export function codePointLength(value) {
|
|
88
|
+
let length = value.length;
|
|
89
|
+
for (let index = 0; index < value.length - 1; index++) {
|
|
90
|
+
const high = value.charCodeAt(index);
|
|
91
|
+
if (high < 0xd800 || high > 0xdbff)
|
|
92
|
+
continue;
|
|
93
|
+
const low = value.charCodeAt(index + 1);
|
|
94
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
95
|
+
length--;
|
|
96
|
+
index++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return length;
|
|
100
|
+
}
|
|
101
|
+
const patterns = new Map();
|
|
102
|
+
const PATTERN_CACHE_LIMIT = 1024;
|
|
103
|
+
/**
|
|
104
|
+
* Compile a JSON Schema `pattern` as an ECMAScript regex with the `u` flag,
|
|
105
|
+
* falling back to legacy syntax for patterns that only parse without it.
|
|
106
|
+
* Regexes are stateless (no `g`/`y`), so equal sources share one instance.
|
|
107
|
+
*
|
|
108
|
+
* # Errors
|
|
109
|
+
* Throws `SyntaxError` when the source is not a regular expression at all.
|
|
110
|
+
*/
|
|
111
|
+
export function compilePattern(source) {
|
|
112
|
+
let regex = patterns.get(source);
|
|
113
|
+
if (regex !== undefined)
|
|
114
|
+
return regex;
|
|
115
|
+
try {
|
|
116
|
+
regex = new RegExp(source, "u");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
regex = new RegExp(source);
|
|
120
|
+
}
|
|
121
|
+
if (patterns.size >= PATTERN_CACHE_LIMIT)
|
|
122
|
+
patterns.clear();
|
|
123
|
+
patterns.set(source, regex);
|
|
124
|
+
return regex;
|
|
125
|
+
}
|
|
126
|
+
/** Property names inherited by plain objects; reading them needs an own-property check. */
|
|
127
|
+
export function isInheritedName(key) {
|
|
128
|
+
return key in Object.prototype;
|
|
129
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema validator engine behind `fromJsonSchema`: plans the document,
|
|
3
|
+
* interprets the plan for the first calls, and swaps in generated code once a
|
|
4
|
+
* schema proves hot. Where code generation is forbidden (strict CSP) the
|
|
5
|
+
* interpreter keeps serving every call.
|
|
6
|
+
*/
|
|
7
|
+
import { generate } from "./codegen.js";
|
|
8
|
+
import { Sink, validate } from "./interpret.js";
|
|
9
|
+
import { Planner } from "./plan.js";
|
|
10
|
+
import { Registry } from "./resolve.js";
|
|
11
|
+
/** Calls interpreted before the JIT generates code; generation costs about as much as a hundred interpreted checks. */
|
|
12
|
+
const JIT_THRESHOLD = 64;
|
|
13
|
+
/** A compiled JSON Schema. */
|
|
14
|
+
export class Validator {
|
|
15
|
+
/** Allocation-free validity test; safe to detach (`const check = validator.check`). */
|
|
16
|
+
check;
|
|
17
|
+
/** Whether some `properties` subschema declares a `default` for {@link fillDefaults} to apply. */
|
|
18
|
+
hasDefaults;
|
|
19
|
+
#root;
|
|
20
|
+
/**
|
|
21
|
+
* `onCompile` receives the steady-state check once the schema turns hot
|
|
22
|
+
* (generated code, or the interpreter where code generation is forbidden), so
|
|
23
|
+
* callers holding their own reference can drop the dispatch through `check`.
|
|
24
|
+
*
|
|
25
|
+
* # Errors
|
|
26
|
+
* Throws `OmpTypeError` for unresolvable or malformed `$ref`s, invalid
|
|
27
|
+
* patterns, and schema nodes that are neither booleans nor objects.
|
|
28
|
+
*/
|
|
29
|
+
constructor(schema, options = {}, onCompile) {
|
|
30
|
+
const registry = new Registry(options.draft ?? "2020-12", options.schemas);
|
|
31
|
+
const planner = new Planner(registry, options.formats);
|
|
32
|
+
const root = planner.root(schema);
|
|
33
|
+
this.hasDefaults = planner.sawDefaults;
|
|
34
|
+
this.#root = root;
|
|
35
|
+
let calls = 0;
|
|
36
|
+
let impl = (value) => {
|
|
37
|
+
if (++calls < JIT_THRESHOLD)
|
|
38
|
+
return validate(root, value);
|
|
39
|
+
impl = generate(root) ?? ((input) => validate(root, input));
|
|
40
|
+
onCompile?.(impl);
|
|
41
|
+
return impl(value);
|
|
42
|
+
};
|
|
43
|
+
this.check = value => impl(value);
|
|
44
|
+
}
|
|
45
|
+
/** Every issue that makes `value` invalid (empty when it is valid). */
|
|
46
|
+
explain(value) {
|
|
47
|
+
const sink = new Sink();
|
|
48
|
+
validate(this.#root, value, sink);
|
|
49
|
+
return sink.issues;
|
|
50
|
+
}
|
|
51
|
+
/** `value` with absent declared properties set to their `default`, copied on write. */
|
|
52
|
+
fillDefaults(value) {
|
|
53
|
+
return fill(this.#root, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Defaults descend through in-place `$ref`/`allOf`, declared properties, and items. */
|
|
57
|
+
function fill(plan, value) {
|
|
58
|
+
let out = value;
|
|
59
|
+
if (plan.refs !== undefined)
|
|
60
|
+
for (const ref of plan.refs)
|
|
61
|
+
out = fill(ref, out);
|
|
62
|
+
if (plan.allOf !== undefined)
|
|
63
|
+
for (const branch of plan.allOf)
|
|
64
|
+
out = fill(branch, out);
|
|
65
|
+
if (typeof out !== "object" || out === null)
|
|
66
|
+
return out;
|
|
67
|
+
if (Array.isArray(out)) {
|
|
68
|
+
const prefix = plan.prefixItems ?? [];
|
|
69
|
+
if (prefix.length === 0 && plan.items === undefined)
|
|
70
|
+
return out;
|
|
71
|
+
let copy;
|
|
72
|
+
for (let index = 0; index < out.length; index++) {
|
|
73
|
+
const itemPlan = index < prefix.length ? prefix[index] : plan.items;
|
|
74
|
+
if (itemPlan === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
const item = fill(itemPlan, out[index]);
|
|
77
|
+
if (item === out[index])
|
|
78
|
+
continue;
|
|
79
|
+
copy ??= [...out];
|
|
80
|
+
copy[index] = item;
|
|
81
|
+
}
|
|
82
|
+
return copy ?? out;
|
|
83
|
+
}
|
|
84
|
+
if (plan.properties === undefined)
|
|
85
|
+
return out;
|
|
86
|
+
const source = out;
|
|
87
|
+
let copy;
|
|
88
|
+
for (const { key, plan: propertyPlan } of plan.properties) {
|
|
89
|
+
if (!Object.hasOwn(source, key) || source[key] === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
const filled = fill(propertyPlan, source[key]);
|
|
92
|
+
if (filled === source[key])
|
|
93
|
+
continue;
|
|
94
|
+
copy ??= { ...source };
|
|
95
|
+
copy[key] = filled;
|
|
96
|
+
}
|
|
97
|
+
if (plan.defaults !== undefined) {
|
|
98
|
+
for (const { key, value: fallback } of plan.defaults) {
|
|
99
|
+
if (Object.hasOwn(source, key) && source[key] !== undefined)
|
|
100
|
+
continue;
|
|
101
|
+
copy ??= { ...source };
|
|
102
|
+
copy[key] = typeof fallback === "object" && fallback !== null ? structuredClone(fallback) : fallback;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return copy ?? out;
|
|
106
|
+
}
|
package/dist/js/typebox.js
CHANGED
|
@@ -283,7 +283,7 @@ function tRecord(key, value, opts) {
|
|
|
283
283
|
return ctx.mustBe("an object with valid record keys");
|
|
284
284
|
return true;
|
|
285
285
|
});
|
|
286
|
-
return applyMeta(base, opts);
|
|
286
|
+
return asSchema(applyMeta(base, opts));
|
|
287
287
|
}
|
|
288
288
|
function tOptional(schema, opts) {
|
|
289
289
|
const marker = applyMeta(asRuntime(schema).or(asRuntime(type.raw("undefined"))), opts);
|
|
@@ -304,7 +304,7 @@ function tPartial(schema) {
|
|
|
304
304
|
const props = {};
|
|
305
305
|
for (const key in info.props)
|
|
306
306
|
props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ? info.props[key] : tOptional(info.props[key]);
|
|
307
|
-
return tObject(props, { additionalProperties: info.additionalProperties });
|
|
307
|
+
return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
|
|
308
308
|
}
|
|
309
309
|
function tRequired(schema) {
|
|
310
310
|
const info = requireObject(schema, "Required");
|
|
@@ -312,7 +312,7 @@ function tRequired(schema) {
|
|
|
312
312
|
for (const key in info.props) {
|
|
313
313
|
props[key] = asRuntime(info.props[key])[OPTIONAL_INNER] ?? info.props[key];
|
|
314
314
|
}
|
|
315
|
-
return tObject(props, { additionalProperties: info.additionalProperties });
|
|
315
|
+
return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
|
|
316
316
|
}
|
|
317
317
|
function tPick(schema, keys) {
|
|
318
318
|
const info = requireObject(schema, "Pick");
|
|
@@ -320,7 +320,7 @@ function tPick(schema, keys) {
|
|
|
320
320
|
for (const key of keys)
|
|
321
321
|
if (typeof key === "string" && info.props[key])
|
|
322
322
|
props[key] = info.props[key];
|
|
323
|
-
return tObject(props, { additionalProperties: info.additionalProperties });
|
|
323
|
+
return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
|
|
324
324
|
}
|
|
325
325
|
function tOmit(schema, keys) {
|
|
326
326
|
const info = requireObject(schema, "Omit");
|
|
@@ -329,7 +329,7 @@ function tOmit(schema, keys) {
|
|
|
329
329
|
for (const key in info.props)
|
|
330
330
|
if (!omitted.has(key))
|
|
331
331
|
props[key] = info.props[key];
|
|
332
|
-
return tObject(props, { additionalProperties: info.additionalProperties });
|
|
332
|
+
return asSchema(tObject(props, { additionalProperties: info.additionalProperties }));
|
|
333
333
|
}
|
|
334
334
|
function tComposite(schemas, opts) {
|
|
335
335
|
const props = {};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { type ValidatorOptions } from "./jsonschema/validator.js";
|
|
1
2
|
import { type BaseType } from "./type.js";
|
|
3
|
+
/** Options for {@link fromJsonSchema}. */
|
|
4
|
+
export type FromJsonSchemaOptions = ValidatorOptions;
|
|
2
5
|
/**
|
|
3
6
|
* Build a callable omptype schema from a JSON Schema document.
|
|
4
7
|
*
|
|
5
8
|
* # Errors
|
|
6
|
-
* Throws
|
|
7
|
-
*
|
|
9
|
+
* Throws `OmpTypeError` when a `$ref` is malformed or names a document or
|
|
10
|
+
* location that neither `schema` nor `options.schemas` contains, when a
|
|
11
|
+
* `pattern` is not a regular expression, or when a schema node is neither a
|
|
12
|
+
* boolean nor an object.
|
|
8
13
|
*/
|
|
9
|
-
export declare function fromJsonSchema(schema: unknown): BaseType;
|
|
14
|
+
export declare function fromJsonSchema(schema: unknown, options?: FromJsonSchemaOptions): BaseType;
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -158,6 +158,8 @@ export type IR = IRAnalysis & ({
|
|
|
158
158
|
k: "refine";
|
|
159
159
|
base: IR;
|
|
160
160
|
pred: (value: unknown) => boolean | OmpErrors;
|
|
161
|
+
/** Allocation-free equivalent of `pred(value) === true` for boolean-only paths (`allows`, unions). */
|
|
162
|
+
check?: (value: unknown) => boolean;
|
|
161
163
|
expected: string;
|
|
162
164
|
json?: Record<string, unknown>;
|
|
163
165
|
desc?: string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Plan } from "./plan.js";
|
|
2
|
+
/** One failure found while explaining an invalid value. */
|
|
3
|
+
export interface Issue {
|
|
4
|
+
readonly path: PropertyKey[];
|
|
5
|
+
readonly expected: string;
|
|
6
|
+
readonly data: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** Issue collector for explain mode. */
|
|
9
|
+
export declare class Sink {
|
|
10
|
+
readonly issues: Issue[];
|
|
11
|
+
add(expected: string, data: unknown, key?: PropertyKey): void;
|
|
12
|
+
/** Prefix the paths of issues recorded since `from` with `key`. */
|
|
13
|
+
prefix(from: number, key: PropertyKey): void;
|
|
14
|
+
}
|
|
15
|
+
/** Locations of the current instance evaluated so far by successful subschemas. */
|
|
16
|
+
export interface Ann {
|
|
17
|
+
/** Evaluated property names, or `true` once every property is evaluated. */
|
|
18
|
+
props: Set<string> | true | undefined;
|
|
19
|
+
/** Every index below this is evaluated (`Infinity` for all). */
|
|
20
|
+
items: number;
|
|
21
|
+
/** Indices evaluated by `contains`. */
|
|
22
|
+
contained: Set<number> | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Human-readable expectation for a `T_*` mask. */
|
|
25
|
+
export declare function describeTypes(types: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Object property whose value selects the only `anyOf`/`oneOf` branch that can
|
|
28
|
+
* pass: every branch is an object schema requiring the property with distinct
|
|
29
|
+
* primitive `const`/`enum` values.
|
|
30
|
+
*/
|
|
31
|
+
export interface Discriminator {
|
|
32
|
+
readonly key: string;
|
|
33
|
+
/** Tag value → branch index. */
|
|
34
|
+
readonly branches: Map<unknown, number>;
|
|
35
|
+
}
|
|
36
|
+
/** Follow plans that are a bare single `$ref`. */
|
|
37
|
+
export declare function unwrapRef(plan: Plan): Plan;
|
|
38
|
+
/** True when the plan's only constraint is its `refs`. */
|
|
39
|
+
export declare function isRefOnly(plan: Plan): boolean;
|
|
40
|
+
/** Find a {@link Discriminator} for union branches, if one exists. */
|
|
41
|
+
export declare function findDiscriminator(branches: readonly Plan[]): Discriminator | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Whether `value` satisfies `plan`.
|
|
44
|
+
*
|
|
45
|
+
* Infinite `$ref` cycles that never descend into the value (`{"$ref": "#"}`)
|
|
46
|
+
* overflow the stack, as they would in any evaluator.
|
|
47
|
+
*/
|
|
48
|
+
export declare function validate(plan: Plan, value: unknown, sink?: Sink, ann?: Ann): boolean;
|