ata-validator 1.47.0 → 1.49.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 -3
- package/index.js +22 -4
- package/lib/aot-impl.js +9 -1
- package/lib/draft7.js +106 -8
- package/lib/enrich-error.js +88 -12
- package/lib/enrich-site.js +1 -1
- package/lib/js-compiler-errors.js +103 -8
- package/lib/js-compiler.js +119 -7
- package/lib/metaschemas.js +21 -2
- package/lib/regex-linear.js +194 -37
- package/lib/rejections.js +27 -11
- package/lib/safe-regex.js +23 -9
- package/lib/scan-compiler.js +14 -6
- package/lib/validator-core.js +23 -16
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +12 -0
- package/lite.d.ts +12 -0
- package/lite.js +20 -0
- package/lite.mjs +3 -0
- package/package.json +18 -9
package/README.md
CHANGED
|
@@ -43,6 +43,16 @@ npm install ata-validator --omit=optional
|
|
|
43
43
|
|
|
44
44
|
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
|
|
45
45
|
|
|
46
|
+
Where the size of what ships matters more than nanoseconds per call, `ata-validator/lite` is
|
|
47
|
+
the same `Validator` on the interpreted engine alone, without the code generator: the same
|
|
48
|
+
verdicts and errors, 58 KB gzipped in a bundled worker against 136 KB for the full entry.
|
|
49
|
+
It leaves out what needs the generator, `parse()` and the ahead-of-time bundle methods.
|
|
50
|
+
|
|
51
|
+
On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
|
|
52
|
+
re-exports the npm one at the same version, and CI runs the official suite under Deno. The
|
|
53
|
+
details, and what Deno Deploy's refusal of dynamic code means for it, are in
|
|
54
|
+
[docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
55
|
+
|
|
46
56
|
Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
47
57
|
|
|
48
58
|
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
@@ -99,6 +109,29 @@ file.
|
|
|
99
109
|
passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
|
|
100
110
|
declared in a pending harness change that waits for a Bowtie release.
|
|
101
111
|
|
|
112
|
+
### Side by side
|
|
113
|
+
|
|
114
|
+
[validator-benchmarks](https://github.com/ata-core/validator-benchmarks) is ata's own
|
|
115
|
+
comparison, so it comes after the ones above: eleven validators on three realistic bodies
|
|
116
|
+
and four scenes, each library in its own process, 27 samples per cell, every schema and
|
|
117
|
+
document in the repository. On an order body with formats and a pattern, 900 valid
|
|
118
|
+
documents, Node 24:
|
|
119
|
+
|
|
120
|
+
[](https://github.com/ata-core/validator-benchmarks/blob/main/media/race-order-verdict-valid.mp4)
|
|
121
|
+
|
|
122
|
+
| | documents a second, the verdict | same, `new Function` refused |
|
|
123
|
+
|---|---:|---:|
|
|
124
|
+
| ata | 3,295,880 | 607,614 |
|
|
125
|
+
| ata, compiled module | 3,319,594 | 3,344,948 |
|
|
126
|
+
| typebox | 1,638,813 | EvalError |
|
|
127
|
+
| ajv | 1,285,492 | EvalError |
|
|
128
|
+
| typia | 939,572 | 964,507 |
|
|
129
|
+
| zod | 440,473 | 237,176 |
|
|
130
|
+
|
|
131
|
+
Where ata is behind is in the same tables: on a four-field body that fails, ajv answers the
|
|
132
|
+
verdict faster, and with every error read it does 6.6M a second against ata's 2.9M with rich
|
|
133
|
+
errors (7.6M with `richErrors: false`).
|
|
134
|
+
|
|
102
135
|
## Why AOT
|
|
103
136
|
|
|
104
137
|
| Dimension | Schema | ata-AOT | runtime validator | Difference |
|
|
@@ -244,13 +277,13 @@ is the most common way to get a misleading number out of this library.
|
|
|
244
277
|
|
|
245
278
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
246
279
|
|---|---|---|
|
|
247
|
-
| In a bundle, gzipped | **2.1 KB** |
|
|
280
|
+
| In a bundle, gzipped | **2.1 KB** | 126.1 KB |
|
|
248
281
|
| Time to a served request | **3.5 ms** | 7.6 ms |
|
|
249
282
|
| Schema known when | build time | any time |
|
|
250
283
|
|
|
251
284
|
The bundle row is the ten-field user schema in
|
|
252
285
|
`tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
|
|
253
|
-
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.
|
|
286
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.48.0.
|
|
254
287
|
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
|
|
255
288
|
`benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
|
|
256
289
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
@@ -280,6 +313,15 @@ const result = v.validate(data)
|
|
|
280
313
|
|
|
281
314
|
The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. `npx ata migrate` reports what the switch would change in a project, and what it cannot translate, before anything is written. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
|
|
282
315
|
|
|
316
|
+
Coming from the default validator, one command reports what the switch touches and a second applies the mechanical part:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npx ata migrate # per file: imports to rewrite, addFormats() calls that go, sites to look at by hand
|
|
320
|
+
npx ata migrate --write # rewrites the imports to ata-validator/compat and removes the calls, leaves the rest
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Coercion, defaults, `allErrors` and the error objects behave as before; the details and the limits are in [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
|
|
324
|
+
|
|
283
325
|
## Usage
|
|
284
326
|
|
|
285
327
|
### Node.js
|
|
@@ -684,7 +726,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
|
|
|
684
726
|
|
|
685
727
|
### Dialects
|
|
686
728
|
|
|
687
|
-
`$schema` selects the dialect. Draft 2020-12 is the default
|
|
729
|
+
`$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
|
|
688
730
|
|
|
689
731
|
Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
|
|
690
732
|
|
package/index.js
CHANGED
|
@@ -814,22 +814,40 @@ function hasDefaultsInside(node, seen) {
|
|
|
814
814
|
function emitDefaults(node, ov, lines, st) {
|
|
815
815
|
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
816
816
|
st.seen.add(node);
|
|
817
|
+
// A default is written only where the key is not an own property, and the
|
|
818
|
+
// own-property test used to be Object.hasOwn for every default on every
|
|
819
|
+
// call: on a configuration schema of 82 defaults that was 820 ns a call
|
|
820
|
+
// for a document that already carried every key, against 12 ns with the
|
|
821
|
+
// pass off. For a plain object (prototype Object.prototype) a key the
|
|
822
|
+
// prototype does not have is own exactly when reading it gives a value, so
|
|
823
|
+
// the read, an inline-cached load, settles the common case and hasOwn is
|
|
824
|
+
// consulted only when the read gives undefined. Keys Object.prototype does
|
|
825
|
+
// have (constructor, toString, __proto__) keep the hasOwn test, since the
|
|
826
|
+
// read would find the inherited one; so does any object with another
|
|
827
|
+
// prototype, where a read may find an inherited value of the same name.
|
|
828
|
+
const pk = '_pk' + st.n++;
|
|
829
|
+
lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`);
|
|
817
830
|
for (const [key, prop] of Object.entries(node.properties || {})) {
|
|
818
831
|
if (!prop || typeof prop !== 'object') continue;
|
|
819
832
|
const k = JSON.stringify(key);
|
|
833
|
+
const inherited = key in Object.prototype;
|
|
834
|
+
const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
|
|
820
835
|
if (prop.default !== undefined) {
|
|
821
836
|
const def = JSON.stringify(prop.default);
|
|
822
837
|
// Assignment to a key named __proto__ hits the prototype setter
|
|
823
838
|
// instead of creating a property; defineProperty writes an own key.
|
|
824
839
|
lines.push(key === '__proto__'
|
|
825
|
-
? `if(
|
|
826
|
-
: `if(
|
|
840
|
+
? `if(${absent})Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
|
|
841
|
+
: `if(${absent})${ov}[${k}]=${def}`);
|
|
827
842
|
}
|
|
828
843
|
// Into an own property that holds an object, arrays included, as the
|
|
829
|
-
// closure pass walks it.
|
|
844
|
+
// closure pass walks it. Object.prototype's own properties are functions
|
|
845
|
+
// apart from the __proto__ accessor, so on a plain object a read that
|
|
846
|
+
// gives an object is an own property for every other key.
|
|
830
847
|
if (prop.properties && hasDefaultsInside(prop, new Set())) {
|
|
831
848
|
const n = '_d' + st.n++;
|
|
832
|
-
|
|
849
|
+
const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
|
|
850
|
+
lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
|
|
833
851
|
emitDefaults(prop, n, lines, st);
|
|
834
852
|
lines.push('}}');
|
|
835
853
|
}
|
package/lib/aot-impl.js
CHANGED
|
@@ -113,6 +113,9 @@ function toStandalone(validator) {
|
|
|
113
113
|
// Also capture error function source for zero-compile standalone load
|
|
114
114
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
115
115
|
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
116
|
+
null,
|
|
117
|
+
null,
|
|
118
|
+
{ noOrdinal: true },
|
|
116
119
|
);
|
|
117
120
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
118
121
|
|
|
@@ -388,11 +391,14 @@ function toStandaloneModule(validator, opts) {
|
|
|
388
391
|
`const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
|
|
389
392
|
`${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
|
|
390
393
|
} else if (!abortEarly) {
|
|
394
|
+
// A module returns the collector's errors as they are and never sorts
|
|
395
|
+
// them, so the ordinal the runtime sorts by (`_o`) has no reader there;
|
|
396
|
+
// it leaked into every error object a module reported.
|
|
391
397
|
jsErrFn = compileToJSCodegenWithErrors(
|
|
392
398
|
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
393
399
|
null,
|
|
394
400
|
validator._userFormats,
|
|
395
|
-
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames } :
|
|
401
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal: true } : { noOrdinal: true },
|
|
396
402
|
);
|
|
397
403
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
398
404
|
if (errSrc && jsErrFn._errFactory) {
|
|
@@ -665,6 +671,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
665
671
|
v._schemaObj,
|
|
666
672
|
v._schemaMap,
|
|
667
673
|
v._userFormats,
|
|
674
|
+
{ noOrdinal: true },
|
|
668
675
|
);
|
|
669
676
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
670
677
|
return { v, jsFn, jsErrFn };
|
|
@@ -757,6 +764,7 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
757
764
|
v._schemaObj,
|
|
758
765
|
v._schemaMap,
|
|
759
766
|
v._userFormats,
|
|
767
|
+
{ noOrdinal: true },
|
|
760
768
|
);
|
|
761
769
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
762
770
|
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
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,11 +277,25 @@ 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
|
|
|
288
|
+
// The frames come from the position map, which is a walk of the document.
|
|
289
|
+
// Given lazily (validateJSON's rejections), the walk waits for the first
|
|
290
|
+
// read of any error's `dataFrame` or `anchor`: a caller that reads the
|
|
291
|
+
// messages and paths never pays for it, and one that reads every frame
|
|
292
|
+
// pays once, as before. Given eagerly, the fields are set as before.
|
|
293
|
+
const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
|
|
257
294
|
if (positions && positions[path]) {
|
|
258
295
|
const p = positions[path];
|
|
259
296
|
out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
297
|
+
} else if (!positions && opts && opts.lazyPositions) {
|
|
298
|
+
defineLazyFrames(out, path, named, opts.lazyPositions);
|
|
260
299
|
}
|
|
261
300
|
|
|
262
301
|
// Attach schema source frame when the validator was constructed with a
|
|
@@ -278,7 +317,7 @@ function enrich (rawErr, opts) {
|
|
|
278
317
|
const sugg = suggestFor(out, data, at === MISSING ? undefined : at);
|
|
279
318
|
if (sugg) out.suggestion = sugg;
|
|
280
319
|
|
|
281
|
-
const detail = detailFor(rawErr, out);
|
|
320
|
+
const detail = detailFor(rawErr, out, at);
|
|
282
321
|
if (detail !== undefined) out.detail = detail;
|
|
283
322
|
out.rank = st.rank;
|
|
284
323
|
|
|
@@ -286,7 +325,6 @@ function enrich (rawErr, opts) {
|
|
|
286
325
|
// adds the key span so a caret can sit on the property that is wrong rather
|
|
287
326
|
// than on the object containing it.
|
|
288
327
|
if (opts && opts.positions) {
|
|
289
|
-
const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
|
|
290
328
|
const own = opts.positions[path];
|
|
291
329
|
const child = named ? opts.positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
292
330
|
const src = child || own;
|
|
@@ -303,4 +341,42 @@ function enrich (rawErr, opts) {
|
|
|
303
341
|
return out;
|
|
304
342
|
}
|
|
305
343
|
|
|
344
|
+
// `dataFrame` and `anchor` as accessors that resolve the position map on the
|
|
345
|
+
// first read of either, on any error of the list (the map is shared through
|
|
346
|
+
// `lazy`), and then become plain data properties, or go away when the map has
|
|
347
|
+
// no entry for the error, which is the shape the eager path gives. Both are
|
|
348
|
+
// enumerable, so JSON.stringify and a deep comparison read them as values;
|
|
349
|
+
// a write replaces the accessor the way an assignment to a plain field would.
|
|
350
|
+
function defineLazyFrames (out, path, named, lazy) {
|
|
351
|
+
let done = false;
|
|
352
|
+
let frames = null;
|
|
353
|
+
const resolve = () => {
|
|
354
|
+
if (done) return frames;
|
|
355
|
+
done = true;
|
|
356
|
+
const positions = lazy();
|
|
357
|
+
const own = positions ? positions[path] : null;
|
|
358
|
+
const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
|
|
359
|
+
const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
360
|
+
const src = child || own;
|
|
361
|
+
let anchor;
|
|
362
|
+
if (src) {
|
|
363
|
+
anchor = { line: src.line, col: src.col, length: src.length };
|
|
364
|
+
if (src.keyOffset !== undefined) {
|
|
365
|
+
anchor.keyLine = src.keyLine;
|
|
366
|
+
anchor.keyCol = src.keyCol;
|
|
367
|
+
anchor.keyLength = src.keyLength;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
frames = { dataFrame, anchor };
|
|
371
|
+
if (dataFrame !== undefined) Object.defineProperty(out, 'dataFrame', { value: dataFrame, enumerable: true, configurable: true, writable: true });
|
|
372
|
+
else delete out.dataFrame;
|
|
373
|
+
if (anchor !== undefined) Object.defineProperty(out, 'anchor', { value: anchor, enumerable: true, configurable: true, writable: true });
|
|
374
|
+
else delete out.anchor;
|
|
375
|
+
return frames;
|
|
376
|
+
};
|
|
377
|
+
const settable = (name) => (v) => Object.defineProperty(out, name, { value: v, enumerable: true, configurable: true, writable: true });
|
|
378
|
+
Object.defineProperty(out, 'dataFrame', { get () { return resolve().dataFrame; }, set: settable('dataFrame'), enumerable: true, configurable: true });
|
|
379
|
+
Object.defineProperty(out, 'anchor', { get () { return resolve().anchor; }, set: settable('anchor'), enumerable: true, configurable: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
306
382
|
module.exports = { enrich, reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE };
|
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;
|