ata-validator 1.62.1 → 1.63.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 +63 -8
- package/build.mjs +1 -0
- package/index.browser.mjs +23 -4
- package/index.bundle.mjs +14 -4
- package/index.edge.mjs +14 -4
- package/index.js +7 -2
- package/lib/aot-build.js +18 -0
- package/lib/codegen-engine.js +72 -3
- package/lib/compiled-common.js +2 -1
- package/lib/email-literal.js +4 -1
- package/lib/formats-source.js +39 -40
- package/lib/interpreter-core.js +17 -9
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +560 -0
- package/lib/ir/front.mjs +834 -0
- package/lib/ir/index.mjs +10 -0
- package/lib/ir/refs.mjs +108 -0
- package/lib/ir/run.mjs +520 -0
- package/lib/ir/table.mjs +233 -0
- package/lib/js-compiler-errors.js +9 -4
- package/lib/kernels/collapse.mjs +114 -0
- package/lib/kernels/custom.mjs +70 -0
- package/lib/kernels/fill.mjs +6 -0
- package/lib/kernels/formats-small.mjs +40 -0
- package/lib/kernels/formats.mjs +29 -0
- package/lib/kernels/index.mjs +11 -0
- package/lib/kernels/kernels.mjs +285 -0
- package/lib/kernels/leaves.mjs +28 -0
- package/lib/kernels/order.mjs +25 -0
- package/lib/kernels/plan-rich.mjs +17 -0
- package/lib/kernels/plan.d.ts +9 -0
- package/lib/kernels/plan.mjs +128 -0
- package/lib/kernels/preprocess.mjs +223 -0
- package/lib/kernels/rank.mjs +97 -0
- package/lib/kernels/rt.mjs +249 -0
- package/lib/kernels/table.mjs +182 -0
- package/lib/kernels/unique.mjs +157 -0
- package/lib/native-positions.js +1 -1
- package/lib/openapi.d.ts +2 -0
- package/lib/openapi.js +128 -0
- package/lib/plan-compiler.js +84 -7
- package/lib/public-api.js +7 -2
- package/lib/rejections.js +3 -1
- package/lib/schema-helpers.js +12 -0
- package/lib/schema-helpers.mjs +20 -0
- package/lib/schema-order.js +4 -2
- package/lib/standard-schema.js +1 -1
- package/lib/validator-core.js +35 -30
- package/lib/version.js +1 -1
- package/lite.js +7 -2
- package/package.json +26 -8
package/lib/ir/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ata-validator/ir: the schema compiler built on one definition per keyword.
|
|
2
|
+
// front() turns a schema into procedures of a small IR (lib/ir/README.md);
|
|
3
|
+
// emitIR()/specialize() write a JavaScript module that imports its helpers
|
|
4
|
+
// from ata-validator/kernels; compile() in run.mjs builds closures from the
|
|
5
|
+
// same IR, without eval. A schema outside what the IR covers throws an error
|
|
6
|
+
// whose message starts with "ir:", so a caller can fall back.
|
|
7
|
+
export { front, normalize } from './front.mjs'
|
|
8
|
+
export { emitIR, specialize } from './emit.mjs'
|
|
9
|
+
export { compile as compileClosures, compileIR } from './run.mjs'
|
|
10
|
+
export { tableModule, tablePlan, PLAN_VERSION } from './table.mjs'
|
package/lib/ir/refs.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// $ref resolution inside one document. The index maps every resource
|
|
2
|
+
// ($id, resolved against its parent) and every $anchor to its schema; a JSON
|
|
3
|
+
// pointer is read from the resource it is relative to. A reference that
|
|
4
|
+
// leaves the document is refused, never passed over.
|
|
5
|
+
|
|
6
|
+
import { isObj } from '../kernels/rt.mjs'
|
|
7
|
+
|
|
8
|
+
// A base for a root without $id. Any absolute URI works; this one cannot
|
|
9
|
+
// collide with a real $id.
|
|
10
|
+
const ROOT = 'https://ata.invalid/root.json'
|
|
11
|
+
|
|
12
|
+
// A reference that names nothing this index holds. The front end turns it
|
|
13
|
+
// into the current engine's error for it (ATA5001) where the reference runs.
|
|
14
|
+
const unresolved = (message) => Object.assign(new Error(message), { unresolved: true })
|
|
15
|
+
|
|
16
|
+
const strip = (u) => { const i = u.indexOf('#'); return i === -1 ? u : u.slice(0, i) }
|
|
17
|
+
const resolveUri = (ref, base) => new URL(ref, base).href
|
|
18
|
+
|
|
19
|
+
// Keywords whose values are subschemas, and how to reach them, for the walk
|
|
20
|
+
// that finds $id and $anchor. Values of other keywords (const, enum,
|
|
21
|
+
// examples, default) are data and are not searched.
|
|
22
|
+
const ONE = ['additionalProperties', 'propertyNames', 'items', 'contains', 'not', 'if', 'then', 'else', 'unevaluatedProperties', 'unevaluatedItems', 'contentSchema']
|
|
23
|
+
const MAP = ['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']
|
|
24
|
+
const LIST = ['prefixItems', 'allOf', 'anyOf', 'oneOf']
|
|
25
|
+
|
|
26
|
+
// `registry`: more documents (the `schemas` option), keyed by the URI each
|
|
27
|
+
// was retrieved from; each is also addressable by the $id it declares.
|
|
28
|
+
// `external(uri)`: a document to load when a reference names one the index
|
|
29
|
+
// does not hold (the metaschemas), or undefined.
|
|
30
|
+
export function buildIndex (root, registry = null, external = null) {
|
|
31
|
+
const resources = new Map() // uri without fragment -> schema
|
|
32
|
+
const anchors = new Map() // uri#anchor -> schema
|
|
33
|
+
const dynAnchors = new Map() // uri#name -> schema, for $dynamicRef
|
|
34
|
+
const baseOf = new Map() // schema object -> its base uri
|
|
35
|
+
const walk = (s, base) => {
|
|
36
|
+
if (!isObj(s)) return
|
|
37
|
+
// An $id with a fragment (`#/definitions/x`, which SchemaStore writes)
|
|
38
|
+
// names no resource: it neither opens one nor moves the base, as in the
|
|
39
|
+
// current engine. Registering its stripped URI replaced the document
|
|
40
|
+
// that held it.
|
|
41
|
+
if (typeof s.$id === 'string' && !/#./.test(s.$id)) {
|
|
42
|
+
base = strip(resolveUri(s.$id, base))
|
|
43
|
+
resources.set(base, s)
|
|
44
|
+
}
|
|
45
|
+
baseOf.set(s, base)
|
|
46
|
+
if (typeof s.$anchor === 'string') anchors.set(base + '#' + s.$anchor, s)
|
|
47
|
+
// A $dynamicAnchor is also a plain anchor for $ref and for the static
|
|
48
|
+
// first step of $dynamicRef.
|
|
49
|
+
if (typeof s.$dynamicAnchor === 'string') { dynAnchors.set(base + '#' + s.$dynamicAnchor, s); if (!anchors.has(base + '#' + s.$dynamicAnchor)) anchors.set(base + '#' + s.$dynamicAnchor, s) }
|
|
50
|
+
for (const k of ONE) if (k in s) walk(s[k], base)
|
|
51
|
+
for (const k of MAP) if (isObj(s[k])) for (const p in s[k]) walk(s[k][p], base)
|
|
52
|
+
for (const k of LIST) if (Array.isArray(s[k])) for (const x of s[k]) walk(x, base)
|
|
53
|
+
}
|
|
54
|
+
const rootBase = isObj(root) && typeof root.$id === 'string' && !/#./.test(root.$id) ? strip(resolveUri(root.$id, ROOT)) : ROOT
|
|
55
|
+
if (!resources.has(rootBase)) resources.set(rootBase, root)
|
|
56
|
+
walk(root, ROOT)
|
|
57
|
+
baseOf.set(root, rootBase)
|
|
58
|
+
const addDoc = (uri, doc) => {
|
|
59
|
+
// A document registered under a relative name (`shared`, as Fastify
|
|
60
|
+
// registers them) is addressed from the root's base, where a reference
|
|
61
|
+
// to `shared#` from the root lands.
|
|
62
|
+
const at = strip(resolveUri(uri, rootBase))
|
|
63
|
+
if (!resources.has(at)) resources.set(at, doc)
|
|
64
|
+
walk(doc, at)
|
|
65
|
+
}
|
|
66
|
+
if (registry !== null) for (const [uri, doc] of registry) addDoc(uri, doc)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
baseOf: (s) => baseOf.get(s) || rootBase,
|
|
70
|
+
dynAnchor: (base, name) => dynAnchors.get(base + '#' + name),
|
|
71
|
+
// Every resource that declares $dynamicAnchor `name`: the candidates a
|
|
72
|
+
// $dynamicRef to it can land on, by base.
|
|
73
|
+
dynCandidates: (name) => [...dynAnchors].filter(([k]) => k.endsWith('#' + name) && k.slice(0, k.length - name.length - 1).indexOf('#') === -1).map(([k, v]) => [k.slice(0, k.length - name.length - 1), v]),
|
|
74
|
+
resolve (ref, base) {
|
|
75
|
+
const abs = resolveUri(ref, base)
|
|
76
|
+
const doc = strip(abs)
|
|
77
|
+
const frag = abs.length > doc.length ? abs.slice(doc.length + 1) : ''
|
|
78
|
+
let res = resources.get(doc)
|
|
79
|
+
if (res === undefined && external !== null) {
|
|
80
|
+
const d = external(doc)
|
|
81
|
+
if (d !== undefined) { addDoc(doc, d); res = resources.get(doc) }
|
|
82
|
+
}
|
|
83
|
+
if (res === undefined) throw unresolved(`ir: $ref "${ref}" leaves the document`)
|
|
84
|
+
if (frag === '') return res
|
|
85
|
+
if (!frag.startsWith('/')) {
|
|
86
|
+
const a = anchors.get(doc + '#' + frag)
|
|
87
|
+
if (a === undefined) throw unresolved(`ir: $ref "${ref}" names an unknown anchor`)
|
|
88
|
+
return a
|
|
89
|
+
}
|
|
90
|
+
let cur = res
|
|
91
|
+
for (const raw of frag.slice(1).split('/')) {
|
|
92
|
+
const segm = decodeURIComponent(raw).replace(/~1/g, '/').replace(/~0/g, '~')
|
|
93
|
+
if (cur === null || typeof cur !== 'object') throw unresolved(`ir: $ref "${ref}" points at nothing`)
|
|
94
|
+
// Normalization (lib/draft7.js) renames definitions to $defs and
|
|
95
|
+
// tuple items to prefixItems but leaves pointers as written, so the
|
|
96
|
+
// old names read as the new ones, as the current engine reads them.
|
|
97
|
+
let name = segm
|
|
98
|
+
if (!(name in cur)) {
|
|
99
|
+
const alias = name === 'definitions' ? '$defs' : name === '$defs' ? 'definitions' : name === 'items' && Array.isArray(cur.prefixItems) ? 'prefixItems' : null
|
|
100
|
+
if (alias === null || !(alias in cur)) throw unresolved(`ir: $ref "${ref}" points at nothing`)
|
|
101
|
+
name = alias
|
|
102
|
+
}
|
|
103
|
+
cur = cur[name]
|
|
104
|
+
}
|
|
105
|
+
return cur
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
package/lib/ir/run.mjs
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// IR backend: closures. It knows the IR operations (README.md) and
|
|
2
|
+
// nothing about JSON Schema. Each procedure becomes a function over a frame
|
|
3
|
+
// (an array); variable names are resolved to slots once, here.
|
|
4
|
+
//
|
|
5
|
+
// A statement returns 1 to return false from its procedure (a failed verdict)
|
|
6
|
+
// and 0 to go on.
|
|
7
|
+
|
|
8
|
+
import { E, own, keysOf, seg, newAnn, mergeAnn, isObj, relate, setOwn, copyOf } from '../kernels/rt.mjs'
|
|
9
|
+
import * as kernels from '../kernels/kernels.mjs'
|
|
10
|
+
import { ordered } from '../kernels/order.mjs'
|
|
11
|
+
import { front, orderSkeleton, needsOrder, normalize } from './front.mjs'
|
|
12
|
+
// Filling defaults is its own pass, shared with the current engine and its
|
|
13
|
+
// compiled modules (lib/defaults.js): it runs before validation, in place.
|
|
14
|
+
import defaults from '../defaults.js'
|
|
15
|
+
import { buildCoercer, buildRemover, resolveSchemaByPath, stripOrdinal, resolveSchemaForPreprocess } from '../kernels/preprocess.mjs'
|
|
16
|
+
import dialect from '../dialect.js'
|
|
17
|
+
import pointer from '../pointer.js'
|
|
18
|
+
import errorMessages from '../error-messages.js'
|
|
19
|
+
import enrichLib from '../enrich-error.js'
|
|
20
|
+
import { typeBit, dataBits, dataCtx, reread, ctxRoot, setCtxRoot } from '../kernels/custom.mjs'
|
|
21
|
+
|
|
22
|
+
const PN = ({}).__proto__ === Object.prototype
|
|
23
|
+
const NONE = Object.freeze([])
|
|
24
|
+
const PARAMS = ['d', 'e', 's', 'P', 'V', 'SP', 'A']
|
|
25
|
+
const [, iE, iS, , , , iA] = PARAMS.map((_, i) => i)
|
|
26
|
+
|
|
27
|
+
export function compileIR (ir, { richErrors = true } = {}) {
|
|
28
|
+
// Errors a custom keyword writes name their own paths: always sorted then.
|
|
29
|
+
const SK = ir.custom || needsOrder(ir.schema) ? orderSkeleton(ir.schema) : null
|
|
30
|
+
// The document being validated, for the errors a custom keyword writes:
|
|
31
|
+
// their received value is read from it by path, as the current engine does.
|
|
32
|
+
let ROOT
|
|
33
|
+
const plain = richErrors === false
|
|
34
|
+
// A call that gathers no errors reads no instance or schema path, unless a
|
|
35
|
+
// custom keyword is in the schema (its function is given the paths), so
|
|
36
|
+
// such a call is not given the strings it would build for them.
|
|
37
|
+
const QUIET = !ir.custom
|
|
38
|
+
const fns = new Map()
|
|
39
|
+
// The dynamic scope: resources entered on the way here.
|
|
40
|
+
let SC = null
|
|
41
|
+
const pick = (table, fallback) => {
|
|
42
|
+
const chain = []
|
|
43
|
+
for (let c = SC; c !== null; c = c.up) chain.push(c.b)
|
|
44
|
+
for (let i = chain.length - 1; i >= 0; i--) { const g = table.get(chain[i]); if (g !== undefined) return g }
|
|
45
|
+
return fallback
|
|
46
|
+
}
|
|
47
|
+
const procFn = (id) => {
|
|
48
|
+
if (fns.has(id)) return fns.get(id)
|
|
49
|
+
const cell = { f: null }
|
|
50
|
+
const call = (d, e, s, P, V, SP, A) => cell.f(d, e, s, P, V, SP, A)
|
|
51
|
+
fns.set(id, call)
|
|
52
|
+
const p = ir.procs.get(id)
|
|
53
|
+
const slots = new Map(PARAMS.map((n, i) => [n, i]))
|
|
54
|
+
const slot = (n) => { if (!slots.has(n)) slots.set(n, slots.size); return slots.get(n) }
|
|
55
|
+
const body = stmt(p.body, slot)
|
|
56
|
+
const width = slots.size
|
|
57
|
+
const ann = p.mode === 'ann'
|
|
58
|
+
const base = p.base
|
|
59
|
+
// A procedure that can reach itself on the same value (lib/ir/front.mjs
|
|
60
|
+
// `guard`): a second entry with the same value answers with no error.
|
|
61
|
+
const run = (d, e, s, P, V, SP, A) => {
|
|
62
|
+
const f = new Array(width)
|
|
63
|
+
f[0] = d; f[1] = e; f[2] = s; f[3] = P; f[4] = V; f[5] = SP; f[6] = ann && (A === null || A === undefined) ? newAnn() : A
|
|
64
|
+
if (!ir.dynamic) return body(f) === 0
|
|
65
|
+
const saved = SC
|
|
66
|
+
if (SC === null || SC.b !== base) SC = { b: base, up: SC }
|
|
67
|
+
try { return body(f) === 0 } finally { SC = saved }
|
|
68
|
+
}
|
|
69
|
+
cell.f = run
|
|
70
|
+
return call
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stmt (n, slot) {
|
|
74
|
+
switch (n.t) {
|
|
75
|
+
case 'seq': {
|
|
76
|
+
// Short sequences are unrolled: a loop over an array of closures is
|
|
77
|
+
// one megamorphic call site V8 cannot inline through.
|
|
78
|
+
const b = n.body.map((x) => stmt(x, slot))
|
|
79
|
+
if (b.length === 0) return () => 0
|
|
80
|
+
if (b.length === 1) return b[0]
|
|
81
|
+
const pair = (x, y) => (f) => x(f) === 1 ? 1 : y(f)
|
|
82
|
+
const tree = (lo, hi) => hi - lo === 1 ? b[lo] : pair(tree(lo, (lo + hi) >> 1), tree((lo + hi) >> 1, hi))
|
|
83
|
+
return tree(0, b.length)
|
|
84
|
+
}
|
|
85
|
+
case 'props': {
|
|
86
|
+
const all = stmt({ t: 'seq', body: n.body }, slot)
|
|
87
|
+
// Below this many names a lookup per name costs less than listing the
|
|
88
|
+
// object's names. On SchemaStore's samples with code generation
|
|
89
|
+
// blocked, the median verdict was 3.26x the interpreter's at 32, 3.00x
|
|
90
|
+
// at 8 and 2.95x at 4.
|
|
91
|
+
if (n.keyed === null || n.keyed.length < 4) return all
|
|
92
|
+
const b = n.body.map((x) => stmt(x, slot))
|
|
93
|
+
const keyed = new Set(n.keyed.map(([, i]) => i))
|
|
94
|
+
const byName = new Map(n.keyed.map(([p, i]) => [p, b[i]]))
|
|
95
|
+
const always = b.filter((_, i) => !keyed.has(i))
|
|
96
|
+
const xi = slot(n.x.n); const want = n.keyed.length
|
|
97
|
+
// A verdict on an object with fewer own names than the schema lists:
|
|
98
|
+
// visit its names. An entry runs only for a name that is an own
|
|
99
|
+
// property with a defined value (the entry checks that itself), so
|
|
100
|
+
// the names it would skip are names the object does not have. Errors
|
|
101
|
+
// keep the schema's order, and so the lookups.
|
|
102
|
+
return (f) => {
|
|
103
|
+
if (f[iE] !== null) return all(f)
|
|
104
|
+
const names = Object.getOwnPropertyNames(f[xi])
|
|
105
|
+
if (names.length >= want) return all(f)
|
|
106
|
+
for (let j = 0; j < always.length; j++) if (always[j](f) === 1) return 1
|
|
107
|
+
for (let j = 0; j < names.length; j++) { const g = byName.get(names[j]); if (g !== undefined && g(f) === 1) return 1 }
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
case 'if': {
|
|
112
|
+
const t = stmt(n.then, slot); const e = n.else ? stmt(n.else, slot) : null
|
|
113
|
+
if (!e && n.c.t === 'not') { const c = expr(n.c.e, slot); return (f) => c(f) ? 0 : t(f) }
|
|
114
|
+
const c = expr(n.c, slot)
|
|
115
|
+
return e ? (f) => c(f) ? t(f) : e(f) : (f) => c(f) ? t(f) : 0
|
|
116
|
+
}
|
|
117
|
+
case 'let': {
|
|
118
|
+
const i = slot(n.name); const b = stmt(n.body, slot)
|
|
119
|
+
if (n.value.t === 'get' && n.value.o.t === 'var' && n.value.k.t === 'lit') { const o = slot(n.value.o.n); const k = n.value.k.v; return (f) => { f[i] = f[o][k]; return b(f) } }
|
|
120
|
+
const v = expr(n.value, slot)
|
|
121
|
+
return (f) => { f[i] = v(f); return b(f) }
|
|
122
|
+
}
|
|
123
|
+
case 'bind': {
|
|
124
|
+
const is = n.names.map(([name]) => slot(name)); const vs = n.names.map(([, value]) => expr(value, slot))
|
|
125
|
+
if (is.length === 2) { const [i0, i1] = is; const [v0, v1] = vs; return (f) => { f[i0] = v0(f); f[i1] = v1(f); return 0 } }
|
|
126
|
+
return (f) => { for (let j = 0; j < is.length; j++) f[is[j]] = vs[j](f); return 0 }
|
|
127
|
+
}
|
|
128
|
+
case 'keys': {
|
|
129
|
+
const o = expr(n.obj, slot); const i = slot(n.k); const b = stmt(n.body, slot)
|
|
130
|
+
if (n.order === 'forin') return (f) => { const x = o(f); for (const k in x) { f[i] = k; if (b(f) === 1) return 1 } return 0 }
|
|
131
|
+
const list = n.order === 'protoLast' ? keysOf : Object.keys
|
|
132
|
+
return (f) => { const ks = list(o(f)); for (let j = 0; j < ks.length; j++) { f[i] = ks[j]; if (b(f) === 1) return 1 } return 0 }
|
|
133
|
+
}
|
|
134
|
+
case 'range': {
|
|
135
|
+
const a = expr(n.arr, slot); const from = typeof n.from === 'number' ? () => n.from : expr(n.from, slot); const i = slot(n.i); const b = stmt(n.body, slot)
|
|
136
|
+
return (f) => { const x = a(f); for (let j = from(f); j < x.length; j++) { f[i] = j; if (b(f) === 1) return 1 } return 0 }
|
|
137
|
+
}
|
|
138
|
+
case 'count': {
|
|
139
|
+
const i = slot(n.name); const b = stmt(n.body, slot)
|
|
140
|
+
return (f) => { f[i] = 0; return b(f) }
|
|
141
|
+
}
|
|
142
|
+
case 'incr': { const i = slot(n.name); return (f) => { f[i]++; return 0 } }
|
|
143
|
+
case 'do': { const e = expr(n.e, slot); return (f) => { e(f); return 0 } }
|
|
144
|
+
case 'ret': return () => 1
|
|
145
|
+
case 'require': {
|
|
146
|
+
const e = expr(n.e, slot)
|
|
147
|
+
return (f) => e(f) || f[iE] !== null ? 0 : 1
|
|
148
|
+
}
|
|
149
|
+
case 'fail': {
|
|
150
|
+
const b = errBuilder(n.err, slot)
|
|
151
|
+
return (f) => { const e = f[iE]; if (e === null || e === undefined) return 1; e.push(b(f)); return 0 }
|
|
152
|
+
}
|
|
153
|
+
case 'branches': return branches(n, slot)
|
|
154
|
+
case 'custom': {
|
|
155
|
+
// lib/interpreter-core.js runCustom: each keyword whose type admits
|
|
156
|
+
// the value runs; the errors its function left on itself replace the
|
|
157
|
+
// keyword's own error, and are cleared after every call.
|
|
158
|
+
const x = expr(n.x, slot); const path = expr(n.path, slot); const sp = expr(n.sp, slot)
|
|
159
|
+
const ops = n.ops.map((op) => ({ keyword: op.keyword, fn: op.fn, ctx: op.ctx === true, mask: op.types === null ? 127 : op.types.reduce((m, t) => m | typeBit(t), 0) }))
|
|
160
|
+
const out = plain ? (r) => r : (r) => enrichLib.enrich(r, { data: ROOT })
|
|
161
|
+
// `here`: the value read again from where it sits, as the current
|
|
162
|
+
// engine reads it before the keywords declared post.
|
|
163
|
+
const at0 = n.here ? (f) => { const p = path(f); if (p === '') return ROOT; const c = dataCtx(p, '', ''); return c.parentData !== null && typeof c.parentData === 'object' ? c.parentData[c.parentDataProperty] : x(f) } : x
|
|
164
|
+
return (f) => {
|
|
165
|
+
let v = at0(f); let bits = dataBits(v); const e = f[iE]
|
|
166
|
+
for (let i = 0; i < ops.length; i++) {
|
|
167
|
+
const op = ops[i]
|
|
168
|
+
if ((bits & op.mask) === 0) continue
|
|
169
|
+
let ok
|
|
170
|
+
if (op.ctx) {
|
|
171
|
+
const c = dataCtx(path(f), sp(f), op.keyword)
|
|
172
|
+
ok = op.fn(v, c)
|
|
173
|
+
const now = reread(c, v)
|
|
174
|
+
if (now !== v) { v = now; bits = dataBits(v) }
|
|
175
|
+
} else ok = op.fn(v)
|
|
176
|
+
const holder = op.fn.source || op.fn
|
|
177
|
+
const own = holder.errors
|
|
178
|
+
if (own) holder.errors = null
|
|
179
|
+
if (ok) continue
|
|
180
|
+
if (e === null || e === undefined) return 1
|
|
181
|
+
const at = path(f); const kwPath = sp(f) + '/' + op.keyword
|
|
182
|
+
if (Array.isArray(own) && own.length > 0) {
|
|
183
|
+
// A context-reading keyword writes the path it was given.
|
|
184
|
+
for (const r of own) e.push(out({ keyword: r.keyword || op.keyword, instancePath: r.instancePath !== undefined ? (op.ctx ? r.instancePath : at + r.instancePath) : at, schemaPath: r.schemaPath !== undefined ? r.schemaPath : kwPath, params: r.params !== undefined ? r.params : {}, message: r.message !== undefined ? r.message : `must pass "${op.keyword}" keyword validation` }))
|
|
185
|
+
} else e.push(out({ keyword: op.keyword, instancePath: at, schemaPath: kwPath, params: { keyword: op.keyword }, message: `must pass "${op.keyword}" keyword validation` }))
|
|
186
|
+
}
|
|
187
|
+
return 0
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
case 'setDefault': { const o = expr(n.o, slot); const k = n.k; const v = n.v; return (f) => { const x = o(f); if (!own(x, k)) setOwn(x, k, copyOf(v)); return 0 } }
|
|
191
|
+
case 'fcall': { const x = expr(n.x, slot); const p = procFn(n.proc); return (f) => { p(x(f)); return 0 } }
|
|
192
|
+
case 'defaultsMask': {
|
|
193
|
+
const o = expr(n.o, slot); const idx = new Map(n.entries.map(([k], i) => [k, i])); const N = n.entries.length
|
|
194
|
+
return (f) => { const x = o(f); const seen = new Uint8Array(N); const ks = Object.getOwnPropertyNames(x); for (let j = 0; j < ks.length; j++) { const i = idx.get(ks[j]); if (i !== undefined) seen[i] = 1 } for (let i = 0; i < N; i++) if (seen[i] === 0) x[n.entries[i][0]] = copyOf(n.entries[i][1]); return 0 }
|
|
195
|
+
}
|
|
196
|
+
case 'fillWalk': {
|
|
197
|
+
const o = expr(n.o, slot); const T = new Map(n.entries.map(([k, id]) => [k, procFn(id)]))
|
|
198
|
+
return (f) => { const x = o(f); const ks = Object.getOwnPropertyNames(x); for (let j = 0; j < ks.length; j++) { const p = T.get(ks[j]); if (p !== undefined) p(x[ks[j]]) } return 0 }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new Error('ir run: statement ' + n.t)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function errBuilder (er, slot) {
|
|
205
|
+
const path = expr(er.path, slot); const sp = expr(er.sp, slot); const params = expr(er.params, slot); const msg = expr(er.msg, slot); const ev = expr(er.ev, slot)
|
|
206
|
+
const detail = er.detail ? er.detail.fn : undefined; const suggest = er.suggest ? er.suggest.fn : undefined
|
|
207
|
+
if (plain) return (f) => ({ keyword: er.kw, instancePath: path(f), schemaPath: sp(f), params: params(f), message: msg(f) })
|
|
208
|
+
return (f) => E(er.code, er.kw, path(f), sp(f), params(f), msg(f), er.exp, er.rank, ev(f), f[iS], detail, suggest)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// anyOf / oneOf: every branch's verdict, or in error mode each branch's own
|
|
212
|
+
// errors through the collapse. Annotations: `each` valid branch, or
|
|
213
|
+
// `onlyIfOne` (oneOf keeps them only when exactly one passed).
|
|
214
|
+
function branches (n, slot) {
|
|
215
|
+
const x = expr(n.x, slot); const path = expr(n.path, slot); const sp = expr(n.sp, slot); const ev = expr(n.ev, slot)
|
|
216
|
+
const sps = n.sps.map((e) => expr(e, slot))
|
|
217
|
+
const ps = n.procs.map(procFn)
|
|
218
|
+
const isAny = n.kw === 'anyOf'
|
|
219
|
+
return (f) => {
|
|
220
|
+
const A = f[iA]; const e = f[iE]
|
|
221
|
+
const hold = n.ann === 'onlyIfOne' ? newAnn() : A
|
|
222
|
+
const v = x(f)
|
|
223
|
+
// An anyOf with no annotations and a list to fill runs each branch once
|
|
224
|
+
// and stops at the first that passes (see the module writer's branches).
|
|
225
|
+
if (isAny && !n.ann && e !== null && e !== undefined) {
|
|
226
|
+
const br = []
|
|
227
|
+
for (let i = 0; i < ps.length; i++) {
|
|
228
|
+
const be = []
|
|
229
|
+
ps[i](v, be, f[iS], path(f), ev(f), sps[i](f), undefined)
|
|
230
|
+
if (be.length === 0) return 0
|
|
231
|
+
br.push({ valid: false, errors: be })
|
|
232
|
+
}
|
|
233
|
+
const r = n.collapse(n.kw, br, path(f), sp(f), null, n.titles)
|
|
234
|
+
if (r !== null) e.push(plain ? r : E(r.code, n.kw, path(f), sp(f), r.params, r.message, undefined, n.rank, ev(f), f[iS], undefined, undefined, r.branchErrors))
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
// Otherwise the verdict first also when a list is filled: the branches'
|
|
238
|
+
// errors are built only when the keyword fails. Not with custom
|
|
239
|
+
// keywords, which may change the data and must run once.
|
|
240
|
+
if (e === null || e === undefined || QUIET) {
|
|
241
|
+
let c = 0
|
|
242
|
+
for (let i = 0; i < ps.length; i++) {
|
|
243
|
+
const a = n.ann ? newAnn() : undefined
|
|
244
|
+
if (QUIET ? ps[i](v, null, f[iS], '', ev(f), '', a) : ps[i](v, null, f[iS], path(f), ev(f), sps[i](f), a)) { c++; if (n.ann) mergeAnn(hold, a); else if (isAny) break }
|
|
245
|
+
}
|
|
246
|
+
if (isAny ? c !== 0 : c === 1) {
|
|
247
|
+
if (n.ann === 'onlyIfOne') mergeAnn(A, hold)
|
|
248
|
+
return 0
|
|
249
|
+
}
|
|
250
|
+
if (e === null || e === undefined) return 1
|
|
251
|
+
}
|
|
252
|
+
const br = ps.map((p, i) => { const be = []; const a = n.ann ? newAnn() : undefined; p(v, be, f[iS], path(f), ev(f), sps[i](f), a); const ok = be.length === 0; if (ok && n.ann) mergeAnn(hold, a); return { valid: ok, errors: be } })
|
|
253
|
+
const r = n.collapse(n.kw, br, path(f), sp(f), null, n.titles)
|
|
254
|
+
if (r !== null) e.push(plain ? r : E(r.code, n.kw, path(f), sp(f), r.params, r.message, undefined, n.rank, ev(f), f[iS], undefined, undefined, r.branchErrors))
|
|
255
|
+
else if (n.ann === 'onlyIfOne') mergeAnn(A, hold)
|
|
256
|
+
return 0
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const GUARD = { isStr: kernels.isStr, isNum: kernels.isNum, isArr: kernels.isArr, isObjV: kernels.isObjV, isAny: kernels.isAny }
|
|
261
|
+
|
|
262
|
+
// Superinstructions: an operand that is a variable or a literal is read
|
|
263
|
+
// directly instead of through a closure of its own.
|
|
264
|
+
const isVar = (e) => e.t === 'var'
|
|
265
|
+
const isLit = (e) => e.t === 'lit'
|
|
266
|
+
function expr (n, slot) {
|
|
267
|
+
switch (n.t) {
|
|
268
|
+
case 'get': if (isVar(n.o) && isLit(n.k)) { const o = slot(n.o.n); const k = n.k.v; return (f) => f[o][k] } break
|
|
269
|
+
case 'own': if (isVar(n.o) && isLit(n.k)) { const o = slot(n.o.n); const k = n.k.v; return (f) => own(f[o], k) } if (isVar(n.o) && isVar(n.k)) { const o = slot(n.o.n); const k = slot(n.k.n); return (f) => own(f[o], f[k]) } break
|
|
270
|
+
case 'cmp': if (isVar(n.a) && isLit(n.b)) {
|
|
271
|
+
const a = slot(n.a.n); const v = n.b.v
|
|
272
|
+
switch (n.op) { case '===': return (f) => f[a] === v; case '!==': return (f) => f[a] !== v; case '<': return (f) => f[a] < v; case '>': return (f) => f[a] > v }
|
|
273
|
+
} break
|
|
274
|
+
case 'type': if (isVar(n.x) && n.types.length === 1) { const i = slot(n.x.n); const t = kernels.TYPES[n.types[0]] || (() => false); return (f) => t(f[i]) } break
|
|
275
|
+
case 'kernel': if (isVar(n.x)) { const i = slot(n.x.n); const ok = n.K.ok; const pv = n.pv; return (f) => ok(f[i], pv) } break
|
|
276
|
+
case 'guard': if (isVar(n.x)) { const i = slot(n.x.n); const g = GUARD[n.g]; return (f) => g(f[i]) } break
|
|
277
|
+
case 'and': if (isVar(n.a)) { const a = slot(n.a.n); const b = expr(n.b, slot); return (f) => f[a] && b(f) } break
|
|
278
|
+
case 'or': if (isVar(n.a)) { const a = slot(n.a.n); const b = expr(n.b, slot); return (f) => f[a] || b(f) } break
|
|
279
|
+
}
|
|
280
|
+
switch (n.t) {
|
|
281
|
+
case 'lit': { const v = n.v; return () => v }
|
|
282
|
+
case 'var': { const i = slot(n.n); return (f) => f[i] }
|
|
283
|
+
case 'get': { const o = expr(n.o, slot); const k = expr(n.k, slot); return (f) => o(f)[k(f)] }
|
|
284
|
+
case 'field': { const o = expr(n.o, slot); const k = n.k; return (f) => o(f)[k] }
|
|
285
|
+
case 'own': { const o = expr(n.o, slot); const k = expr(n.k, slot); return (f) => own(o(f), k(f)) }
|
|
286
|
+
case 'not': { const e = expr(n.e, slot); return (f) => !e(f) }
|
|
287
|
+
case 'and': { const a = expr(n.a, slot); const b = expr(n.b, slot); return (f) => a(f) && b(f) }
|
|
288
|
+
case 'or': { const a = expr(n.a, slot); const b = expr(n.b, slot); return (f) => a(f) || b(f) }
|
|
289
|
+
case 'cmp': {
|
|
290
|
+
const a = expr(n.a, slot); const b = expr(n.b, slot)
|
|
291
|
+
switch (n.op) {
|
|
292
|
+
case '===': return (f) => a(f) === b(f)
|
|
293
|
+
case '!==': return (f) => a(f) !== b(f)
|
|
294
|
+
case '<': return (f) => a(f) < b(f)
|
|
295
|
+
case '>': return (f) => a(f) > b(f)
|
|
296
|
+
}
|
|
297
|
+
throw new Error('ir run: cmp ' + n.op)
|
|
298
|
+
}
|
|
299
|
+
case 'isObj': { const x = expr(n.x, slot); return (f) => isObj(x(f)) }
|
|
300
|
+
case 'isArr': { const x = expr(n.x, slot); return (f) => Array.isArray(x(f)) }
|
|
301
|
+
case 'isObjAny': { const x = expr(n.x, slot); return (f) => { const v = x(f); return typeof v === 'object' && v !== null } }
|
|
302
|
+
case 'len': { const x = expr(n.x, slot); return (f) => x(f).length }
|
|
303
|
+
case 'plainProto': { const x = expr(n.x, slot); return (f) => { const v = x(f); return (PN ? v.__proto__ : Object.getPrototypeOf(v)) === Object.prototype } }
|
|
304
|
+
case 'type': {
|
|
305
|
+
const x = expr(n.x, slot); const ts = n.types.map((t) => kernels.TYPES[t] || (() => false))
|
|
306
|
+
return ts.length === 1 ? ((t) => (f) => t(x(f)))(ts[0]) : (f) => { const v = x(f); for (const t of ts) if (t(v)) return true; return false }
|
|
307
|
+
}
|
|
308
|
+
case 'guard': { const x = expr(n.x, slot); const g = GUARD[n.g]; return (f) => g(x(f)) }
|
|
309
|
+
case 'kernel': { const x = expr(n.x, slot); const ok = n.K.ok; const pv = n.pv; return (f) => ok(x(f), pv) }
|
|
310
|
+
case 'kfail': { const x = expr(n.x, slot); const fl = n.K.fail; const pv = n.pv; return (f) => fl(x(f), pv) }
|
|
311
|
+
case 'known': {
|
|
312
|
+
const k = expr(n.k, slot); const names = new Set(n.names); const res = n.pats.map((p) => new RegExp(p, 'u'))
|
|
313
|
+
if (res.length === 0) return (f) => names.has(k(f))
|
|
314
|
+
return (f) => { const v = k(f); if (names.has(v)) return true; for (const re of res) if (re.test(v)) return true; return false }
|
|
315
|
+
}
|
|
316
|
+
case 're': { const re = new RegExp(n.src, 'u'); const k = expr(n.k, slot); return (f) => re.test(k(f)) }
|
|
317
|
+
case 'seg': { const k = expr(n.k, slot); return (f) => seg(k(f)) }
|
|
318
|
+
case 'cat': { const a = expr(n.a, slot); const b = expr(n.b, slot); return (f) => a(f) + b(f) }
|
|
319
|
+
case 'str': { const ps = n.parts.map((p) => expr(p, slot)); return (f) => ps.map((p) => p(f)).join('') }
|
|
320
|
+
case 'obj': { const fs = n.fields.map(([k, v]) => [k, expr(v, slot)]); return (f) => { const o = {}; for (const [k, v] of fs) o[k] = v(f); return o } }
|
|
321
|
+
case 'annKey': { const k = expr(n.k, slot); return (f) => { const A = f[iA]; (A.p === null ? A.p = new Set() : A.p).add(k(f)) } }
|
|
322
|
+
case 'annHasKey': { const k = expr(n.k, slot); return (f) => { const A = f[iA]; return A.p !== null && A.p.has(k(f)) } }
|
|
323
|
+
case 'annPrefix': { const a = expr(n.arr, slot); const m = n.n; return (f) => { const A = f[iA]; A.n = Math.max(A.n, Math.min(m, a(f).length)) } }
|
|
324
|
+
case 'annAll': return (f) => { f[iA].all = true }
|
|
325
|
+
case 'annAllSet': return (f) => f[iA].all
|
|
326
|
+
case 'annAt': { const i = expr(n.i, slot); return (f) => { const A = f[iA]; (A.at === null ? A.at = new Set() : A.at).add(i(f)) } }
|
|
327
|
+
case 'annAtHas': { const i = expr(n.i, slot); return (f) => { const A = f[iA]; return A.at !== null && A.at.has(i(f)) } }
|
|
328
|
+
case 'annN': return (f) => f[iA].n
|
|
329
|
+
case 'firstUneval': { const x = expr(n.x, slot); return (f) => { const A = f[iA]; const v = x(f); let j = A.n; if (A.at !== null) while (j < v.length && A.at.has(j)) j++; return j } }
|
|
330
|
+
case 'pcall': return pcall(n, slot)
|
|
331
|
+
}
|
|
332
|
+
throw new Error('ir run: expression ' + n.t)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A procedure called in place. Its validity: the verdict it returns, or in
|
|
336
|
+
// error mode whether the error list grew. Under `merge` it gets a collector
|
|
337
|
+
// of its own, merged into this frame's when valid.
|
|
338
|
+
function pcall (n, slot) {
|
|
339
|
+
const x = expr(n.x, slot)
|
|
340
|
+
const path = n.path ? expr(n.path, slot) : () => ''
|
|
341
|
+
const sp = n.sp ? expr(n.sp, slot) : () => ''
|
|
342
|
+
const ev = n.ev ? expr(n.ev, slot) : x
|
|
343
|
+
const table = n.dynamic ? new Map(n.dynamic.map(([b, id]) => [b, procFn(id)])) : null
|
|
344
|
+
// No fixed target: a v1 $dynamicRef that resolves only on the scope; when
|
|
345
|
+
// nothing on the scope matches, its error (n.missing).
|
|
346
|
+
const fixed = n.proc === null ? null : procFn(n.proc)
|
|
347
|
+
const target = table ? () => pick(table, fixed) : () => fixed
|
|
348
|
+
const missing = n.missing ? errBuilder(n.missing, slot) : null
|
|
349
|
+
const inherit = n.e === 'inherit'
|
|
350
|
+
// `fresh`: errors gathered and set aside, so every keyword of the
|
|
351
|
+
// subschema runs, as the current engine runs it under ctx.
|
|
352
|
+
const fresh = n.e === 'fresh'
|
|
353
|
+
// A call on a cycle (lib/ir/front.mjs `guard`): it keeps the values it is
|
|
354
|
+
// on and answers a second entry with no error, as the current engine cuts
|
|
355
|
+
// a reference it meets again.
|
|
356
|
+
const active = n.guard ? new Set() : null
|
|
357
|
+
const through = active === null ? null : (t) => (d, e, s, P, V, SP, A) => {
|
|
358
|
+
if (active.has(d)) return true
|
|
359
|
+
active.add(d)
|
|
360
|
+
try { return t(d, e, s, P, V, SP, A) } finally { active.delete(d) }
|
|
361
|
+
}
|
|
362
|
+
return (f) => {
|
|
363
|
+
const e = inherit ? f[iE] : fresh ? [] : null
|
|
364
|
+
let t = target()
|
|
365
|
+
if (through !== null && t !== null) t = through(t)
|
|
366
|
+
if (t === null) { if (e) e.push(missing(f)); return false }
|
|
367
|
+
const a = n.ann === 'merge' ? newAnn() : null
|
|
368
|
+
const before = e ? e.length : 0
|
|
369
|
+
const ok = e === null && QUIET ? t(x(f), null, f[iS], '', ev(f), '', a) : t(x(f), e, f[iS], path(f), ev(f), sp(f), a) && (!e || e.length === before)
|
|
370
|
+
if (ok && a !== null) mergeAnn(f[iA], a)
|
|
371
|
+
return ok
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const dual = procFn(ir.dual)
|
|
376
|
+
const verdict = procFn(ir.verdict)
|
|
377
|
+
if (ir.ctx) {
|
|
378
|
+
// One walk answers both, as the current engine's collecting walk does:
|
|
379
|
+
// a keyword that changes the value runs once. The document is the root
|
|
380
|
+
// its keywords read their context from, kept across a nested validate().
|
|
381
|
+
const run = (d) => {
|
|
382
|
+
const outer = ctxRoot(); setCtxRoot({ _ctxRoot: d }); ROOT = d
|
|
383
|
+
const e = []
|
|
384
|
+
try { dual(d, e, !(!d && d !== 0 && d !== false), '', d, '#', null) } finally { setCtxRoot(outer) }
|
|
385
|
+
if (e.length === 0) return { valid: true, data: d, errors: NONE }
|
|
386
|
+
const o = SK === null ? e : ordered(SK, e)
|
|
387
|
+
return { valid: false, errors: ir.relate && !plain && o.length > 1 ? relate(o) : o }
|
|
388
|
+
}
|
|
389
|
+
return { fill: (id) => { const p = procFn(id); return (d) => { p(d) } }, isValid: (d) => run(d).valid, validate: run }
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
fill: (id) => { const p = procFn(id); return (d) => { p(d) } },
|
|
393
|
+
isValid: (d) => { ROOT = d; return verdict(d, null, true, '', d, '#', null) },
|
|
394
|
+
validate (d) {
|
|
395
|
+
// The current engine's result: the value it checked, after defaults.
|
|
396
|
+
ROOT = d
|
|
397
|
+
if (verdict(d, null, true, '', d, '#', null)) return { valid: true, data: d, errors: NONE }
|
|
398
|
+
const e = []
|
|
399
|
+
// lib/enrich-error.js resolveAt: a falsy root other than 0 and false
|
|
400
|
+
// reports no received value on any error.
|
|
401
|
+
dual(d, e, !(!d && d !== 0 && d !== false), '', d, '#', null)
|
|
402
|
+
const o = SK === null ? e : ordered(SK, e)
|
|
403
|
+
return { valid: false, errors: ir.relate && !plain && o.length > 1 ? relate(o) : o }
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasDefault (s) {
|
|
409
|
+
if (typeof s !== 'object' || s === null) return false
|
|
410
|
+
if (Array.isArray(s)) return s.some(hasDefault)
|
|
411
|
+
if ('default' in s) return true
|
|
412
|
+
for (const k in s) if (hasDefault(s[k])) return true
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// The passes before the check, in the current engine's order (lib/validator-
|
|
417
|
+
// core.js): additional properties removed, values coerced, defaults filled.
|
|
418
|
+
// Any true value turns either option on, as there; coerceTypes: 'array' also
|
|
419
|
+
// wraps a lone top-level value of an array property.
|
|
420
|
+
export function preprocessOptions (opts) {
|
|
421
|
+
return { coerce: !!opts.coerceTypes, arrayMode: opts.coerceTypes === 'array', remove: !!opts.removeAdditional }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The schema the passes read: references into the `schemas` documents
|
|
425
|
+
// followed, over the same map the current engine builds from them
|
|
426
|
+
// (lib/validator-core.js _buildSchemaMap, declaredId).
|
|
427
|
+
function preprocessSchemaOf (ir, schema, opts) {
|
|
428
|
+
const schemas = opts.schemas
|
|
429
|
+
if (!schemas || (Array.isArray(schemas) ? schemas.length === 0 : Object.keys(schemas).length === 0)) return ir.schema
|
|
430
|
+
// The current engine adds the metaschemas to that map for a schema that
|
|
431
|
+
// references one; not reproduced here.
|
|
432
|
+
if (/"\$(?:ref|dynamicRef|recursiveRef)":"[^"]*json-schema\.org\//.test(JSON.stringify(schema))) throw new Error('ir: coerceTypes or removeAdditional with a reference to a metaschema is not supported')
|
|
433
|
+
const inherit = schema && typeof schema === 'object' ? dialect.draftCode(schema.$schema) : 0
|
|
434
|
+
const declaredId = (original, normalized) => {
|
|
435
|
+
const n = normalized && typeof normalized === 'object' ? normalized.$id : undefined
|
|
436
|
+
if (typeof n === 'string' && n !== '') return n
|
|
437
|
+
const o = original && typeof original === 'object' ? original.$id : undefined
|
|
438
|
+
if (typeof o === 'string' && o !== '' && o[0] !== '#') return o
|
|
439
|
+
return undefined
|
|
440
|
+
}
|
|
441
|
+
const map = new Map()
|
|
442
|
+
if (Array.isArray(schemas)) {
|
|
443
|
+
for (const d of schemas) {
|
|
444
|
+
const n = normalize(d, inherit); const id = declaredId(d, n)
|
|
445
|
+
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
446
|
+
map.set(id, n)
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
for (const [key, d] of Object.entries(schemas)) {
|
|
450
|
+
const n = normalize(d, inherit); map.set(key, n)
|
|
451
|
+
const id = declaredId(d, n)
|
|
452
|
+
if (id && id !== key) map.set(id, n)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return resolveSchemaForPreprocess(ir.schema, map)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// The current engine's abortEarly answer for any rejected document: one
|
|
459
|
+
// frozen error, shared (lib/validator-core.js ABORT_EARLY_RESULT).
|
|
460
|
+
const ABORT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze(Object.defineProperty({ code: 'ATA9000', message: 'validation failed', keyword: '__abort_early__', path: '' }, '_s', { value: true }))]) })
|
|
461
|
+
|
|
462
|
+
export function compile (schema, opts = {}) {
|
|
463
|
+
const pp = preprocessOptions(opts)
|
|
464
|
+
const rich = opts.richErrors !== false
|
|
465
|
+
const verbose = opts.verbose === true
|
|
466
|
+
const ir = front(schema, { assertFormat: opts.assertFormat !== false, schemas: opts.schemas || null, metaschemas: opts.metaschemas || null, errorMessages: true, formats: opts.formats || null, keywords: opts.keywords || null, baseURI: opts.baseURI || null })
|
|
467
|
+
// Under verbose the errors are built as the current engine builds them:
|
|
468
|
+
// plain, given the three fields, then enriched (lib/validator-core.js
|
|
469
|
+
// _verboseWrap below the presentation layer).
|
|
470
|
+
// Under ctx too: the current engine enriches those errors when they are
|
|
471
|
+
// read, from the document as its keywords left it.
|
|
472
|
+
const late = verbose || ir.ctx
|
|
473
|
+
const engine = compileIR(ir, { richErrors: late ? false : opts.richErrors })
|
|
474
|
+
let fill = null
|
|
475
|
+
if (opts.useDefaults !== false && hasDefault(schema)) {
|
|
476
|
+
// The specialized fill from the IR; the shared pass where it declined.
|
|
477
|
+
if (ir.fill === 'generic') fill = defaults.buildDefaultsApplier(ir.schema)
|
|
478
|
+
else if (ir.fill) fill = engine.fill(ir.fill)
|
|
479
|
+
}
|
|
480
|
+
const ppSchema = pp.coerce || pp.remove ? preprocessSchemaOf(ir, schema, opts) : ir.schema
|
|
481
|
+
const passes = [pp.remove ? buildRemover(ppSchema) : null, pp.coerce ? buildCoercer(ppSchema, pp.arrayMode) : null, fill].filter(Boolean)
|
|
482
|
+
const pre = passes.length === 0 ? null : passes.length === 1 ? passes[0] : (d) => { for (const p of passes) p(d) }
|
|
483
|
+
const isValid = pre === null ? engine.isValid : (d) => { pre(d); return engine.isValid(d) }
|
|
484
|
+
let validate = pre === null ? engine.validate : (d) => { pre(d); return engine.validate(d) }
|
|
485
|
+
// abortEarly: the verdict, and the shared stub for a rejection.
|
|
486
|
+
if (opts.abortEarly === true) validate = (d) => isValid(d) ? { valid: true, data: d, errors: NONE } : ABORT
|
|
487
|
+
if (late) {
|
|
488
|
+
const inner = validate
|
|
489
|
+
const root = ir.schema
|
|
490
|
+
validate = (d) => {
|
|
491
|
+
const r = inner(d)
|
|
492
|
+
if (r.valid) return r
|
|
493
|
+
let errs = !verbose ? r.errors : r.errors.map((err) => {
|
|
494
|
+
if (!err || err.parentSchema !== undefined) return err
|
|
495
|
+
const parentSchema = resolveSchemaByPath(root, err.schemaPath)
|
|
496
|
+
const sp = typeof err.schemaPath === 'string' ? err.schemaPath : ''
|
|
497
|
+
const last = sp.slice(sp.lastIndexOf('/') + 1).replace(/~1/g, '/').replace(/~0/g, '~')
|
|
498
|
+
const keywordSchema = parentSchema !== null && typeof parentSchema === 'object' && last ? parentSchema[last] : undefined
|
|
499
|
+
return { ...err, parentSchema, schema: keywordSchema, data: pointer.resolvePointer(d, err.instancePath, undefined) }
|
|
500
|
+
})
|
|
501
|
+
if (rich) {
|
|
502
|
+
errs = errs.map((e) => enrichLib.enrich(e, { data: d }))
|
|
503
|
+
if (ir.relate && errs.length > 1) errs = relate(errs)
|
|
504
|
+
} else errs = errs.map(stripOrdinal)
|
|
505
|
+
return { valid: false, errors: errs }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Custom messages over the finished errors, outermost, as the current
|
|
509
|
+
// engine layers them (lib/validator-core.js), resolved in the same schema.
|
|
510
|
+
if (errorMessages.schemaHasErrorMessages(JSON.stringify(ir.schema))) {
|
|
511
|
+
const inner = validate
|
|
512
|
+
validate = (d) => {
|
|
513
|
+
const r = inner(d)
|
|
514
|
+
if (r.valid || r === ABORT || !r.errors.length) return r
|
|
515
|
+
const o = errorMessages.applyErrorMessages(r.errors, ir.schema)
|
|
516
|
+
return o === r.errors ? r : { valid: false, errors: o }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { isValid, validate }
|
|
520
|
+
}
|