ata-validator 1.0.2 → 1.2.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 +24 -0
- package/README.md +19 -2
- package/index.js +110 -12
- package/lib/aot.js +1 -1
- package/lib/interpreter.js +617 -0
- package/lib/js-compiler.js +32 -0
- package/lib/native-load.browser.js +1 -1
- package/lib/native-load.js +83 -11
- package/lib/t.js +90 -0
- package/lib/version.js +3 -3
- package/package.json +14 -20
- package/t.d.ts +84 -0
- package/CMakeLists.txt +0 -150
- package/binding/ata_napi.cpp +0 -1574
- package/binding-options.js +0 -4
- package/deps/simdjson/simdjson.cpp +0 -56221
- package/deps/simdjson/simdjson.h +0 -122784
- package/include/ata.h +0 -111
- package/include/ata_c.h +0 -57
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-arm64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-win32-x64/node-napi-v10.node +0 -0
- package/scripts/check-doc-coverage.js +0 -34
- package/scripts/check-prebuilds.js +0 -54
- package/scripts/install.js +0 -23
- package/scripts/regen-error-codes-doc.js +0 -40
- package/scripts/regen-lock.js +0 -19
- package/scripts/regen-safe-regex-source.js +0 -33
- package/src/ata.cpp +0 -3146
- package/src/ata_c.cpp +0 -63
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Interpreted draft 2020-12 engine. This is the correctness fallback for
|
|
4
|
+
// schemas the JS codegen cannot represent when the native engine is absent
|
|
5
|
+
// (browser, edge workers, ATA_NO_NATIVE). It walks the schema directly with
|
|
6
|
+
// full annotation tracking (unevaluatedProperties/Items), $id/$anchor
|
|
7
|
+
// resolution, and $dynamicRef dynamic-scope semantics. It optimizes for
|
|
8
|
+
// correctness and clarity, not speed: hot paths belong to codegen and the
|
|
9
|
+
// native engine.
|
|
10
|
+
|
|
11
|
+
const { compileSafe } = require('./safe-regex');
|
|
12
|
+
|
|
13
|
+
const SCHEMA_KEYWORDS = {
|
|
14
|
+
single: ['additionalProperties', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'items', 'unevaluatedItems', 'unevaluatedProperties'],
|
|
15
|
+
maps: ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas'],
|
|
16
|
+
lists: ['allOf', 'anyOf', 'oneOf', 'prefixItems'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FALLBACK_BASE = 'ata://root';
|
|
20
|
+
|
|
21
|
+
function resolveUri(base, ref) {
|
|
22
|
+
try {
|
|
23
|
+
return new URL(ref, base || FALLBACK_BASE).href;
|
|
24
|
+
} catch {
|
|
25
|
+
return ref;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function splitFragment(uri) {
|
|
30
|
+
const hash = uri.indexOf('#');
|
|
31
|
+
if (hash < 0) return [uri, ''];
|
|
32
|
+
return [uri.slice(0, hash), decodeURIComponent(uri.slice(hash + 1))];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------- resource index ----------
|
|
36
|
+
|
|
37
|
+
function indexSchemas(rootSchema, schemaMap) {
|
|
38
|
+
const state = {
|
|
39
|
+
resources: new Map(), // baseUri -> schema node of the resource root
|
|
40
|
+
anchors: new Map(), // baseUri -> Map(anchorName -> node)
|
|
41
|
+
dynamicAnchors: new Map(), // baseUri -> Map(anchorName -> node)
|
|
42
|
+
nodeBase: new Map(), // schema node -> baseUri of its enclosing resource
|
|
43
|
+
rootBase: FALLBACK_BASE,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const rootBase =
|
|
47
|
+
typeof rootSchema === 'object' && rootSchema !== null && typeof rootSchema.$id === 'string'
|
|
48
|
+
? resolveUri(FALLBACK_BASE, splitFragment(rootSchema.$id)[0])
|
|
49
|
+
: FALLBACK_BASE;
|
|
50
|
+
state.rootBase = rootBase;
|
|
51
|
+
indexResource(rootSchema, rootBase, state);
|
|
52
|
+
|
|
53
|
+
if (schemaMap) {
|
|
54
|
+
for (const [id, schema] of schemaMap) {
|
|
55
|
+
const base = resolveUri(FALLBACK_BASE, splitFragment(id)[0]);
|
|
56
|
+
if (!state.resources.has(base)) indexResource(schema, base, state);
|
|
57
|
+
// Also register under the raw key so non-URI ids stay addressable.
|
|
58
|
+
if (!state.resources.has(id)) state.resources.set(id, schema);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function indexResource(node, baseUri, state) {
|
|
65
|
+
if (typeof node !== 'object' || node === null) return;
|
|
66
|
+
if (!state.resources.has(baseUri)) state.resources.set(baseUri, node);
|
|
67
|
+
if (!state.anchors.has(baseUri)) state.anchors.set(baseUri, new Map());
|
|
68
|
+
if (!state.dynamicAnchors.has(baseUri)) state.dynamicAnchors.set(baseUri, new Map());
|
|
69
|
+
walkSchema(node, baseUri, state, true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function walkSchema(node, baseUri, state, isResourceRoot) {
|
|
73
|
+
if (typeof node !== 'object' || node === null) return;
|
|
74
|
+
if (state.nodeBase.has(node)) return; // cycle guard
|
|
75
|
+
state.nodeBase.set(node, baseUri);
|
|
76
|
+
|
|
77
|
+
if (!isResourceRoot && typeof node.$id === 'string') {
|
|
78
|
+
// Embedded resource: new base URI, indexed as its own resource.
|
|
79
|
+
const newBase = splitFragment(resolveUri(baseUri, node.$id))[0];
|
|
80
|
+
state.nodeBase.delete(node); // re-register under the new base
|
|
81
|
+
indexResource(node, newBase, state);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof node.$anchor === 'string') state.anchors.get(baseUri).set(node.$anchor, node);
|
|
86
|
+
if (typeof node.$dynamicAnchor === 'string') {
|
|
87
|
+
state.dynamicAnchors.get(baseUri).set(node.$dynamicAnchor, node);
|
|
88
|
+
// A $dynamicAnchor is also addressable as a plain anchor.
|
|
89
|
+
state.anchors.get(baseUri).set(node.$dynamicAnchor, node);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const kw of SCHEMA_KEYWORDS.single) {
|
|
93
|
+
if (node[kw] !== undefined) walkSchema(node[kw], baseUri, state, false);
|
|
94
|
+
}
|
|
95
|
+
for (const kw of SCHEMA_KEYWORDS.maps) {
|
|
96
|
+
const map = node[kw];
|
|
97
|
+
if (map && typeof map === 'object' && !Array.isArray(map)) {
|
|
98
|
+
for (const key of Object.keys(map)) walkSchema(map[key], baseUri, state, false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const kw of SCHEMA_KEYWORDS.lists) {
|
|
102
|
+
const list = node[kw];
|
|
103
|
+
if (Array.isArray(list)) for (const sub of list) walkSchema(sub, baseUri, state, false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- ref resolution ----------
|
|
108
|
+
|
|
109
|
+
function walkPointer(root, pointer) {
|
|
110
|
+
if (pointer === '' || pointer === '/') return root;
|
|
111
|
+
const parts = pointer
|
|
112
|
+
.split('/')
|
|
113
|
+
.slice(1)
|
|
114
|
+
.map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
115
|
+
let node = root;
|
|
116
|
+
for (const part of parts) {
|
|
117
|
+
if (node === null || typeof node !== 'object') return undefined;
|
|
118
|
+
if (Array.isArray(node)) {
|
|
119
|
+
node = node[Number(part)];
|
|
120
|
+
} else if (part in node) {
|
|
121
|
+
node = node[part];
|
|
122
|
+
} else if (part === 'definitions' && node.$defs) {
|
|
123
|
+
// draft-07 normalization renames definitions to $defs; keep old
|
|
124
|
+
// pointers working (mirrors the 1.0.2 alias fix).
|
|
125
|
+
node = node.$defs;
|
|
126
|
+
} else if (part === '$defs' && node.definitions) {
|
|
127
|
+
node = node.definitions;
|
|
128
|
+
} else {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveRef(ref, fromBase, state) {
|
|
136
|
+
const abs = resolveUri(fromBase, ref);
|
|
137
|
+
const [uri, fragment] = splitFragment(abs);
|
|
138
|
+
let resource = state.resources.get(uri);
|
|
139
|
+
let resourceBase = uri;
|
|
140
|
+
if (resource === undefined && (uri === FALLBACK_BASE || uri === '')) {
|
|
141
|
+
resource = state.resources.get(state.rootBase);
|
|
142
|
+
resourceBase = state.rootBase;
|
|
143
|
+
}
|
|
144
|
+
if (resource === undefined) {
|
|
145
|
+
// Raw-id registration (schemaMap keys that are not URIs).
|
|
146
|
+
const [rawUri, rawFragment] = splitFragment(ref);
|
|
147
|
+
if (state.resources.has(rawUri)) {
|
|
148
|
+
resource = state.resources.get(rawUri);
|
|
149
|
+
resourceBase = rawUri;
|
|
150
|
+
if (rawFragment === '') return { node: resource, base: resourceBase };
|
|
151
|
+
if (rawFragment.startsWith('/')) return { node: walkPointer(resource, rawFragment), base: resourceBase };
|
|
152
|
+
const anchored = state.anchors.get(resourceBase);
|
|
153
|
+
return { node: anchored ? anchored.get(rawFragment) : undefined, base: resourceBase };
|
|
154
|
+
}
|
|
155
|
+
return { node: undefined, base: resourceBase };
|
|
156
|
+
}
|
|
157
|
+
if (fragment === '') return { node: resource, base: resourceBase };
|
|
158
|
+
if (fragment.startsWith('/')) {
|
|
159
|
+
const node = walkPointer(resource, fragment);
|
|
160
|
+
// A pointer can land inside an embedded resource; its recorded base wins.
|
|
161
|
+
const base = node !== null && typeof node === 'object' && state.nodeBase.has(node) ? state.nodeBase.get(node) : resourceBase;
|
|
162
|
+
return { node, base };
|
|
163
|
+
}
|
|
164
|
+
const anchored = state.anchors.get(resourceBase);
|
|
165
|
+
return { node: anchored ? anchored.get(fragment) : undefined, base: resourceBase };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------- value helpers ----------
|
|
169
|
+
|
|
170
|
+
function deepEqual(a, b) {
|
|
171
|
+
if (a === b) return true;
|
|
172
|
+
if (typeof a === 'number' && typeof b === 'number') return a === b;
|
|
173
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
174
|
+
const aArr = Array.isArray(a);
|
|
175
|
+
if (aArr !== Array.isArray(b)) return false;
|
|
176
|
+
if (aArr) {
|
|
177
|
+
if (a.length !== b.length) return false;
|
|
178
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const ak = Object.keys(a);
|
|
182
|
+
const bk = Object.keys(b);
|
|
183
|
+
if (ak.length !== bk.length) return false;
|
|
184
|
+
for (const k of ak) {
|
|
185
|
+
if (!(k in b) || !deepEqual(a[k], b[k])) return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function codePointLength(s) {
|
|
191
|
+
let n = 0;
|
|
192
|
+
for (let i = 0; i < s.length; i++) {
|
|
193
|
+
const c = s.charCodeAt(i);
|
|
194
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) i++;
|
|
195
|
+
n++;
|
|
196
|
+
}
|
|
197
|
+
return n;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function typeMatches(type, d) {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'string': return typeof d === 'string';
|
|
203
|
+
case 'number': return typeof d === 'number' && isFinite(d);
|
|
204
|
+
case 'integer': return typeof d === 'number' && Number.isInteger(d);
|
|
205
|
+
case 'boolean': return typeof d === 'boolean';
|
|
206
|
+
case 'null': return d === null;
|
|
207
|
+
case 'array': return Array.isArray(d);
|
|
208
|
+
case 'object': return typeof d === 'object' && d !== null && !Array.isArray(d);
|
|
209
|
+
default: return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function multipleOfOk(d, m) {
|
|
214
|
+
if (m === 0) return false;
|
|
215
|
+
const q = d / m;
|
|
216
|
+
if (Number.isInteger(q)) return true;
|
|
217
|
+
// Float remainder tolerance (0.0075 % 0.0001 style cases)
|
|
218
|
+
return Math.abs(q - Math.round(q)) < 1e-9;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const FORMAT_CHECKS = {
|
|
222
|
+
email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
|
|
223
|
+
date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31; },
|
|
224
|
+
'date-time': (s) => /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/.test(s) && !isNaN(Date.parse(s)),
|
|
225
|
+
time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
|
|
226
|
+
duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
|
|
227
|
+
uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
|
|
228
|
+
uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s),
|
|
229
|
+
'uri-reference': (s) => s === '' || !/\s/.test(s),
|
|
230
|
+
ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every((n) => { const v = +n; return n !== '' && v >= 0 && v <= 255 && String(v) === n; }); },
|
|
231
|
+
ipv6: (s) => s !== '' && /^[0-9a-fA-F:.]+$/.test(s) && s.split(':').length >= 3 && s.split(':').length <= 8,
|
|
232
|
+
hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s),
|
|
233
|
+
regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { try { new RegExp(s); return true; } catch { return false; } } },
|
|
234
|
+
'json-pointer': (s) => s === '' || (/^\//.test(s) && !/~(?![01])/.test(s)),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ---------- evaluation ----------
|
|
238
|
+
|
|
239
|
+
const NO_ANNOTATIONS = { props: null, items: null };
|
|
240
|
+
|
|
241
|
+
function mergeAnnotations(target, from) {
|
|
242
|
+
if (from.props && from.props.size) {
|
|
243
|
+
if (!target.props) target.props = new Set();
|
|
244
|
+
for (const p of from.props) target.props.add(p);
|
|
245
|
+
}
|
|
246
|
+
if (from.items && from.items.size) {
|
|
247
|
+
if (!target.items) target.items = new Set();
|
|
248
|
+
for (const i of from.items) target.items.add(i);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class Interpreter {
|
|
253
|
+
constructor(rootSchema, options) {
|
|
254
|
+
const opts = options || {};
|
|
255
|
+
this.root = rootSchema;
|
|
256
|
+
this.state = indexSchemas(rootSchema, opts.schemaMap);
|
|
257
|
+
this.userFormats = opts.formats || null;
|
|
258
|
+
this.patternCache = new Map();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
pattern(src) {
|
|
262
|
+
let re = this.patternCache.get(src);
|
|
263
|
+
if (!re) {
|
|
264
|
+
if (/\\[pP]\{/.test(src)) {
|
|
265
|
+
// Unicode property escapes need a real RegExp in unicode mode; the
|
|
266
|
+
// linear engine does not understand them.
|
|
267
|
+
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
268
|
+
} else {
|
|
269
|
+
try {
|
|
270
|
+
re = compileSafe(src);
|
|
271
|
+
} catch {
|
|
272
|
+
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.patternCache.set(src, re);
|
|
276
|
+
}
|
|
277
|
+
return re;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
validate(data) {
|
|
281
|
+
const errors = [];
|
|
282
|
+
const dynScope = [this.state.rootBase];
|
|
283
|
+
const valid = this.eval(this.root, data, this.state.rootBase, dynScope, errors, '', '#', [], NO_ANNOTATIONS_SINK());
|
|
284
|
+
return valid ? { valid: true, data, errors: [] } : { valid: false, errors };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Evaluates `schema` against `data`. Collected annotations for data are
|
|
288
|
+
// written into `sink` ({props, items}); errors append to `errors`.
|
|
289
|
+
// `stack` breaks infinite $ref recursion on identical (schema, data) pairs.
|
|
290
|
+
eval(schema, data, base, dynScope, errors, instancePath, schemaPath, stack, sink) {
|
|
291
|
+
if (schema === true) return true;
|
|
292
|
+
if (schema === false) {
|
|
293
|
+
errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (typeof schema !== 'object' || schema === null) return true;
|
|
297
|
+
|
|
298
|
+
for (const frame of stack) {
|
|
299
|
+
if (frame[0] === schema && frame[1] === data) return true; // fixed point
|
|
300
|
+
}
|
|
301
|
+
stack = stack.concat([[schema, data]]);
|
|
302
|
+
|
|
303
|
+
const nodeBase = this.state.nodeBase.get(schema);
|
|
304
|
+
if (nodeBase !== undefined && nodeBase !== base) base = nodeBase;
|
|
305
|
+
// Entering a schema resource extends the dynamic scope.
|
|
306
|
+
if (this.state.resources.get(base) === schema && dynScope[dynScope.length - 1] !== base) {
|
|
307
|
+
dynScope = dynScope.concat(base);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let valid = true;
|
|
311
|
+
const local = { props: null, items: null };
|
|
312
|
+
|
|
313
|
+
// $ref / $dynamicRef: in-place applicators, annotations flow through
|
|
314
|
+
if (typeof schema.$ref === 'string') {
|
|
315
|
+
const { node, base: refBase } = resolveRef(schema.$ref, base, this.state);
|
|
316
|
+
if (node === undefined) {
|
|
317
|
+
errors.push(err('$ref', '$ref', instancePath, schemaPath + '/$ref', { ref: schema.$ref }, `cannot resolve $ref ${schema.$ref}`));
|
|
318
|
+
valid = false;
|
|
319
|
+
} else {
|
|
320
|
+
const sub = { props: null, items: null };
|
|
321
|
+
if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$ref', stack, sub)) valid = false;
|
|
322
|
+
else mergeAnnotations(local, sub);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (typeof schema.$dynamicRef === 'string') {
|
|
327
|
+
const ref = schema.$dynamicRef;
|
|
328
|
+
let { node, base: refBase } = resolveRef(ref, base, this.state);
|
|
329
|
+
const [, fragment] = splitFragment(resolveUri(base, ref));
|
|
330
|
+
if (node !== undefined && fragment && !fragment.startsWith('/')) {
|
|
331
|
+
// Initial target must itself carry the matching $dynamicAnchor for
|
|
332
|
+
// dynamic resolution; otherwise it behaves like $ref.
|
|
333
|
+
const initialDyn = this.state.dynamicAnchors.get(refBase);
|
|
334
|
+
if (initialDyn && initialDyn.get(fragment) === node) {
|
|
335
|
+
for (const scopeBase of dynScope) {
|
|
336
|
+
const dyn = this.state.dynamicAnchors.get(scopeBase);
|
|
337
|
+
if (dyn && dyn.has(fragment)) {
|
|
338
|
+
node = dyn.get(fragment);
|
|
339
|
+
refBase = scopeBase;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (node === undefined) {
|
|
346
|
+
errors.push(err('$dynamicRef', '$dynamicRef', instancePath, schemaPath + '/$dynamicRef', { ref }, `cannot resolve $dynamicRef ${ref}`));
|
|
347
|
+
valid = false;
|
|
348
|
+
} else {
|
|
349
|
+
const sub = { props: null, items: null };
|
|
350
|
+
if (!this.eval(node, data, refBase, dynScope, errors, instancePath, schemaPath + '/$dynamicRef', stack, sub)) valid = false;
|
|
351
|
+
else mergeAnnotations(local, sub);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// type / enum / const
|
|
356
|
+
if (schema.type !== undefined) {
|
|
357
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
358
|
+
if (!types.some((t) => typeMatches(t, data))) {
|
|
359
|
+
errors.push(err('type', 'type', instancePath, schemaPath + '/type', { type: schema.type }, `must be ${types.join(' or ')}`));
|
|
360
|
+
valid = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (schema.enum !== undefined) {
|
|
364
|
+
if (!schema.enum.some((v) => deepEqual(v, data))) {
|
|
365
|
+
errors.push(err('enum', 'enum', instancePath, schemaPath + '/enum', { allowedValues: schema.enum }, 'must be equal to one of the allowed values'));
|
|
366
|
+
valid = false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (schema.const !== undefined) {
|
|
370
|
+
if (!deepEqual(schema.const, data)) {
|
|
371
|
+
errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: schema.const }, 'must be equal to constant'));
|
|
372
|
+
valid = false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// numbers
|
|
377
|
+
if (typeof data === 'number') {
|
|
378
|
+
if (typeof schema.minimum === 'number' && !(data >= schema.minimum)) { errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: schema.minimum }, `must be >= ${schema.minimum}`)); valid = false; }
|
|
379
|
+
if (typeof schema.maximum === 'number' && !(data <= schema.maximum)) { errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: schema.maximum }, `must be <= ${schema.maximum}`)); valid = false; }
|
|
380
|
+
if (typeof schema.exclusiveMinimum === 'number' && !(data > schema.exclusiveMinimum)) { errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: schema.exclusiveMinimum }, `must be > ${schema.exclusiveMinimum}`)); valid = false; }
|
|
381
|
+
if (typeof schema.exclusiveMaximum === 'number' && !(data < schema.exclusiveMaximum)) { errors.push(err('exclusiveMaximum', 'exclusiveMaximum', instancePath, schemaPath + '/exclusiveMaximum', { comparison: '<', limit: schema.exclusiveMaximum }, `must be < ${schema.exclusiveMaximum}`)); valid = false; }
|
|
382
|
+
if (typeof schema.multipleOf === 'number' && !multipleOfOk(data, schema.multipleOf)) { errors.push(err('multipleOf', 'multipleOf', instancePath, schemaPath + '/multipleOf', { multipleOf: schema.multipleOf }, `must be multiple of ${schema.multipleOf}`)); valid = false; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// strings
|
|
386
|
+
if (typeof data === 'string') {
|
|
387
|
+
if (schema.minLength !== undefined && codePointLength(data) < schema.minLength) { errors.push(err('minLength', 'minLength', instancePath, schemaPath + '/minLength', { limit: schema.minLength }, `must NOT have fewer than ${schema.minLength} characters`)); valid = false; }
|
|
388
|
+
if (schema.maxLength !== undefined && codePointLength(data) > schema.maxLength) { errors.push(err('maxLength', 'maxLength', instancePath, schemaPath + '/maxLength', { limit: schema.maxLength }, `must NOT have more than ${schema.maxLength} characters`)); valid = false; }
|
|
389
|
+
if (schema.pattern !== undefined && !this.pattern(schema.pattern).test(data)) { errors.push(err('pattern', 'pattern', instancePath, schemaPath + '/pattern', { pattern: schema.pattern }, `must match pattern "${schema.pattern}"`)); valid = false; }
|
|
390
|
+
if (schema.format !== undefined) {
|
|
391
|
+
const fc = (this.userFormats && typeof this.userFormats[schema.format] === 'function') ? this.userFormats[schema.format] : FORMAT_CHECKS[schema.format];
|
|
392
|
+
if (fc && !fc(data)) { errors.push(err('format', 'format', instancePath, schemaPath + '/format', { format: schema.format }, `must match format "${schema.format}"`)); valid = false; }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// arrays
|
|
397
|
+
if (Array.isArray(data)) {
|
|
398
|
+
if (schema.minItems !== undefined && data.length < schema.minItems) { errors.push(err('minItems', 'minItems', instancePath, schemaPath + '/minItems', { limit: schema.minItems }, `must NOT have fewer than ${schema.minItems} items`)); valid = false; }
|
|
399
|
+
if (schema.maxItems !== undefined && data.length > schema.maxItems) { errors.push(err('maxItems', 'maxItems', instancePath, schemaPath + '/maxItems', { limit: schema.maxItems }, `must NOT have more than ${schema.maxItems} items`)); valid = false; }
|
|
400
|
+
if (schema.uniqueItems === true) {
|
|
401
|
+
outer: for (let i = 0; i < data.length; i++) {
|
|
402
|
+
for (let j = i + 1; j < data.length; j++) {
|
|
403
|
+
if (deepEqual(data[i], data[j])) {
|
|
404
|
+
errors.push(err('uniqueItems', 'uniqueItems', instancePath, schemaPath + '/uniqueItems', { i, j }, 'must NOT have duplicate items'));
|
|
405
|
+
valid = false;
|
|
406
|
+
break outer;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : null;
|
|
412
|
+
if (prefix) {
|
|
413
|
+
for (let i = 0; i < Math.min(prefix.length, data.length); i++) {
|
|
414
|
+
const sub = { props: null, items: null };
|
|
415
|
+
if (!this.eval(prefix[i], data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack, sub)) valid = false;
|
|
416
|
+
if (!local.items) local.items = new Set();
|
|
417
|
+
local.items.add(i);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (schema.items !== undefined) {
|
|
421
|
+
const start = prefix ? prefix.length : 0;
|
|
422
|
+
for (let i = start; i < data.length; i++) {
|
|
423
|
+
const sub = { props: null, items: null };
|
|
424
|
+
if (!this.eval(schema.items, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/items', stack, sub)) valid = false;
|
|
425
|
+
if (!local.items) local.items = new Set();
|
|
426
|
+
local.items.add(i);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (schema.contains !== undefined) {
|
|
430
|
+
const matched = [];
|
|
431
|
+
for (let i = 0; i < data.length; i++) {
|
|
432
|
+
const scratch = [];
|
|
433
|
+
const sub = { props: null, items: null };
|
|
434
|
+
if (this.eval(schema.contains, data[i], base, dynScope, scratch, instancePath + '/' + i, schemaPath + '/contains', stack, sub)) matched.push(i);
|
|
435
|
+
}
|
|
436
|
+
const minC = schema.minContains !== undefined ? schema.minContains : 1;
|
|
437
|
+
if (matched.length < minC) { errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC }, `must contain at least ${minC} valid item(s)`)); valid = false; }
|
|
438
|
+
if (schema.maxContains !== undefined && matched.length > schema.maxContains) { errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: schema.maxContains }, `must NOT contain more than ${schema.maxContains} valid item(s)`)); valid = false; }
|
|
439
|
+
if (matched.length) {
|
|
440
|
+
if (!local.items) local.items = new Set();
|
|
441
|
+
for (const i of matched) local.items.add(i);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// objects
|
|
447
|
+
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
448
|
+
const keys = Object.keys(data);
|
|
449
|
+
if (Array.isArray(schema.required)) {
|
|
450
|
+
for (const key of schema.required) {
|
|
451
|
+
if (!Object.prototype.hasOwnProperty.call(data, key)) { errors.push(err('required', 'required', instancePath, schemaPath + '/required', { missingProperty: key }, `must have required property '${key}'`)); valid = false; }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (schema.minProperties !== undefined && keys.length < schema.minProperties) { errors.push(err('minProperties', 'minProperties', instancePath, schemaPath + '/minProperties', { limit: schema.minProperties }, `must NOT have fewer than ${schema.minProperties} properties`)); valid = false; }
|
|
455
|
+
if (schema.maxProperties !== undefined && keys.length > schema.maxProperties) { errors.push(err('maxProperties', 'maxProperties', instancePath, schemaPath + '/maxProperties', { limit: schema.maxProperties }, `must NOT have more than ${schema.maxProperties} properties`)); valid = false; }
|
|
456
|
+
if (schema.dependentRequired && typeof schema.dependentRequired === 'object') {
|
|
457
|
+
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
458
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
459
|
+
for (const dep of deps) {
|
|
460
|
+
if (!Object.prototype.hasOwnProperty.call(data, dep)) { errors.push(err('required', 'required', instancePath, schemaPath + '/dependentRequired', { missingProperty: dep }, `must have required property '${dep}'`)); valid = false; }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (schema.propertyNames !== undefined) {
|
|
466
|
+
for (const key of keys) {
|
|
467
|
+
if (!this.eval(schema.propertyNames, key, base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/propertyNames', stack, { props: null, items: null })) valid = false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : null;
|
|
471
|
+
const patterns = schema.patternProperties && typeof schema.patternProperties === 'object' ? Object.keys(schema.patternProperties) : null;
|
|
472
|
+
for (const key of keys) {
|
|
473
|
+
let evaluated = false;
|
|
474
|
+
if (props && Object.prototype.hasOwnProperty.call(props, key)) {
|
|
475
|
+
const sub = { props: null, items: null };
|
|
476
|
+
if (!this.eval(props[key], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/properties/' + escapePointer(key), stack, sub)) valid = false;
|
|
477
|
+
evaluated = true;
|
|
478
|
+
}
|
|
479
|
+
if (patterns) {
|
|
480
|
+
for (const src of patterns) {
|
|
481
|
+
if (this.pattern(src).test(key)) {
|
|
482
|
+
const sub = { props: null, items: null };
|
|
483
|
+
if (!this.eval(schema.patternProperties[src], data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(src), stack, sub)) valid = false;
|
|
484
|
+
evaluated = true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!evaluated && schema.additionalProperties !== undefined) {
|
|
489
|
+
const sub = { props: null, items: null };
|
|
490
|
+
if (!this.eval(schema.additionalProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack, sub)) valid = false;
|
|
491
|
+
evaluated = true;
|
|
492
|
+
}
|
|
493
|
+
if (evaluated) {
|
|
494
|
+
if (!local.props) local.props = new Set();
|
|
495
|
+
local.props.add(key);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (schema.dependentSchemas && typeof schema.dependentSchemas === 'object') {
|
|
499
|
+
for (const [key, dep] of Object.entries(schema.dependentSchemas)) {
|
|
500
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
501
|
+
const sub = { props: null, items: null };
|
|
502
|
+
if (!this.eval(dep, data, base, dynScope, errors, instancePath, schemaPath + '/dependentSchemas/' + escapePointer(key), stack, sub)) valid = false;
|
|
503
|
+
else mergeAnnotations(local, sub);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// in-place applicators
|
|
510
|
+
if (Array.isArray(schema.allOf)) {
|
|
511
|
+
for (let i = 0; i < schema.allOf.length; i++) {
|
|
512
|
+
const sub = { props: null, items: null };
|
|
513
|
+
if (!this.eval(schema.allOf[i], data, base, dynScope, errors, instancePath, schemaPath + '/allOf/' + i, stack, sub)) valid = false;
|
|
514
|
+
else mergeAnnotations(local, sub);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (Array.isArray(schema.anyOf)) {
|
|
518
|
+
let any = false;
|
|
519
|
+
const scratch = [];
|
|
520
|
+
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
521
|
+
const sub = { props: null, items: null };
|
|
522
|
+
if (this.eval(schema.anyOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, sub)) {
|
|
523
|
+
any = true;
|
|
524
|
+
mergeAnnotations(local, sub);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (!any) {
|
|
528
|
+
for (const e of scratch) errors.push(e);
|
|
529
|
+
errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
|
|
530
|
+
valid = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(schema.oneOf)) {
|
|
534
|
+
let count = 0;
|
|
535
|
+
let winner = null;
|
|
536
|
+
const scratch = [];
|
|
537
|
+
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
538
|
+
const sub = { props: null, items: null };
|
|
539
|
+
if (this.eval(schema.oneOf[i], data, base, dynScope, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, sub)) {
|
|
540
|
+
count++;
|
|
541
|
+
winner = sub;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (count === 1) mergeAnnotations(local, winner);
|
|
545
|
+
else {
|
|
546
|
+
if (count === 0) for (const e of scratch) errors.push(e);
|
|
547
|
+
errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
|
|
548
|
+
valid = false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (schema.not !== undefined) {
|
|
552
|
+
const scratch = [];
|
|
553
|
+
if (this.eval(schema.not, data, base, dynScope, scratch, instancePath, schemaPath + '/not', stack, { props: null, items: null })) {
|
|
554
|
+
errors.push(err('not', 'not', instancePath, schemaPath + '/not', {}, 'must NOT be valid'));
|
|
555
|
+
valid = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (schema.if !== undefined) {
|
|
559
|
+
const ifSub = { props: null, items: null };
|
|
560
|
+
const ifScratch = [];
|
|
561
|
+
const ifOk = this.eval(schema.if, data, base, dynScope, ifScratch, instancePath, schemaPath + '/if', stack, ifSub);
|
|
562
|
+
if (ifOk) {
|
|
563
|
+
mergeAnnotations(local, ifSub);
|
|
564
|
+
if (schema.then !== undefined) {
|
|
565
|
+
const sub = { props: null, items: null };
|
|
566
|
+
if (!this.eval(schema.then, data, base, dynScope, errors, instancePath, schemaPath + '/then', stack, sub)) valid = false;
|
|
567
|
+
else mergeAnnotations(local, sub);
|
|
568
|
+
}
|
|
569
|
+
} else if (schema.else !== undefined) {
|
|
570
|
+
const sub = { props: null, items: null };
|
|
571
|
+
if (!this.eval(schema.else, data, base, dynScope, errors, instancePath, schemaPath + '/else', stack, sub)) valid = false;
|
|
572
|
+
else mergeAnnotations(local, sub);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// unevaluated*: run last, against annotations from everything above
|
|
577
|
+
if (schema.unevaluatedProperties !== undefined && typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
578
|
+
for (const key of Object.keys(data)) {
|
|
579
|
+
if (local.props && local.props.has(key)) continue;
|
|
580
|
+
const sub = { props: null, items: null };
|
|
581
|
+
if (!this.eval(schema.unevaluatedProperties, data[key], base, dynScope, errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack, sub)) valid = false;
|
|
582
|
+
if (!local.props) local.props = new Set();
|
|
583
|
+
local.props.add(key);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (schema.unevaluatedItems !== undefined && Array.isArray(data)) {
|
|
587
|
+
for (let i = 0; i < data.length; i++) {
|
|
588
|
+
if (local.items && local.items.has(i)) continue;
|
|
589
|
+
const sub = { props: null, items: null };
|
|
590
|
+
if (!this.eval(schema.unevaluatedItems, data[i], base, dynScope, errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack, sub)) valid = false;
|
|
591
|
+
if (!local.items) local.items = new Set();
|
|
592
|
+
local.items.add(i);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (valid) mergeAnnotations(sink, local);
|
|
597
|
+
return valid;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function NO_ANNOTATIONS_SINK() {
|
|
602
|
+
return { props: null, items: null };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function escapePointer(s) {
|
|
606
|
+
return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function err(code, keyword, instancePath, schemaPath, params, message) {
|
|
610
|
+
return { keyword, instancePath, schemaPath, params, message };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function createInterpreter(schema, options) {
|
|
614
|
+
return new Interpreter(schema, options);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
module.exports = { createInterpreter };
|
package/lib/js-compiler.js
CHANGED
|
@@ -702,6 +702,27 @@ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
|
|
|
702
702
|
return refs.every(r => anchors.has(r))
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
+
// Returns true if the node's subtree contains a local $ref pointing to one of
|
|
706
|
+
// the given def names (i.e., '#/$defs/Name' or '#/definitions/Name' where Name
|
|
707
|
+
// is in defNames). Used to detect whether a $defs body cycles back to a sibling.
|
|
708
|
+
function subtreeRefersToLocalDef(node, defNames) {
|
|
709
|
+
if (typeof node !== 'object' || node === null) return false
|
|
710
|
+
if (node.$ref) {
|
|
711
|
+
const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
|
|
712
|
+
if (m && defNames.has(m[1])) return true
|
|
713
|
+
}
|
|
714
|
+
for (const val of Object.values(node)) {
|
|
715
|
+
if (typeof val === 'object' && val !== null) {
|
|
716
|
+
if (Array.isArray(val)) {
|
|
717
|
+
for (const item of val) { if (subtreeRefersToLocalDef(item, defNames)) return true }
|
|
718
|
+
} else {
|
|
719
|
+
if (subtreeRefersToLocalDef(val, defNames)) return true
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return false
|
|
724
|
+
}
|
|
725
|
+
|
|
705
726
|
// Recursively check if a schema can be safely compiled to JS codegen.
|
|
706
727
|
// Returns false if any sub-schema contains features codegen gets wrong.
|
|
707
728
|
function codegenSafe(schema, schemaMap) {
|
|
@@ -809,6 +830,17 @@ function codegenSafe(schema, schemaMap) {
|
|
|
809
830
|
// Check $defs: targets must be safe, names must be simple, no nested $ref chains
|
|
810
831
|
const defs = schema.$defs || schema.definitions
|
|
811
832
|
if (defs) {
|
|
833
|
+
const defNames = new Set(Object.keys(defs))
|
|
834
|
+
// Detect cyclic local refs: if any def's own body references back to a sibling
|
|
835
|
+
// def, the schema contains a cycle. The JS codegen returns (() => true) on cycle
|
|
836
|
+
// (permissive), so cyclic schemas must route to the interpreted engine instead.
|
|
837
|
+
// We check only the def bodies, not the top-level schema, so that a plain
|
|
838
|
+
// { $ref: '#/$defs/Name', $defs: { Name: { type: 'string' } } } (no cycle)
|
|
839
|
+
// still compiles.
|
|
840
|
+
const hasCycle = Object.values(defs).some(
|
|
841
|
+
(def) => typeof def === 'object' && def !== null && subtreeRefersToLocalDef(def, defNames)
|
|
842
|
+
)
|
|
843
|
+
if (hasCycle) return false
|
|
812
844
|
for (const [name, def] of Object.entries(defs)) {
|
|
813
845
|
if (/[~/"']/.test(name)) return false // special chars in def name
|
|
814
846
|
if (typeof def === 'boolean') return false
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Browser stub: there is no native addon in the browser. Swapped in via the
|
|
4
|
-
// package.json `browser` field so bundlers
|
|
4
|
+
// package.json `browser` field so bundlers skip native platform probing.
|
|
5
5
|
|
|
6
6
|
module.exports = function loadNative() {
|
|
7
7
|
return null;
|