ata-validator 1.62.1 → 1.63.1

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.
Files changed (53) hide show
  1. package/README.md +63 -8
  2. package/build.mjs +1 -0
  3. package/index.browser.mjs +23 -4
  4. package/index.bundle.mjs +14 -4
  5. package/index.edge.mjs +14 -4
  6. package/index.js +7 -2
  7. package/lib/aot-build.js +18 -0
  8. package/lib/codegen-engine.js +72 -3
  9. package/lib/compiled-common.js +2 -1
  10. package/lib/email-literal.js +4 -1
  11. package/lib/formats-source.js +39 -40
  12. package/lib/interpreter-core.js +148 -70
  13. package/lib/ir/emit-deps.mjs +131 -0
  14. package/lib/ir/emit.mjs +707 -0
  15. package/lib/ir/front.mjs +865 -0
  16. package/lib/ir/index.mjs +10 -0
  17. package/lib/ir/refs.mjs +108 -0
  18. package/lib/ir/run.mjs +520 -0
  19. package/lib/ir/table.mjs +233 -0
  20. package/lib/js-compiler-errors.js +9 -4
  21. package/lib/kernels/collapse.mjs +114 -0
  22. package/lib/kernels/custom.mjs +70 -0
  23. package/lib/kernels/fill.mjs +6 -0
  24. package/lib/kernels/formats-small.mjs +40 -0
  25. package/lib/kernels/formats.mjs +29 -0
  26. package/lib/kernels/index.mjs +11 -0
  27. package/lib/kernels/kernels.mjs +285 -0
  28. package/lib/kernels/leaves.mjs +28 -0
  29. package/lib/kernels/order.mjs +25 -0
  30. package/lib/kernels/plan-rich.mjs +17 -0
  31. package/lib/kernels/plan.d.ts +9 -0
  32. package/lib/kernels/plan.mjs +128 -0
  33. package/lib/kernels/preprocess.mjs +223 -0
  34. package/lib/kernels/rank.mjs +97 -0
  35. package/lib/kernels/rt.mjs +249 -0
  36. package/lib/kernels/table.mjs +182 -0
  37. package/lib/kernels/unique.mjs +157 -0
  38. package/lib/native-positions.js +1 -1
  39. package/lib/openapi.d.ts +2 -0
  40. package/lib/openapi.js +128 -0
  41. package/lib/plan-compiler.js +113 -14
  42. package/lib/public-api.js +7 -2
  43. package/lib/rejections.js +3 -1
  44. package/lib/schema-helpers.js +12 -0
  45. package/lib/schema-helpers.mjs +20 -0
  46. package/lib/schema-order.js +4 -2
  47. package/lib/shape-classifier.js +13 -1
  48. package/lib/standard-schema.js +1 -1
  49. package/lib/tier0.js +38 -8
  50. package/lib/validator-core.js +59 -36
  51. package/lib/version.js +1 -1
  52. package/lite.js +7 -2
  53. package/package.json +26 -8
@@ -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'
@@ -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
+ }