ata-validator 1.48.0 → 1.50.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/README.md +45 -7
- package/aot.d.ts +40 -0
- package/aot.mjs +1 -1
- package/bin/ata.js +14 -1
- package/build.d.ts +1 -1
- package/error-runtime.d.ts +24 -0
- package/error-runtime.js +40 -0
- package/error-runtime.mjs +3 -0
- package/index.browser.mjs +14 -5
- package/index.edge.js +14 -0
- package/index.edge.mjs +13 -0
- package/index.js +8 -996
- package/lib/aot-build.js +211 -68
- package/lib/aot-impl.js +216 -8
- package/lib/codegen-engine.js +1024 -0
- package/lib/draft7.js +106 -8
- package/lib/enrich-error.js +42 -11
- package/lib/enrich-site.js +1 -1
- package/lib/error-code-of.js +22 -0
- package/lib/error-runtime-entry.js +85 -0
- package/lib/error-runtime.js +152 -0
- package/lib/error-sort-rank.js +61 -0
- package/lib/interpreter-core.js +1356 -0
- package/lib/interpreter.js +8 -1260
- package/lib/js-compiler-errors.js +6 -4
- package/lib/js-compiler.js +50 -2
- package/lib/metaschemas.js +21 -2
- package/lib/public-api.js +80 -0
- package/lib/regex-linear.js +194 -37
- package/lib/safe-regex.js +23 -9
- package/lib/schema-runtime-entry.js +21 -0
- package/lib/schema-strip.js +115 -0
- package/lib/tools.mjs +21 -0
- package/lib/validator-core.js +35 -20
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +12 -0
- package/package.json +29 -9
package/lib/draft7.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
// draft-06 is read through the same rules: it differs from draft-07 only in
|
|
4
|
+
// lacking `if`/`then`/`else` (and the content and readOnly annotations), so a
|
|
5
|
+
// draft-06 schema is a draft-07 schema with those three keywords inert. Before
|
|
6
|
+
// this, a draft-06 schema was read under 2020-12 and its tuple `items`,
|
|
7
|
+
// `additionalItems` and `dependencies` were not applied: 30 cases of the
|
|
8
|
+
// official draft-06 suite accepted documents they must reject.
|
|
3
9
|
const DRAFT7_SCHEMAS = new Set([
|
|
4
10
|
'http://json-schema.org/draft-07/schema#',
|
|
5
11
|
'http://json-schema.org/draft-07/schema',
|
|
12
|
+
'http://json-schema.org/draft-06/schema#',
|
|
13
|
+
'http://json-schema.org/draft-06/schema',
|
|
14
|
+
])
|
|
15
|
+
const DRAFT6_SCHEMAS = new Set([
|
|
16
|
+
'http://json-schema.org/draft-06/schema#',
|
|
17
|
+
'http://json-schema.org/draft-06/schema',
|
|
6
18
|
])
|
|
7
19
|
|
|
8
20
|
function isDraft7(schema) {
|
|
9
|
-
return !!(schema && schema.$schema && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
21
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isDraft6(schema) {
|
|
25
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT6_SCHEMAS.has(schema.$schema))
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
// `force` applies the draft-07 rules to a document that declares no dialect
|
|
13
29
|
// of its own, which is how a retrieved schema inherits the root's draft.
|
|
14
|
-
|
|
30
|
+
// `draft6` says the document, or the root it inherits from, is draft-06.
|
|
31
|
+
function normalizeDraft7(schema, force, draft6) {
|
|
15
32
|
if (!force && !isDraft7(schema)) return schema
|
|
16
|
-
_normalize(schema)
|
|
33
|
+
_normalize(schema, draft6 === true || isDraft6(schema))
|
|
17
34
|
return schema
|
|
18
35
|
}
|
|
19
36
|
|
|
@@ -21,9 +38,13 @@ function normalizeDraft7(schema, force) {
|
|
|
21
38
|
// definitions are addressable by pointer, annotations are inert.
|
|
22
39
|
const REF_SIBLINGS_KEPT = new Set(['$ref', '$defs', 'definitions', '$schema', '$comment', 'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly'])
|
|
23
40
|
|
|
24
|
-
function _normalize(schema) {
|
|
41
|
+
function _normalize(schema, d6) {
|
|
25
42
|
if (typeof schema !== 'object' || schema === null) return
|
|
26
43
|
|
|
44
|
+
// draft-06 has no conditional: `if`, `then` and `else` are unknown keywords
|
|
45
|
+
// there, so they are dropped rather than applied.
|
|
46
|
+
if (d6) { delete schema.if; delete schema.then; delete schema.else }
|
|
47
|
+
|
|
27
48
|
// In draft-07 an object with `$ref` is that reference and nothing else:
|
|
28
49
|
// sibling keywords, `$id` included, are ignored. Dropping them here gives
|
|
29
50
|
// every engine the same reading without each having to know the draft.
|
|
@@ -74,7 +95,7 @@ function _normalize(schema) {
|
|
|
74
95
|
for (const key of objSubs) {
|
|
75
96
|
if (schema[key] && typeof schema[key] === 'object') {
|
|
76
97
|
for (const v of Object.values(schema[key])) {
|
|
77
|
-
if (typeof v === 'object' && v !== null) _normalize(v)
|
|
98
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
101
|
}
|
|
@@ -84,7 +105,7 @@ function _normalize(schema) {
|
|
|
84
105
|
for (const key of arrSubs) {
|
|
85
106
|
if (Array.isArray(schema[key])) {
|
|
86
107
|
for (const s of schema[key]) {
|
|
87
|
-
if (typeof s === 'object' && s !== null) _normalize(s)
|
|
108
|
+
if (typeof s === 'object' && s !== null) _normalize(s, d6)
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
}
|
|
@@ -94,7 +115,7 @@ function _normalize(schema) {
|
|
|
94
115
|
'additionalProperties', 'propertyNames']
|
|
95
116
|
for (const key of singleSubs) {
|
|
96
117
|
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
97
|
-
_normalize(schema[key])
|
|
118
|
+
_normalize(schema[key], d6)
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
}
|
|
@@ -248,4 +269,81 @@ function _stripFormat(schema, seen) {
|
|
|
248
269
|
}
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
// Draft 2019-09 differs from 2020-12, which every engine here reads, in two
|
|
273
|
+
// places: the array form of `items` with `additionalItems` (2020-12 spells
|
|
274
|
+
// them `prefixItems` and `items`), and `$recursiveRef`/`$recursiveAnchor`,
|
|
275
|
+
// which 2020-12 generalised into `$dynamicRef`/`$dynamicAnchor`. Both are
|
|
276
|
+
// rewritten to the 2020-12 spelling. Before, a 2019-09 schema was read as
|
|
277
|
+
// 2020-12 as written, and 38 cases of the official 2019-09 suite accepted
|
|
278
|
+
// documents they must reject.
|
|
279
|
+
//
|
|
280
|
+
// The recursive pair maps one to one: a resource whose root says
|
|
281
|
+
// `$recursiveAnchor: true` gets a `$dynamicAnchor` of a reserved name, and
|
|
282
|
+
// `$recursiveRef: "#"` becomes a `$dynamicRef` to that name when its resource
|
|
283
|
+
// carries the anchor, and a plain `$ref: "#"` when it does not, which is what
|
|
284
|
+
// 2019-09 says it means then. The outward search for the outermost anchored
|
|
285
|
+
// resource is the same rule in both drafts.
|
|
286
|
+
const DRAFT2019_SCHEMAS = new Set([
|
|
287
|
+
'https://json-schema.org/draft/2019-09/schema',
|
|
288
|
+
'https://json-schema.org/draft/2019-09/schema#',
|
|
289
|
+
])
|
|
290
|
+
const RECURSIVE_ANCHOR = '__ata_recursive_anchor'
|
|
291
|
+
|
|
292
|
+
function isDraft2019(schema) {
|
|
293
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT2019_SCHEMAS.has(schema.$schema))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const SUB_OBJECT = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
297
|
+
const SUB_ARRAY = ['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items']
|
|
298
|
+
const SUB_SINGLE = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'additionalProperties',
|
|
299
|
+
'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema']
|
|
300
|
+
|
|
301
|
+
function normalizeDraft2019(schema, force) {
|
|
302
|
+
if (!force && !isDraft2019(schema)) return schema
|
|
303
|
+
_normalize2019(schema, schema, false)
|
|
304
|
+
return schema
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _normalize2019(node, resourceRoot, isRoot) {
|
|
308
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) return
|
|
309
|
+
// A subschema with its own `$id` starts a new resource.
|
|
310
|
+
const root = !isRoot && typeof node.$id === 'string' && !node.$id.startsWith('#') ? node : resourceRoot
|
|
311
|
+
if (node.$recursiveAnchor === true && node === root) node.$dynamicAnchor = RECURSIVE_ANCHOR
|
|
312
|
+
delete node.$recursiveAnchor
|
|
313
|
+
if (typeof node.$recursiveRef === 'string') {
|
|
314
|
+
if (node.$recursiveRef === '#' && root.$dynamicAnchor === RECURSIVE_ANCHOR) node.$dynamicRef = '#' + RECURSIVE_ANCHOR
|
|
315
|
+
else if (node.$ref === undefined) node.$ref = node.$recursiveRef
|
|
316
|
+
delete node.$recursiveRef
|
|
317
|
+
}
|
|
318
|
+
// Recurse before the items rewrite, so tuple members are walked as members.
|
|
319
|
+
for (const key of SUB_OBJECT) {
|
|
320
|
+
const sub = node[key]
|
|
321
|
+
if (sub && typeof sub === 'object') for (const v of Object.values(sub)) _normalize2019(v, root, false)
|
|
322
|
+
}
|
|
323
|
+
for (const key of SUB_ARRAY) {
|
|
324
|
+
if (Array.isArray(node[key])) for (const v of node[key]) _normalize2019(v, root, false)
|
|
325
|
+
}
|
|
326
|
+
for (const key of SUB_SINGLE) {
|
|
327
|
+
const sub = node[key]
|
|
328
|
+
if (sub && typeof sub === 'object' && !Array.isArray(sub)) _normalize2019(sub, root, false)
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(node.items)) {
|
|
331
|
+
node.prefixItems = node.items
|
|
332
|
+
if (node.additionalItems !== undefined) { node.items = node.additionalItems; delete node.additionalItems } else delete node.items
|
|
333
|
+
} else {
|
|
334
|
+
delete node.additionalItems
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The draft a `$schema` value names, for the normalizers: 2 draft-06,
|
|
339
|
+
// 1 draft-07, 3 2019-09, 0 anything else. Takes the string so the caller
|
|
340
|
+
// reads the property once.
|
|
341
|
+
function draftCode(uri) {
|
|
342
|
+
if (typeof uri !== 'string') return 0
|
|
343
|
+
if (DRAFT6_SCHEMAS.has(uri)) return 2
|
|
344
|
+
if (DRAFT7_SCHEMAS.has(uri)) return 1
|
|
345
|
+
if (DRAFT2019_SCHEMAS.has(uri)) return 3
|
|
346
|
+
return 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { draftCode, isDraft7, isDraft6, isDraft2019, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
|
package/lib/enrich-error.js
CHANGED
|
@@ -77,7 +77,19 @@ function reprValue (v) {
|
|
|
77
77
|
if (t === 'number' || t === 'boolean') return String(v);
|
|
78
78
|
if (Array.isArray(v)) return `[array, ${v.length} items]`;
|
|
79
79
|
if (t === 'object') {
|
|
80
|
-
|
|
80
|
+
// The keys first: every key costs at least five characters serialised
|
|
81
|
+
// (`"k":` and a one-character value), so past twelve keys the 60-character
|
|
82
|
+
// body is out of reach and the size walk is skipped. The count is what the
|
|
83
|
+
// fallback prints, so it is taken once either way; the walk and the
|
|
84
|
+
// serialisation used to run on every container an error named, and a
|
|
85
|
+
// `required` or `unevaluatedProperties` error names its container.
|
|
86
|
+
let n;
|
|
87
|
+
try {
|
|
88
|
+
n = Object.keys(v).length;
|
|
89
|
+
} catch {
|
|
90
|
+
return '[object, unserializable]';
|
|
91
|
+
}
|
|
92
|
+
if (n <= 12 && jsonSizeWithin(v, 60) >= 0) {
|
|
81
93
|
try {
|
|
82
94
|
const s = JSON.stringify(v);
|
|
83
95
|
if (s !== undefined && s.length <= 60) return s;
|
|
@@ -85,12 +97,6 @@ function reprValue (v) {
|
|
|
85
97
|
return '[object, unserializable]';
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
|
-
let n;
|
|
89
|
-
try {
|
|
90
|
-
n = Object.keys(v).length;
|
|
91
|
-
} catch {
|
|
92
|
-
return '[object, unserializable]';
|
|
93
|
-
}
|
|
94
100
|
return `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
|
|
95
101
|
}
|
|
96
102
|
return `[${t}]`;
|
|
@@ -131,11 +137,12 @@ function resolveAt (err, data) {
|
|
|
131
137
|
*/
|
|
132
138
|
// Observation-first wording. `message` stays as it is for parity, so this is a
|
|
133
139
|
// separate field a consumer opts into.
|
|
134
|
-
function detailFor (err, out) {
|
|
140
|
+
function detailFor (err, out, at) {
|
|
135
141
|
const p = err.params || {};
|
|
136
142
|
switch (err.keyword) {
|
|
137
143
|
case 'type':
|
|
138
|
-
|
|
144
|
+
// From the value when the caller resolved it; from the repr otherwise.
|
|
145
|
+
return `expected ${p.type}, found ${(at === undefined || at === MISSING ? null : typeNameOfValue(at)) || typeNameOf(out.received)}`;
|
|
139
146
|
case 'required':
|
|
140
147
|
return `missing required property "${p.missingProperty}"`;
|
|
141
148
|
case 'additionalProperties':
|
|
@@ -157,6 +164,24 @@ function detailFor (err, out) {
|
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
// The same names from the value itself, for the callers that hold it: the
|
|
168
|
+
// repr is a string built to be read, and parsing it back cost a regex per
|
|
169
|
+
// type error. Agrees with typeNameOf on every value reprValue can produce:
|
|
170
|
+
// a number that is not finite prints as NaN or Infinity, which the repr
|
|
171
|
+
// parser calls a value, and so does this.
|
|
172
|
+
// An object with its own toJSON prints as whatever that returns, so it is
|
|
173
|
+
// left to the repr route (null here).
|
|
174
|
+
function typeNameOfValue (v) {
|
|
175
|
+
if (v === null) return 'null';
|
|
176
|
+
switch (typeof v) {
|
|
177
|
+
case 'boolean': return 'boolean';
|
|
178
|
+
case 'string': return 'string';
|
|
179
|
+
case 'number': return Number.isFinite(v) ? 'number' : 'value';
|
|
180
|
+
case 'object': return Array.isArray(v) ? 'array' : typeof v.toJSON === 'function' ? null : 'object';
|
|
181
|
+
default: return 'value';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
// Derived from the repr in `received`, which is already a JSON-ish string.
|
|
161
186
|
function typeNameOf (received) {
|
|
162
187
|
if (received === undefined) return 'nothing';
|
|
@@ -252,7 +277,13 @@ function enrich (rawErr, opts) {
|
|
|
252
277
|
// renderer can surface the closest variant's diagnostics. They are enriched
|
|
253
278
|
// like any other error: passed through raw, they carried whatever the engine
|
|
254
279
|
// that answered wrote, the generator's internal ordering key included.
|
|
255
|
-
|
|
280
|
+
// A branch error the rich combined function already built is kept:
|
|
281
|
+
// enriching it again recomputed every field to the same values, and a
|
|
282
|
+
// collapsed oneOf paid that for each branch error on top of its own
|
|
283
|
+
// enrichment. Such an error carries `rank`, which only this enrichment and
|
|
284
|
+
// makeRich write; a standalone module's error carries a docUrl but no
|
|
285
|
+
// rank and is enriched as before (fromCompiled wraps those).
|
|
286
|
+
if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => (b !== null && typeof b === 'object' && b.rank !== undefined && b.docUrl !== undefined && b._o === undefined) ? b : enrich(b, data !== undefined ? { data } : null));
|
|
256
287
|
|
|
257
288
|
// The frames come from the position map, which is a walk of the document.
|
|
258
289
|
// Given lazily (validateJSON's rejections), the walk waits for the first
|
|
@@ -286,7 +317,7 @@ function enrich (rawErr, opts) {
|
|
|
286
317
|
const sugg = suggestFor(out, data, at === MISSING ? undefined : at);
|
|
287
318
|
if (sugg) out.suggestion = sugg;
|
|
288
319
|
|
|
289
|
-
const detail = detailFor(rawErr, out);
|
|
320
|
+
const detail = detailFor(rawErr, out, at);
|
|
290
321
|
if (detail !== undefined) out.detail = detail;
|
|
291
322
|
out.rank = st.rank;
|
|
292
323
|
|
package/lib/enrich-site.js
CHANGED
|
@@ -49,7 +49,7 @@ function makeRich (site, path, params, message, value, root, spPrefix) {
|
|
|
49
49
|
};
|
|
50
50
|
const sugg = suggestFor(out, root, at === MISSING ? undefined : at);
|
|
51
51
|
if (sugg) out.suggestion = sugg;
|
|
52
|
-
const detail = detailFor({ keyword: site.keyword, params }, out);
|
|
52
|
+
const detail = detailFor({ keyword: site.keyword, params }, out, at);
|
|
53
53
|
if (detail !== undefined) out.detail = detail;
|
|
54
54
|
out.rank = site.rank;
|
|
55
55
|
return out;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The code a module stamps on an error, from its keyword (and format): the
|
|
4
|
+
// first code in sorted order that names the keyword, the same rule as codeFor
|
|
5
|
+
// in lib/error-codes.js and the table a module carries (lib/aot-impl.js).
|
|
6
|
+
const { CODES } = require('./error-codes');
|
|
7
|
+
|
|
8
|
+
const byKw = {};
|
|
9
|
+
const byFmt = {};
|
|
10
|
+
for (const code of Object.keys(CODES).sort()) {
|
|
11
|
+
const c = CODES[code];
|
|
12
|
+
if (byKw[c.keyword] === undefined) byKw[c.keyword] = code;
|
|
13
|
+
if (c.keyword === 'format' && c.format && byFmt[c.format] === undefined) byFmt[c.format] = code;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function codeOf(keyword, format) {
|
|
17
|
+
if (keyword === 'format' && format) { const h = byFmt[format]; return h === undefined ? 'ATA3099' : h; }
|
|
18
|
+
const h = byKw[keyword];
|
|
19
|
+
return h === undefined ? null : h;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { codeOf, byKw, byFmt };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The error runtime that compiled modules share. A module built in the
|
|
4
|
+
// compact shape carries only its verdict function and its schema with the
|
|
5
|
+
// annotations taken out; when a document fails, it hands the document to this
|
|
6
|
+
// runtime, which walks the same schema with the interpreted engine and returns
|
|
7
|
+
// the error list. One copy serves every module of a build, so the error detail
|
|
8
|
+
// a module used to carry as generated code is paid for once.
|
|
9
|
+
//
|
|
10
|
+
// What comes back is what `validate()` returns on the runtime Validator with
|
|
11
|
+
// `richErrors: false`, plus the `code` and `docUrl` a module stamps: the same
|
|
12
|
+
// errors, in schema declaration order.
|
|
13
|
+
//
|
|
14
|
+
// It loads the interpreter without its plan compiler, which adds speed and
|
|
15
|
+
// no answers: lib/error-runtime.js packs this file and what it requires into
|
|
16
|
+
// the runtime a build writes, and `ata-validator/error-runtime` exports it
|
|
17
|
+
// for bundler plugins.
|
|
18
|
+
|
|
19
|
+
const { createInterpreter } = require('./interpreter-core');
|
|
20
|
+
const { sortErrorsByRank } = require('./error-sort-rank');
|
|
21
|
+
|
|
22
|
+
const DOC = 'https://ata-validator.com/e/';
|
|
23
|
+
|
|
24
|
+
function stamp(errors, codeOf) {
|
|
25
|
+
const out = new Array(errors.length);
|
|
26
|
+
for (let i = 0; i < errors.length; i++) {
|
|
27
|
+
const e = errors[i];
|
|
28
|
+
// A collapsed oneOf/anyOf error comes with its code and no docUrl, as
|
|
29
|
+
// the module's own collector leaves it; only its branches are stamped.
|
|
30
|
+
if (e.code !== undefined) {
|
|
31
|
+
const x = {};
|
|
32
|
+
for (const k in e) if (k !== '_o' && k !== 'branchErrors') x[k] = e[k];
|
|
33
|
+
if (Array.isArray(e.branchErrors)) x.branchErrors = stamp(e.branchErrors, codeOf);
|
|
34
|
+
out[i] = x;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let code;
|
|
38
|
+
{
|
|
39
|
+
code = e.keyword === 'required' && typeof e.schemaPath === 'string' && e.schemaPath.endsWith('/dependentRequired')
|
|
40
|
+
? 'ATA7005'
|
|
41
|
+
: codeOf(e.keyword, e.params && e.params.format);
|
|
42
|
+
}
|
|
43
|
+
const x = code === null || code === undefined ? {} : { code };
|
|
44
|
+
for (const k in e) if (k !== 'code' && k !== '_o' && k !== 'docUrl' && k !== 'branchErrors') x[k] = e[k];
|
|
45
|
+
if (code !== null && code !== undefined) x.docUrl = DOC + code;
|
|
46
|
+
if (Array.isArray(e.branchErrors)) x.branchErrors = stamp(e.branchErrors, codeOf);
|
|
47
|
+
out[i] = x;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The error list for a document `interp` rejects, as a module reports it, or
|
|
53
|
+
// null when there is no detail to give.
|
|
54
|
+
function errorsOf(interp, schema, codeOf, data) {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = interp.validate(data).errors;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Data nested deeper than the walk's stack allows: no detail, which the
|
|
60
|
+
// module reports as its ATA9000 stub. The verdict came from the module.
|
|
61
|
+
if (e instanceof RangeError) return null;
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
if (!raw || raw.length === 0) return null;
|
|
65
|
+
const ordered = raw.length > 1 || (raw[0] && Array.isArray(raw[0].branchErrors))
|
|
66
|
+
? sortErrorsByRank(schema, raw)
|
|
67
|
+
: raw;
|
|
68
|
+
return stamp(ordered, codeOf);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function interpreterOptions(options) {
|
|
72
|
+
return { v1: !!(options && options.v1), flat: !!(options && options.flat) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// schema: the schema the module's verdict was compiled from, after dialect
|
|
76
|
+
// normalization. codeOf(keyword, format): the module's own code table.
|
|
77
|
+
function createErrors(schema, codeOf, options) {
|
|
78
|
+
let interp = null;
|
|
79
|
+
return function errorsFor(data) {
|
|
80
|
+
if (interp === null) interp = createInterpreter(schema, interpreterOptions(options));
|
|
81
|
+
return errorsOf(interp, schema, codeOf, data);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { createErrors, errorsOf, interpreterOptions };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Source of the error runtime that compact modules import (see
|
|
4
|
+
// lib/error-runtime-entry.js for what it does). It is put together from the
|
|
5
|
+
// library's own files when a build asks for it, so there is no second copy of
|
|
6
|
+
// the interpreter to keep in step: each file becomes a function in a small
|
|
7
|
+
// module table, and require() inside them resolves against that table. The
|
|
8
|
+
// interpreter comes without its plan compiler (lib/interpreter-core.js), which
|
|
9
|
+
// produces the same errors.
|
|
10
|
+
//
|
|
11
|
+
// The output is not minified. It goes through the application's bundler like
|
|
12
|
+
// the modules that import it.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const FILES = [
|
|
18
|
+
'error-runtime-entry',
|
|
19
|
+
'interpreter-core',
|
|
20
|
+
'error-sort-rank',
|
|
21
|
+
'schema-order',
|
|
22
|
+
'branch-collapse',
|
|
23
|
+
'safe-regex',
|
|
24
|
+
'unique-items',
|
|
25
|
+
'formats',
|
|
26
|
+
];
|
|
27
|
+
// The parts a build can leave out when none of its schemas needs them. The
|
|
28
|
+
// decision is made on the schemas the runtime walks (as embedded, see
|
|
29
|
+
// runtimeFeatures) and errs toward keeping a part: a key with that name
|
|
30
|
+
// anywhere, even inside an enum value, keeps it. A part left out is replaced
|
|
31
|
+
// by a stub that throws, which a schema without that keyword cannot reach.
|
|
32
|
+
const OPTIONAL = {
|
|
33
|
+
formats: { keys: ['format'], stub: 'module.exports = {};' },
|
|
34
|
+
'safe-regex': { keys: ['pattern', 'patternProperties'], stub: "module.exports = { compileSafe() { throw new Error('ata error runtime: built without the pattern engine'); } };" },
|
|
35
|
+
'unique-items': { keys: ['uniqueItems', 'enum', 'const'], stub: "function no() { throw new Error('ata error runtime: built without deep equality'); }\nmodule.exports = { allDistinct: no, duplicatePair: no, _deq: no };" },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Which optional parts these schemas need: { formats, 'safe-regex',
|
|
39
|
+
// 'unique-items' } as booleans.
|
|
40
|
+
function runtimeFeatures(schemas) {
|
|
41
|
+
const keys = new Set();
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const walk = (n) => {
|
|
44
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return;
|
|
45
|
+
seen.add(n);
|
|
46
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return; }
|
|
47
|
+
for (const k of Object.keys(n)) { keys.add(k); walk(n[k]); }
|
|
48
|
+
};
|
|
49
|
+
for (const s of schemas) walk(s);
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [part, { keys: ks }] of Object.entries(OPTIONAL)) out[part] = ks.some((k) => keys.has(k));
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function codeTable() {
|
|
56
|
+
const { byKw, byFmt } = require('./error-code-of');
|
|
57
|
+
return { byKw, byFmt };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Lines that hold nothing but a // comment are about half of these files.
|
|
61
|
+
// They go, unless the line sits inside a template literal (tracked by
|
|
62
|
+
// counting backticks on the lines before it) or the file is one whose
|
|
63
|
+
// templates carry such lines on purpose (formats.js builds code from them).
|
|
64
|
+
const KEEP_COMMENTS = new Set(['formats']);
|
|
65
|
+
function dropCommentLines(src, name) {
|
|
66
|
+
if (KEEP_COMMENTS.has(name)) return src;
|
|
67
|
+
const out = [];
|
|
68
|
+
let inTemplate = false;
|
|
69
|
+
for (const line of src.split('\n')) {
|
|
70
|
+
if (!inTemplate && /^\s*\/\//.test(line)) continue;
|
|
71
|
+
if (!inTemplate && line.trim() === '') continue;
|
|
72
|
+
out.push(line);
|
|
73
|
+
const ticks = (line.match(/`/g) || []).length;
|
|
74
|
+
if (ticks % 2 === 1) inTemplate = !inTemplate;
|
|
75
|
+
}
|
|
76
|
+
return out.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const _cache = new Map();
|
|
80
|
+
|
|
81
|
+
// The files a schema-mode runtime adds: the verdict comes from the
|
|
82
|
+
// interpreter too, with its plan compiler for schemas used often.
|
|
83
|
+
const SCHEMA_FILES = ['schema-runtime-entry', 'interpreter', 'plan-compiler'];
|
|
84
|
+
|
|
85
|
+
// features: from runtimeFeatures, or omitted for a runtime with every part.
|
|
86
|
+
// schemaMode: also carry the verdict (createValidator), for schema modules.
|
|
87
|
+
function errorRuntimeSource(format, version, features, schemaMode) {
|
|
88
|
+
const want = (part) => !features || features[part] !== false;
|
|
89
|
+
const key = (format === 'cjs' ? 'cjs' : 'esm') + ':' + Object.keys(OPTIONAL).map((p) => (want(p) ? '1' : '0')).join('') + (schemaMode ? ':schema' : '');
|
|
90
|
+
const hit = _cache.get(key);
|
|
91
|
+
if (hit !== undefined) return hit;
|
|
92
|
+
const defs = [];
|
|
93
|
+
for (const name of schemaMode ? FILES.concat(SCHEMA_FILES) : FILES) {
|
|
94
|
+
const src = OPTIONAL[name] !== undefined && !want(name)
|
|
95
|
+
? OPTIONAL[name].stub
|
|
96
|
+
: dropCommentLines(fs.readFileSync(path.join(__dirname, name + '.js'), 'utf8'), name);
|
|
97
|
+
defs.push(`${JSON.stringify(name)}: function (module, exports, require) {\n${src}\n}`);
|
|
98
|
+
}
|
|
99
|
+
const { byKw, byFmt } = codeTable();
|
|
100
|
+
const body = `// Auto-generated by ata-validator ${version}, do not edit.
|
|
101
|
+
// The error runtime shared by the compact modules of one build. It imports
|
|
102
|
+
// nothing. Re-run the build after upgrading ata.
|
|
103
|
+
'use strict';
|
|
104
|
+
const _ataDefs = {
|
|
105
|
+
${defs.join(',\n')}
|
|
106
|
+
};
|
|
107
|
+
const _ataCache = {};
|
|
108
|
+
function _ataRequire(spec) {
|
|
109
|
+
const name = spec.replace(/^\\.\\//, '').replace(/\\.js$/, '');
|
|
110
|
+
const hit = _ataCache[name];
|
|
111
|
+
if (hit !== undefined) return hit.exports;
|
|
112
|
+
const def = _ataDefs[name];
|
|
113
|
+
if (def === undefined) throw new Error('ata error runtime: missing module ' + spec);
|
|
114
|
+
const module = { exports: {} };
|
|
115
|
+
_ataCache[name] = module;
|
|
116
|
+
def(module, module.exports, _ataRequire);
|
|
117
|
+
return module.exports;
|
|
118
|
+
}
|
|
119
|
+
const _ataKw = ${JSON.stringify(byKw)};
|
|
120
|
+
const _ataFmt = ${JSON.stringify(byFmt)};
|
|
121
|
+
function _ataCode(k, f) {
|
|
122
|
+
if (k === 'format' && f) { const h = _ataFmt[f]; return h === undefined ? 'ATA3099' : h; }
|
|
123
|
+
const h = _ataKw[k];
|
|
124
|
+
return h === undefined ? null : h;
|
|
125
|
+
}
|
|
126
|
+
// The modules are evaluated on the first call, not when this file loads.
|
|
127
|
+
function createErrors(schema, options) { return _ataRequire('error-runtime-entry').createErrors(schema, _ataCode, options); }
|
|
128
|
+
function compileSafe(src) { return _ataRequire('safe-regex').compileSafe(src); }
|
|
129
|
+
${schemaMode ? "function createValidator(schema, options) { return _ataRequire('schema-runtime-entry').createValidator(schema, _ataCode, options); }\n" : ''}
|
|
130
|
+
const ataVersion = ${JSON.stringify(version)};
|
|
131
|
+
|
|
132
|
+
// Warm-up, in a browser only: the runtime's code compiles on its first use,
|
|
133
|
+
// which a page would otherwise pay for on its first rejected document, about
|
|
134
|
+
// a millisecond. When the browser is idle, one small error list is built here
|
|
135
|
+
// instead, once, so the first real one finds the code compiled. Nothing it
|
|
136
|
+
// does reaches a caller.
|
|
137
|
+
const WARM_SCHEMA = { type: 'object', properties: { a: { type: 'string' }, b: { type: 'integer', minimum: 0 }, c: { anyOf: [{ type: 'string' }, { type: 'null' }] }, f: { type: 'string', format: 'email' } }, required: ['d'], additionalProperties: false };
|
|
138
|
+
function warm() { try { createErrors(WARM_SCHEMA, { flat: true })({ a: 1, b: -1, c: 2, f: 'x', e: 0 }); } catch (_) {} }
|
|
139
|
+
if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm); } catch (_) {} }
|
|
140
|
+
// In Node the same happens once the event loop is first free after start-up,
|
|
141
|
+
// on a timer that does not keep the process alive: a script that exits at
|
|
142
|
+
// once never runs it, and a server has it done before its first request.
|
|
143
|
+
else if (typeof process === 'object' && process !== null && process.versions && process.versions.node && typeof setTimeout === 'function') { try { const t = setTimeout(warm, 0); if (t && typeof t.unref === 'function') t.unref(); } catch (_) {} }
|
|
144
|
+
`;
|
|
145
|
+
const out = format === 'cjs'
|
|
146
|
+
? body + `module.exports = { createErrors, compileSafe, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`
|
|
147
|
+
: body + `export { createErrors, compileSafe, ${schemaMode ? 'createValidator, ' : ''}ataVersion };\n`;
|
|
148
|
+
_cache.set(key, out);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { errorRuntimeSource, runtimeFeatures, codeTable };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Declaration-order sorting for the shared error runtime
|
|
4
|
+
// (lib/error-runtime-entry.js), in its own file so the runtime Validator and
|
|
5
|
+
// ata-validator/lite, which sort by ordinals (lib/error-sort.js), do not carry
|
|
6
|
+
// it.
|
|
7
|
+
const { rankFor: schemaRank } = require('./schema-order');
|
|
8
|
+
|
|
9
|
+
// The same order from ranks alone: a rank is the path of child indexes from
|
|
10
|
+
// the root to the node an error names, built by walking that one path, while
|
|
11
|
+
// the ordinals above number every node of the schema on the first sort. The
|
|
12
|
+
// shared error runtime sorts this way, so a rejection costs a walk of the
|
|
13
|
+
// paths it reports, not of the schema. Comparing ranks segment by segment is
|
|
14
|
+
// the pre-order relation the ordinals encode (see lib/schema-order.js), and a
|
|
15
|
+
// path behind a local `$ref` ranks inside its target as the ordinal's tie
|
|
16
|
+
// does; the runtime only takes schemas whose references stay inside them. An
|
|
17
|
+
// error with no place in the document keeps the place of the one before it.
|
|
18
|
+
function compareRanks(a, b) {
|
|
19
|
+
const m = Math.min(a.length, b.length);
|
|
20
|
+
for (let k = 0; k < m; k++) if (a[k] !== b[k]) return a[k] - b[k];
|
|
21
|
+
return a.length - b.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sortErrorsByRank(rootSchema, errors) {
|
|
25
|
+
let out = null;
|
|
26
|
+
for (let i = 0; i < errors.length; i++) {
|
|
27
|
+
const e = errors[i];
|
|
28
|
+
const b = e !== null && typeof e === 'object' ? e.branchErrors : undefined;
|
|
29
|
+
if (!Array.isArray(b) || b.length === 0) continue;
|
|
30
|
+
const sb = sortErrorsByRank(rootSchema, b);
|
|
31
|
+
if (sb === b) continue;
|
|
32
|
+
if (out === null) out = errors.slice();
|
|
33
|
+
out[i] = Object.assign({}, e, { branchErrors: sb });
|
|
34
|
+
}
|
|
35
|
+
if (out !== null) errors = out;
|
|
36
|
+
const n = errors.length;
|
|
37
|
+
if (n < 2) return errors;
|
|
38
|
+
const keys = new Array(n);
|
|
39
|
+
let prev = [];
|
|
40
|
+
let sorted = true;
|
|
41
|
+
for (let i = 0; i < n; i++) {
|
|
42
|
+
let r = schemaRank(rootSchema, errors[i].schemaPath);
|
|
43
|
+
if (r === null) r = prev;
|
|
44
|
+
keys[i] = r;
|
|
45
|
+
if (i > 0 && compareRanks(prev, r) > 0) sorted = false;
|
|
46
|
+
prev = r;
|
|
47
|
+
}
|
|
48
|
+
if (sorted) return errors;
|
|
49
|
+
const res = errors.slice();
|
|
50
|
+
for (let i = 1; i < n; i++) {
|
|
51
|
+
const k = keys[i];
|
|
52
|
+
const e = res[i];
|
|
53
|
+
let j = i - 1;
|
|
54
|
+
while (j >= 0 && compareRanks(keys[j], k) > 0) { keys[j + 1] = keys[j]; res[j + 1] = res[j]; j--; }
|
|
55
|
+
keys[j + 1] = k;
|
|
56
|
+
res[j + 1] = e;
|
|
57
|
+
}
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { sortErrorsByRank };
|