ata-validator 1.40.1 → 1.42.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 +2 -0
- package/index.js +129 -3
- package/lib/aot-build.js +7 -3
- package/lib/aot-impl.js +22 -5
- package/lib/branch-collapse.js +17 -8
- package/lib/draft7.js +16 -0
- package/lib/enrich-error.js +46 -30
- package/lib/enrich-site.js +58 -0
- package/lib/interpreter.js +7 -1
- package/lib/js-compiler.js +945 -112
- package/lib/plan-compiler.js +7 -2
- package/lib/rejections.js +20 -3
- package/lib/schema-scan.js +6 -3
- package/lib/validator-core.js +127 -17
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -101,6 +101,38 @@ function emitUq(ctx) {
|
|
|
101
101
|
return '_uq'
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// The error and combined generators need the duplicate pair, not only the
|
|
105
|
+
// verdict. `_uqp` returns the index of the duplicate (-1 when every item is
|
|
106
|
+
// distinct) and leaves the item it repeats in `_uqpI`: the first item that has
|
|
107
|
+
// a later equal, paired with its first later equal. That is the pair the
|
|
108
|
+
// interpreted engine reports, and the generators used to report a different
|
|
109
|
+
// one (the earliest repeat, paired with what it repeats), so the same document
|
|
110
|
+
// named a different pair depending on the engine that answered. Short arrays
|
|
111
|
+
// compare pairwise and allocate nothing; longer ones key a Map by value, or by
|
|
112
|
+
// canonical string once an object appears. Building a canonical string for
|
|
113
|
+
// every item on every call, as the generators did, made reading a uniqueItems
|
|
114
|
+
// error up to ten times the cost of the verdict.
|
|
115
|
+
const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
|
|
116
|
+
function emitUqp(ctx) {
|
|
117
|
+
emitUq(ctx)
|
|
118
|
+
hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
|
|
119
|
+
return '_uqp'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// An object or array a const error reports as `allowedValue`, parsed once per
|
|
123
|
+
// compiled function rather than on every error: parsing it per error made
|
|
124
|
+
// reading a const error two to five times what the check costs. A copy of its
|
|
125
|
+
// own, not the compared constant, so an error a caller edits cannot change
|
|
126
|
+
// what the validator compares against. Declared in the function's helper
|
|
127
|
+
// code, so standalone output carries it the same way.
|
|
128
|
+
function emitParamConstant(ctx, value) {
|
|
129
|
+
const name = `_pc${ctx.varCounter++}`
|
|
130
|
+
const decl = `const ${name}=JSON.parse(${JSON.stringify(JSON.stringify(value))})`
|
|
131
|
+
if (ctx.preamble) ctx.preamble.push(decl)
|
|
132
|
+
else ctx.helperCode.push(decl)
|
|
133
|
+
return name
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
// The compared value, parsed once at compile time. `JSON.parse` rather than a
|
|
105
137
|
// JS object literal so a `__proto__` member stays an ordinary property.
|
|
106
138
|
function emitConstant(ctx, value) {
|
|
@@ -1390,6 +1422,25 @@ function unevalPropsModeled(schema, schemaMap) {
|
|
|
1390
1422
|
return true
|
|
1391
1423
|
}
|
|
1392
1424
|
|
|
1425
|
+
// Every `$ref` string under a node, once per node.
|
|
1426
|
+
const _refStrings = new WeakMap()
|
|
1427
|
+
function refStringsOf(node) {
|
|
1428
|
+
let set = _refStrings.get(node)
|
|
1429
|
+
if (set !== undefined) return set
|
|
1430
|
+
set = new Set()
|
|
1431
|
+
const seen = new Set()
|
|
1432
|
+
const walk = (n) => {
|
|
1433
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
1434
|
+
seen.add(n)
|
|
1435
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return }
|
|
1436
|
+
if (typeof n.$ref === 'string') set.add(n.$ref)
|
|
1437
|
+
for (const k of Object.keys(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(n[k])
|
|
1438
|
+
}
|
|
1439
|
+
walk(node)
|
|
1440
|
+
_refStrings.set(node, set)
|
|
1441
|
+
return set
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1393
1444
|
function codegenSafe(schema, schemaMap) {
|
|
1394
1445
|
if (typeof schema === 'boolean') return true
|
|
1395
1446
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -1505,8 +1556,9 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1505
1556
|
// that only the interpreted engine provides.
|
|
1506
1557
|
if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
|
|
1507
1558
|
const evalResult = collectEvaluated(schema, schemaMap)
|
|
1508
|
-
|
|
1509
|
-
|
|
1559
|
+
// What the static model cannot represent is built at run time
|
|
1560
|
+
// (genUnevalDynamicVerdict); shapes that cannot be (dynamic references,
|
|
1561
|
+
// reference cycles) decline there.
|
|
1510
1562
|
}
|
|
1511
1563
|
}
|
|
1512
1564
|
// unevaluatedItems: allow boolean and schema values
|
|
@@ -1527,7 +1579,12 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1527
1579
|
// Defs on a $ref cycle compile as named functions (see genCode); nothing
|
|
1528
1580
|
// to decline here. cyclicDefNames() is the walk that used to bail.
|
|
1529
1581
|
for (const [name, def] of Object.entries(defs)) {
|
|
1530
|
-
|
|
1582
|
+
// A name the generators would have to spell inside a path or an
|
|
1583
|
+
// identifier. Only a problem when a reference names it: tsconfig.json
|
|
1584
|
+
// keeps a comment object under the definition `//`, which nothing
|
|
1585
|
+
// references, and declined whole for it. A definition reached through
|
|
1586
|
+
// a deeper pointer was renamed by normalizeDeepRefs.
|
|
1587
|
+
if (/[~/"']/.test(name) && refStringsOf(schema).has('#/' + (schema.$defs === defs ? '$defs' : 'definitions') + '/' + name.replace(/~/g, '~0').replace(/\//g, '~1'))) return false
|
|
1531
1588
|
if (typeof def === 'boolean') return false
|
|
1532
1589
|
if (typeof def === 'object' && def !== null) {
|
|
1533
1590
|
// A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
|
|
@@ -1587,37 +1644,6 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1587
1644
|
return true
|
|
1588
1645
|
}
|
|
1589
1646
|
|
|
1590
|
-
// Returns true if the schema (or any nested sub-schema reachable through the
|
|
1591
|
-
// usual keywords) has additionalProperties as a schema value. Used by the
|
|
1592
|
-
// combined-codegen bail since that path does not emit the per-key loop yet.
|
|
1593
|
-
function hasAdditionalPropertiesSchema(schema) {
|
|
1594
|
-
if (typeof schema !== 'object' || schema === null) return false
|
|
1595
|
-
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) return true
|
|
1596
|
-
const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
1597
|
-
for (const key of objSubs) {
|
|
1598
|
-
if (schema[key] && typeof schema[key] === 'object') {
|
|
1599
|
-
for (const v of Object.values(schema[key])) {
|
|
1600
|
-
if (hasAdditionalPropertiesSchema(v)) return true
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
1605
|
-
for (const key of arrSubs) {
|
|
1606
|
-
if (Array.isArray(schema[key])) {
|
|
1607
|
-
for (const s of schema[key]) {
|
|
1608
|
-
if (hasAdditionalPropertiesSchema(s)) return true
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else', 'propertyNames']
|
|
1613
|
-
for (const key of singleSubs) {
|
|
1614
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
1615
|
-
if (hasAdditionalPropertiesSchema(schema[key])) return true
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return false
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
1647
|
// Schema-bearing keywords, so the walk below never mistakes a property named
|
|
1622
1648
|
// "$ref" for an actual reference.
|
|
1623
1649
|
const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
@@ -2020,9 +2046,239 @@ function normalizePropertyNamesWalk(root) {
|
|
|
2020
2046
|
return walk(root)
|
|
2021
2047
|
}
|
|
2022
2048
|
|
|
2049
|
+
// A local `$ref` that is a JSON pointer to anything other than a top-level
|
|
2050
|
+
// definition (`#/definitions/item/properties/id`, `#/properties/foo`,
|
|
2051
|
+
// `#/$defs/a/items`) became the generators' own definition reference: the
|
|
2052
|
+
// pointer is resolved here, at compile time, and its target added as a
|
|
2053
|
+
// definition under a name of its own, so every path that already handles
|
|
2054
|
+
// `#/$defs/name` handles it, cycles included. The generators knew only the
|
|
2055
|
+
// top-level form and declined the rest, which kept 40 of 980 SchemaStore
|
|
2056
|
+
// schemas, tsconfig.json among them, on the interpreted engine for every
|
|
2057
|
+
// document. Left alone, as before: a pointer that does not resolve to a
|
|
2058
|
+
// schema, one that passes through a subschema with its own `$id` (another
|
|
2059
|
+
// base, whose pointers mean something else), and every `$ref` inside such a
|
|
2060
|
+
// subschema. Copy on write; errors keep their paths, which name the place of
|
|
2061
|
+
// the reference, not of its target, and are ordered against the schema as
|
|
2062
|
+
// the caller wrote it.
|
|
2063
|
+
const DEF_REF = /^#\/(?:\$defs|definitions)\/[^/]+$/
|
|
2064
|
+
const _deepRefsNormalized = new WeakMap()
|
|
2065
|
+
function normalizeDeepRefs(root) {
|
|
2066
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2067
|
+
const hit = _deepRefsNormalized.get(root)
|
|
2068
|
+
if (hit !== undefined) return hit
|
|
2069
|
+
const out = normalizeDeepRefsWalk(root)
|
|
2070
|
+
_deepRefsNormalized.set(root, out)
|
|
2071
|
+
return out
|
|
2072
|
+
}
|
|
2073
|
+
function resolveLocalPointer(root, ref) {
|
|
2074
|
+
let node = root
|
|
2075
|
+
const segs = ref.slice(2).split('/')
|
|
2076
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2077
|
+
let seg = segs[i].replace(/~1/g, '/').replace(/~0/g, '~')
|
|
2078
|
+
// Draft 7 schemas reach the generators with `definitions` renamed to
|
|
2079
|
+
// `$defs` (lib/draft7.js) and their references as written, which the
|
|
2080
|
+
// top-level form already reads either way; so does this.
|
|
2081
|
+
if (i === 0 && (seg === 'definitions' || seg === '$defs') && !Object.prototype.hasOwnProperty.call(node, seg)) seg = seg === 'definitions' ? '$defs' : 'definitions'
|
|
2082
|
+
if (node === null || typeof node !== 'object') return undefined
|
|
2083
|
+
if (Array.isArray(node)) {
|
|
2084
|
+
if (!/^(?:0|[1-9]\d*)$/.test(seg) || +seg >= node.length) return undefined
|
|
2085
|
+
node = node[+seg]
|
|
2086
|
+
} else {
|
|
2087
|
+
if (!Object.prototype.hasOwnProperty.call(node, seg)) return undefined
|
|
2088
|
+
node = node[seg]
|
|
2089
|
+
}
|
|
2090
|
+
if (node !== null && typeof node === 'object' && !Array.isArray(node) && typeof node.$id === 'string') return undefined
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof node === 'boolean') return node
|
|
2093
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return undefined
|
|
2094
|
+
return node
|
|
2095
|
+
}
|
|
2096
|
+
function normalizeDeepRefsWalk(root) {
|
|
2097
|
+
// First pass: the deep pointers used outside any nested `$id`, and their
|
|
2098
|
+
// targets.
|
|
2099
|
+
const targets = new Map()
|
|
2100
|
+
const scan = (node, nested, seen) => {
|
|
2101
|
+
if (node === null || typeof node !== 'object') return
|
|
2102
|
+
if (seen.has(node)) return
|
|
2103
|
+
seen.add(node)
|
|
2104
|
+
if (Array.isArray(node)) { for (const n of node) scan(n, nested, seen); return }
|
|
2105
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2106
|
+
if (!inner && typeof node.$ref === 'string' && node.$ref.startsWith('#/') && !DEF_REF.test(node.$ref) && !targets.has(node.$ref)) {
|
|
2107
|
+
const t = resolveLocalPointer(root, node.$ref)
|
|
2108
|
+
if (t !== undefined) targets.set(node.$ref, t)
|
|
2109
|
+
}
|
|
2110
|
+
for (const k of Object.keys(node)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') scan(node[k], inner, seen)
|
|
2111
|
+
}
|
|
2112
|
+
scan(root, false, new Set())
|
|
2113
|
+
if (targets.size === 0) return root
|
|
2114
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2115
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2116
|
+
const names = new Map()
|
|
2117
|
+
let n = 0
|
|
2118
|
+
for (const ref of targets.keys()) {
|
|
2119
|
+
let name
|
|
2120
|
+
do { name = '__ata_ptr_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2121
|
+
names.set(ref, name)
|
|
2122
|
+
}
|
|
2123
|
+
// Second pass: the references rewritten, copy on write, sharing one copy
|
|
2124
|
+
// per node so a target reached twice stays one object.
|
|
2125
|
+
const memo = new Map()
|
|
2126
|
+
const walk = (node, nested) => {
|
|
2127
|
+
if (node === null || typeof node !== 'object') return node
|
|
2128
|
+
const key = nested ? null : node
|
|
2129
|
+
if (key !== null && memo.has(key)) return memo.get(key)
|
|
2130
|
+
if (Array.isArray(node)) {
|
|
2131
|
+
let out = node
|
|
2132
|
+
if (key !== null) memo.set(key, out)
|
|
2133
|
+
for (let i = 0; i < node.length; i++) {
|
|
2134
|
+
const w = walk(node[i], nested)
|
|
2135
|
+
if (w !== node[i]) { if (out === node) { out = node.slice(); if (key !== null) memo.set(key, out) } out[i] = w }
|
|
2136
|
+
}
|
|
2137
|
+
return out
|
|
2138
|
+
}
|
|
2139
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2140
|
+
if (inner) return node
|
|
2141
|
+
let out = node
|
|
2142
|
+
memo.set(node, out)
|
|
2143
|
+
const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
|
|
2144
|
+
if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
|
|
2145
|
+
for (const k of Object.keys(node)) {
|
|
2146
|
+
if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
2147
|
+
const v = node[k]
|
|
2148
|
+
if (v === null || typeof v !== 'object') continue
|
|
2149
|
+
const w = walk(v, false)
|
|
2150
|
+
if (w !== v) own(k, w)
|
|
2151
|
+
}
|
|
2152
|
+
return out
|
|
2153
|
+
}
|
|
2154
|
+
const rewritten = walk(root, false)
|
|
2155
|
+
const out = rewritten === root ? { ...root } : rewritten
|
|
2156
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2157
|
+
for (const [ref, name] of names) {
|
|
2158
|
+
const t = targets.get(ref)
|
|
2159
|
+
defs[name] = typeof t === 'boolean' ? t : walk(t, false)
|
|
2160
|
+
}
|
|
2161
|
+
out[defsKey] = defs
|
|
2162
|
+
return out
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Every reference the interpreted engine can resolve becomes a definition
|
|
2166
|
+
// reference the generators handle: URNs, `$id`-relative and absolute URIs,
|
|
2167
|
+
// anchors, pointers into a resource with its own `$id`, names with characters
|
|
2168
|
+
// the generators cannot spell, documents registered through `schemas`. The
|
|
2169
|
+
// target of each is found by the interpreter's own resolver (indexSchemas and
|
|
2170
|
+
// resolveRef in lib/interpreter.js), so the two engines cannot resolve a
|
|
2171
|
+
// reference differently, and is added under a name of its own as with
|
|
2172
|
+
// normalizeDeepRefs. Once every reference names a definition, nested `$id`s
|
|
2173
|
+
// no longer change what anything means and are dropped from the copy, which
|
|
2174
|
+
// is what kept those schemas from compiling. Left to normalizeDeepRefs, as
|
|
2175
|
+
// before: a schema with dynamic references (their meaning depends on the
|
|
2176
|
+
// resources at run time), one with an embedded resource in another dialect,
|
|
2177
|
+
// and one with any reference the resolver cannot resolve.
|
|
2178
|
+
const _refsNormalized = new WeakMap()
|
|
2179
|
+
function normalizeRefs(root, schemaMap) {
|
|
2180
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2181
|
+
let byMap = _refsNormalized.get(root)
|
|
2182
|
+
const mapKey = schemaMap || _refsNormalized
|
|
2183
|
+
if (byMap !== undefined && byMap.has(mapKey)) return byMap.get(mapKey)
|
|
2184
|
+
let out
|
|
2185
|
+
try { out = normalizeRefsWalk(root, schemaMap) } catch { out = null }
|
|
2186
|
+
if (out === null) out = normalizeDeepRefs(root)
|
|
2187
|
+
if (byMap === undefined) { byMap = new Map(); _refsNormalized.set(root, byMap) }
|
|
2188
|
+
byMap.set(mapKey, out)
|
|
2189
|
+
return out
|
|
2190
|
+
}
|
|
2191
|
+
const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
|
|
2192
|
+
function normalizeRefsWalk(root, schemaMap) {
|
|
2193
|
+
const text = JSON.stringify(root)
|
|
2194
|
+
if (!text.includes('"$ref"')) return root
|
|
2195
|
+
if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
|
|
2196
|
+
const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
|
|
2197
|
+
const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
|
|
2198
|
+
const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
|
|
2199
|
+
// Resolve every reference reachable from the root, following into targets
|
|
2200
|
+
// in other documents.
|
|
2201
|
+
const names = new Map() // target node -> synthetic name
|
|
2202
|
+
const refTarget = new Map() // reference site node -> target node
|
|
2203
|
+
const visited = new Set()
|
|
2204
|
+
let fail = false
|
|
2205
|
+
const visit = (node) => {
|
|
2206
|
+
if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
|
|
2207
|
+
visited.add(node)
|
|
2208
|
+
if (Array.isArray(node)) { for (const n of node) visit(n); return }
|
|
2209
|
+
if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
|
|
2210
|
+
// A dynamic reference reached through another document means the same as
|
|
2211
|
+
// one in the root: its target depends on the resources at run time.
|
|
2212
|
+
if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined || node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
|
|
2213
|
+
if (typeof node.$ref === 'string' && state.nodeBase.has(node)) {
|
|
2214
|
+
const base = state.nodeBase.get(node)
|
|
2215
|
+
const ref = node.$ref
|
|
2216
|
+
const local = base === state.rootBase && (ref === '#' || (DEF_REF.test(ref) && !/[~%]/.test(ref)))
|
|
2217
|
+
if (!local) {
|
|
2218
|
+
const { node: t } = resolveRef(ref, base, state)
|
|
2219
|
+
if (t === undefined || (typeof t !== 'boolean' && (t === null || typeof t !== 'object' || Array.isArray(t)))) { fail = true; return }
|
|
2220
|
+
refTarget.set(node, t)
|
|
2221
|
+
// The root, however it is spelled, is `#`: a copy of it under its own
|
|
2222
|
+
// definitions would contain itself.
|
|
2223
|
+
if (t === root) { /* named '#' below */ }
|
|
2224
|
+
else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
|
|
2225
|
+
if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
|
|
2229
|
+
}
|
|
2230
|
+
visit(root)
|
|
2231
|
+
if (fail) return null
|
|
2232
|
+
if (refTarget.size === 0) return root
|
|
2233
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2234
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2235
|
+
let n = 0
|
|
2236
|
+
for (const t of names.keys()) {
|
|
2237
|
+
let name
|
|
2238
|
+
do { name = '__ata_ref_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2239
|
+
names.set(t, name)
|
|
2240
|
+
}
|
|
2241
|
+
// The copy: references renamed, nested `$id`s dropped.
|
|
2242
|
+
const memo = new Map()
|
|
2243
|
+
const walk = (node) => {
|
|
2244
|
+
if (node === null || typeof node !== 'object') return node
|
|
2245
|
+
if (memo.has(node)) return memo.get(node)
|
|
2246
|
+
if (Array.isArray(node)) {
|
|
2247
|
+
const out = []
|
|
2248
|
+
memo.set(node, out)
|
|
2249
|
+
for (const x of node) out.push(walk(x))
|
|
2250
|
+
return out
|
|
2251
|
+
}
|
|
2252
|
+
const out = {}
|
|
2253
|
+
memo.set(node, out)
|
|
2254
|
+
for (const k of Object.keys(node)) {
|
|
2255
|
+
if (k === '$id' && node !== root) continue
|
|
2256
|
+
if (k === '$ref' && refTarget.has(node)) {
|
|
2257
|
+
const t = refTarget.get(node)
|
|
2258
|
+
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2259
|
+
continue
|
|
2260
|
+
}
|
|
2261
|
+
out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
|
|
2262
|
+
}
|
|
2263
|
+
return out
|
|
2264
|
+
}
|
|
2265
|
+
const out = walk(root)
|
|
2266
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2267
|
+
for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
|
|
2268
|
+
out[defsKey] = defs
|
|
2269
|
+
return out
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// What the three generators compile from: the schema with deep local pointers
|
|
2273
|
+
// turned into definition references, then propertyNames normalized. The same
|
|
2274
|
+
// for all three, so they decline and agree on the same schemas.
|
|
2275
|
+
function prepareForCodegen(schema, schemaMap) {
|
|
2276
|
+
return normalizePropertyNames(normalizeRefs(schema, schemaMap))
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2023
2279
|
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
2024
2280
|
const inputSchema = schema
|
|
2025
|
-
schema =
|
|
2281
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
2026
2282
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
2027
2283
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2028
2284
|
// removeAdditional: a verdict function that deletes the keys the remover
|
|
@@ -2093,7 +2349,14 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2093
2349
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
2094
2350
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
2095
2351
|
|
|
2096
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes
|
|
2352
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
|
|
2353
|
+
keywords: (opts && opts.keywords) || null, kwEmitted: null }
|
|
2354
|
+
if (ctx.keywords) {
|
|
2355
|
+
// Macro keywords expand into a schema of their own, which this generator
|
|
2356
|
+
// does not apply; such a schema stays on the interpreted engine.
|
|
2357
|
+
if (usesMacroKeyword(schema, schemaMap, ctx.keywords)) return null
|
|
2358
|
+
ctx.kwEmitted = new Set()
|
|
2359
|
+
}
|
|
2097
2360
|
const lines = []
|
|
2098
2361
|
try {
|
|
2099
2362
|
genCode(schema, 'd', lines, ctx)
|
|
@@ -2101,6 +2364,16 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2101
2364
|
if (e === DECLINE) return null
|
|
2102
2365
|
throw e
|
|
2103
2366
|
}
|
|
2367
|
+
// Every schema node that carries a custom keyword must have had its check
|
|
2368
|
+
// written. A path that inlined a node without going through genCodeNode
|
|
2369
|
+
// would drop the keyword and accept what it should reject, so a node that
|
|
2370
|
+
// was not reached declines the whole schema to the interpreted engine
|
|
2371
|
+
// instead. Unreferenced definitions decline too: slower, never wrong.
|
|
2372
|
+
if (ctx.keywords) {
|
|
2373
|
+
for (const node of keywordNodes(schema, schemaMap, ctx.keywords)) {
|
|
2374
|
+
if (!ctx.kwEmitted.has(node)) return null
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2104
2377
|
|
|
2105
2378
|
// Append deferred checks (additionalProperties, unevaluatedProperties) at the end
|
|
2106
2379
|
if (ctx.deferredChecks) {
|
|
@@ -2137,7 +2410,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2137
2410
|
// are built when first asked for, not here: most validators never call
|
|
2138
2411
|
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
2139
2412
|
// more on every schema was about a third of what a first call cost.
|
|
2140
|
-
let body, hybridThunk, tailThunk
|
|
2413
|
+
let body, hybridThunk, tailThunk, resultThunk
|
|
2141
2414
|
if (ctx.usesRecursion) {
|
|
2142
2415
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
2143
2416
|
// that points back at itself settles instead of exhausting the stack.
|
|
@@ -2147,6 +2420,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2147
2420
|
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2148
2421
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
2149
2422
|
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2423
|
+
resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2150
2424
|
tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2151
2425
|
} else if (defSets.length > 0) {
|
|
2152
2426
|
// No self-reference at the root, but a def on a cycle can still be
|
|
@@ -2154,10 +2428,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2154
2428
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
2155
2429
|
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2156
2430
|
hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2431
|
+
resultThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2157
2432
|
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2158
2433
|
} else {
|
|
2159
2434
|
body = checkStr + '\n return true'
|
|
2160
2435
|
hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
|
|
2436
|
+
resultThunk = () => replaceTopLevel(checkStr + '\n return true', `return ${RESULT_TRUE}`, `return ${RESULT_FALSE}`)
|
|
2161
2437
|
// An early accept inside the checks has to reach the tail too, so every
|
|
2162
2438
|
// top-level `return true` becomes the tail call, by the same rewrite the
|
|
2163
2439
|
// hybrid above relies on and test_hybrid_agreement holds to the suite.
|
|
@@ -2197,6 +2473,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2197
2473
|
return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
|
|
2198
2474
|
}
|
|
2199
2475
|
|
|
2476
|
+
// validate() itself: the same checks returning the result objects, for a
|
|
2477
|
+
// validator past its first calls (see validator-core). One function per
|
|
2478
|
+
// schema means the result is built at an allocation site of its own and
|
|
2479
|
+
// nothing between the caller and the checks is shared with other schemas.
|
|
2480
|
+
// Shared wrappers are what made reading results slow across many schemas:
|
|
2481
|
+
// every validator's calls went through the same few call sites. The
|
|
2482
|
+
// rejection is the lazy one, built from the arguments.
|
|
2483
|
+
let resultFactory
|
|
2484
|
+
boolFn._resultFactory = (LR, B, W, EE) => {
|
|
2485
|
+
if (resultFactory === undefined) {
|
|
2486
|
+
try {
|
|
2487
|
+
resultFactory = new Function(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
|
|
2488
|
+
} catch {
|
|
2489
|
+
resultFactory = null
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2200
2495
|
// The same function with one more check where it would return true, for a
|
|
2201
2496
|
// wrapper that enforces something the schema does not carry (see
|
|
2202
2497
|
// Validator#_extendVerdict). Calling the check from here rather than from a
|
|
@@ -2274,40 +2569,75 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2274
2569
|
|
|
2275
2570
|
// Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
|
|
2276
2571
|
// Tracks function nesting depth to preserve nested function internals.
|
|
2572
|
+
const RESULT_TRUE = '{valid:true,data:d,errors:__ataEE}'
|
|
2573
|
+
const RESULT_FALSE = 'new __ataLR(__ataB,d,__ataW)'
|
|
2574
|
+
// The end of the string literal that starts at `i` (a quote character), or
|
|
2575
|
+
// the end of the code when it does not close.
|
|
2576
|
+
function skipString(code, i) {
|
|
2577
|
+
const q = code[i]
|
|
2578
|
+
let j = i + 1
|
|
2579
|
+
while (j < code.length) {
|
|
2580
|
+
const c = code[j]
|
|
2581
|
+
if (c === '\\') { j += 2; continue }
|
|
2582
|
+
if (c === q) return j + 1
|
|
2583
|
+
j++
|
|
2584
|
+
}
|
|
2585
|
+
return code.length
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2277
2588
|
function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
|
|
2278
2589
|
let fnDepth = 0, result = '', i = 0
|
|
2279
2590
|
while (i < code.length) {
|
|
2591
|
+
// String literals are copied as they are. A property called `function`
|
|
2592
|
+
// reads `d["function"]`, and the scan took the word for a nested function
|
|
2593
|
+
// and left every return after it alone, so the hybrid returned a bare
|
|
2594
|
+
// `false` where a result was due: validateJSON answered `false` instead
|
|
2595
|
+
// of a rejection for such schemas. Text such as `return false` or a brace
|
|
2596
|
+
// inside a string is not code either.
|
|
2597
|
+
const ch = code[i]
|
|
2598
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2599
|
+
const j = skipString(code, i)
|
|
2600
|
+
result += code.slice(i, j)
|
|
2601
|
+
i = j
|
|
2602
|
+
continue
|
|
2603
|
+
}
|
|
2280
2604
|
// A nested closure keeps its own returns. Both spellings count: a
|
|
2281
2605
|
// `function` keyword and an arrow with a block body. The inline pattern
|
|
2282
2606
|
// compiler emits `(()=>{...return false...return true})()` and rewriting
|
|
2283
2607
|
// those returns turned the arrow's boolean into an object, which made
|
|
2284
2608
|
// `!(cond && obj)` false and let the hybrid accept what the boolean
|
|
2285
2609
|
// program rejected.
|
|
2286
|
-
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
|
|
2610
|
+
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1])) && !/[a-zA-Z0-9_$]/.test(code[i + 8] || '')
|
|
2287
2611
|
const isArrowBlock = code.startsWith('=>{', i)
|
|
2288
2612
|
if (isFunctionKw || isArrowBlock) {
|
|
2289
|
-
// Skip to the opening brace, then track all braces inside the body
|
|
2613
|
+
// Skip to the opening brace, then track all braces inside the body,
|
|
2614
|
+
// strings skipped as above.
|
|
2290
2615
|
let j = isArrowBlock ? i + 2 : i + 8
|
|
2291
2616
|
while (j < code.length && code[j] !== '{') j++
|
|
2292
2617
|
result += code.slice(i, j + 1)
|
|
2293
2618
|
i = j + 1
|
|
2294
|
-
// Track braces inside this function body
|
|
2295
2619
|
let braceDepth = 1
|
|
2296
2620
|
while (i < code.length && braceDepth > 0) {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2621
|
+
const c = code[i]
|
|
2622
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
2623
|
+
const k = skipString(code, i)
|
|
2624
|
+
result += code.slice(i, k)
|
|
2625
|
+
i = k
|
|
2626
|
+
continue
|
|
2627
|
+
}
|
|
2628
|
+
if (c === '{') braceDepth++
|
|
2629
|
+
else if (c === '}') braceDepth--
|
|
2630
|
+
result += c
|
|
2301
2631
|
i++
|
|
2302
2632
|
}
|
|
2303
|
-
} else if (code.startsWith('return false', i)) {
|
|
2633
|
+
} else if (code.startsWith('return false', i) && (i + 12 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 12]))) {
|
|
2304
2634
|
result += onFalse
|
|
2305
2635
|
i += 12
|
|
2306
|
-
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-
|
|
2636
|
+
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 11]))) {
|
|
2307
2637
|
result += onTrue
|
|
2308
2638
|
i += 11
|
|
2309
2639
|
} else {
|
|
2310
|
-
result +=
|
|
2640
|
+
result += ch
|
|
2311
2641
|
i++
|
|
2312
2642
|
}
|
|
2313
2643
|
}
|
|
@@ -2449,6 +2779,115 @@ function _deferOrInline(ctx, lines, v, check) {
|
|
|
2449
2779
|
// 'object' = we know v is a non-null non-array object
|
|
2450
2780
|
// 'array' = we know v is an array
|
|
2451
2781
|
// 'string' / 'number' / 'integer' = we know the primitive type
|
|
2782
|
+
// Custom keywords (the `keywords` option, normalized by lib/keywords.js) as
|
|
2783
|
+
// calls into the caller's functions, threaded in as closure variables the way
|
|
2784
|
+
// user formats are. Each check is built here with the interpreter's own
|
|
2785
|
+
// semantics, so the two engines cannot disagree on one: the keyword is skipped
|
|
2786
|
+
// for data outside its declared types (the interpreter's type bits), the
|
|
2787
|
+
// compile form is built once per schema node, and any errors the function
|
|
2788
|
+
// leaves on itself are cleared after the call, as runCustomV does. Written
|
|
2789
|
+
// first in the node, before anything that could return early.
|
|
2790
|
+
// The interpreter's type bits (lib/interpreter.js typeBit and dataBits),
|
|
2791
|
+
// copied rather than required: loading the interpreter added about a
|
|
2792
|
+
// millisecond to the first call of a schema that never needs it.
|
|
2793
|
+
// tests/test_custom_keyword_codegen.js holds the copies equal to the originals.
|
|
2794
|
+
const KW_T_ANY = 127
|
|
2795
|
+
function kwTypeBit (name) {
|
|
2796
|
+
switch (name) {
|
|
2797
|
+
case 'string': return 1
|
|
2798
|
+
case 'number': return 2
|
|
2799
|
+
case 'integer': return 4
|
|
2800
|
+
case 'boolean': return 8
|
|
2801
|
+
case 'null': return 16
|
|
2802
|
+
case 'object': return 32
|
|
2803
|
+
case 'array': return 64
|
|
2804
|
+
default: return KW_T_ANY
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
function kwDataBits (d) {
|
|
2808
|
+
switch (typeof d) {
|
|
2809
|
+
case 'string': return 1
|
|
2810
|
+
case 'number':
|
|
2811
|
+
if (!isFinite(d)) return 0
|
|
2812
|
+
return Number.isInteger(d) ? 2 | 4 : 2
|
|
2813
|
+
case 'boolean': return 8
|
|
2814
|
+
case 'object':
|
|
2815
|
+
if (d === null) return 16
|
|
2816
|
+
return Array.isArray(d) ? 64 : 32
|
|
2817
|
+
default: return 0
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
function genCustomKeywords (schema, v, lines, ctx) {
|
|
2821
|
+
for (const key of Object.keys(schema)) {
|
|
2822
|
+
const def = ctx.keywords[key]
|
|
2823
|
+
if (def === undefined || def.macro !== null) continue
|
|
2824
|
+
const value = schema[key]
|
|
2825
|
+
const dataBits = kwDataBits
|
|
2826
|
+
const T_ANY = KW_T_ANY
|
|
2827
|
+
let mask = T_ANY
|
|
2828
|
+
if (def.types !== null) {
|
|
2829
|
+
mask = 0
|
|
2830
|
+
for (const t of def.types) mask |= kwTypeBit(t)
|
|
2831
|
+
}
|
|
2832
|
+
let fn
|
|
2833
|
+
if (def.compile !== null) {
|
|
2834
|
+
fn = def.compile(value, schema)
|
|
2835
|
+
if (typeof fn !== 'function') throw new Error(`keyword "${key}": compile must return a function`)
|
|
2836
|
+
} else {
|
|
2837
|
+
const validate = def.validate
|
|
2838
|
+
fn = (data) => validate(value, data, schema)
|
|
2839
|
+
fn.source = validate
|
|
2840
|
+
}
|
|
2841
|
+
const holder = () => fn.source || fn
|
|
2842
|
+
const check = mask === T_ANY
|
|
2843
|
+
? (data) => { const ok = fn(data); const h = holder(); if (h.errors) h.errors = null; return !!ok }
|
|
2844
|
+
: (data) => {
|
|
2845
|
+
if ((dataBits(data) & mask) === 0) return true
|
|
2846
|
+
const ok = fn(data); const h = holder(); if (h.errors) h.errors = null; return !!ok
|
|
2847
|
+
}
|
|
2848
|
+
const name = '_kwc' + ctx.closureVars.length
|
|
2849
|
+
ctx.closureVars.push(name)
|
|
2850
|
+
ctx.closureVals.push(check)
|
|
2851
|
+
lines.push(`if(!${name}(${v}))return false`)
|
|
2852
|
+
}
|
|
2853
|
+
ctx.kwEmitted.add(schema)
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Map keys under these are names, not keywords (lib/keywords.js NAME_MAPS).
|
|
2857
|
+
const KW_NAME_MAPS = new Set([
|
|
2858
|
+
'properties', 'patternProperties', '$defs', 'definitions',
|
|
2859
|
+
'dependentSchemas', 'dependentRequired', 'dependencies', 'propertyDependencies',
|
|
2860
|
+
])
|
|
2861
|
+
function forEachKeywordNode (schema, schemaMap, keywords, visit) {
|
|
2862
|
+
const seen = new Set()
|
|
2863
|
+
const walk = (node, keysAreNames) => {
|
|
2864
|
+
if (node === null || typeof node !== 'object' || seen.has(node)) return
|
|
2865
|
+
seen.add(node)
|
|
2866
|
+
if (Array.isArray(node)) { for (const item of node) walk(item, false); return }
|
|
2867
|
+
let has = false
|
|
2868
|
+
for (const key of Object.keys(node)) {
|
|
2869
|
+
if (!keysAreNames && keywords[key] !== undefined) has = true
|
|
2870
|
+
const val = node[key]
|
|
2871
|
+
if (val !== null && typeof val === 'object') walk(val, !keysAreNames && KW_NAME_MAPS.has(key))
|
|
2872
|
+
}
|
|
2873
|
+
if (has) visit(node)
|
|
2874
|
+
}
|
|
2875
|
+
walk(schema, false)
|
|
2876
|
+
if (schemaMap) for (const s of schemaMap.values()) walk(s, false)
|
|
2877
|
+
}
|
|
2878
|
+
function keywordNodes (schema, schemaMap, keywords) {
|
|
2879
|
+
const out = []
|
|
2880
|
+
forEachKeywordNode(schema, schemaMap, keywords, (n) => out.push(n))
|
|
2881
|
+
return out
|
|
2882
|
+
}
|
|
2883
|
+
function usesMacroKeyword (schema, schemaMap, keywords) {
|
|
2884
|
+
let macro = false
|
|
2885
|
+
forEachKeywordNode(schema, schemaMap, keywords, (n) => {
|
|
2886
|
+
for (const key of Object.keys(n)) if (keywords[key] !== undefined && keywords[key].macro !== null) macro = true
|
|
2887
|
+
})
|
|
2888
|
+
return macro
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2452
2891
|
function genCode(schema, v, lines, ctx, knownType) {
|
|
2453
2892
|
return withPlain(schema, v, lines, ctx, () => genCodeNode(schema, v, lines, ctx, knownType))
|
|
2454
2893
|
}
|
|
@@ -2459,6 +2898,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2459
2898
|
if (schema === false) { lines.push('return false'); return }
|
|
2460
2899
|
if (schema === true) return
|
|
2461
2900
|
if (typeof schema !== 'object' || schema === null) return
|
|
2901
|
+
if (ctx.keywords) genCustomKeywords(schema, v, lines, ctx)
|
|
2462
2902
|
if (!ctx.regExpMap) {
|
|
2463
2903
|
ctx.regExpMap = new Map();
|
|
2464
2904
|
}
|
|
@@ -3381,14 +3821,32 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3381
3821
|
return handledPropertyNames
|
|
3382
3822
|
}
|
|
3383
3823
|
|
|
3824
|
+
// Whether the static unevaluatedProperties model checks the anyOf itself, so
|
|
3825
|
+
// genCodeNode leaves it out. Not when the set is built at run time
|
|
3826
|
+
// (unevalBuiltAtRuntime): that form only collects, and dropping the anyOf
|
|
3827
|
+
// there accepted a document no branch matched.
|
|
3384
3828
|
function unevalChecksAnyOf(schema, ctx) {
|
|
3385
3829
|
if (schema.unevaluatedProperties !== false) return false
|
|
3386
3830
|
const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3387
|
-
return r.dynamic && !r.allProps
|
|
3831
|
+
return r.dynamic && !r.allProps && !unevalBuiltAtRuntime(schema, ctx, r)
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
// The unevaluatedProperties forms the static model cannot represent; those
|
|
3835
|
+
// build the evaluated set at run time (genUnevalDynamicVerdict). Decided in
|
|
3836
|
+
// one place so genCodeNode and genUnevaluatedPropertiesNode cannot disagree
|
|
3837
|
+
// about which of them checks the node's anyOf.
|
|
3838
|
+
function unevalBuiltAtRuntime(schema, ctx, r) {
|
|
3839
|
+
if (schema.unevaluatedProperties === true || schema.unevaluatedProperties === undefined) return false
|
|
3840
|
+
if (r === undefined) r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3841
|
+
return r.dynamic && (r.allProps || !unevalPropsModeled(schema, ctx.schemaMap))
|
|
3388
3842
|
}
|
|
3389
3843
|
|
|
3390
3844
|
function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
|
|
3391
3845
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3846
|
+
if (unevalBuiltAtRuntime(schema, ctx, evalResult)) {
|
|
3847
|
+
genUnevalDynamicVerdict(schema, v, lines, ctx, isObj)
|
|
3848
|
+
return
|
|
3849
|
+
}
|
|
3392
3850
|
|
|
3393
3851
|
if (evalResult.allProps || schema.unevaluatedProperties === true) {
|
|
3394
3852
|
// All props evaluated or unevaluatedProperties:true — no-op
|
|
@@ -4204,7 +4662,7 @@ function unevalLocalOk (node, key) {
|
|
|
4204
4662
|
// generic and may visit data positions (an enum value, say); judging those is
|
|
4205
4663
|
// harmless, since a data node that fails the test only makes the answer more
|
|
4206
4664
|
// conservative and the generators never read data positions as schemas.
|
|
4207
|
-
function unevalAllProvablyLocal (root) {
|
|
4665
|
+
function unevalAllProvablyLocal (root, only) {
|
|
4208
4666
|
const seen = new Set()
|
|
4209
4667
|
const stack = [root]
|
|
4210
4668
|
while (stack.length) {
|
|
@@ -4212,7 +4670,7 @@ function unevalAllProvablyLocal (root) {
|
|
|
4212
4670
|
if (n === null || typeof n !== 'object' || seen.has(n)) continue
|
|
4213
4671
|
seen.add(n)
|
|
4214
4672
|
if (!Array.isArray(n)) {
|
|
4215
|
-
if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4673
|
+
if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4216
4674
|
if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
|
|
4217
4675
|
}
|
|
4218
4676
|
for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
|
|
@@ -4269,7 +4727,7 @@ function genCharCodeSwitch(keys, v) {
|
|
|
4269
4727
|
// Valid path is still fast — only error path does extra work.
|
|
4270
4728
|
function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
|
|
4271
4729
|
const inputSchema = schema
|
|
4272
|
-
schema =
|
|
4730
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
4273
4731
|
// unevaluated* is generated only where it is provably its plain counterpart
|
|
4274
4732
|
// (see unevalLocalOk); any occurrence that is not keeps the whole schema on
|
|
4275
4733
|
// the interpreted engine's error path, exactly as before.
|
|
@@ -4502,7 +4960,14 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4502
4960
|
const subLines = []
|
|
4503
4961
|
const subSp = branchSp + '/' + i
|
|
4504
4962
|
if (typeof sub === 'object' && sub !== null) {
|
|
4505
|
-
|
|
4963
|
+
// A branch is a function of its own with a local `_e`; a collapse
|
|
4964
|
+
// inside it reports there, not into the combined function's lists.
|
|
4965
|
+
ctx.inBranchFn = (ctx.inBranchFn || 0) + 1
|
|
4966
|
+
try {
|
|
4967
|
+
genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
|
|
4968
|
+
} finally {
|
|
4969
|
+
ctx.inBranchFn--
|
|
4970
|
+
}
|
|
4506
4971
|
} else if (sub === false) {
|
|
4507
4972
|
// A false branch never matches; it reports the same error the
|
|
4508
4973
|
// interpreter attaches to a false schema.
|
|
@@ -4526,8 +4991,19 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4526
4991
|
const pp = pathExpr || '""'
|
|
4527
4992
|
ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
|
|
4528
4993
|
const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
|
|
4529
|
-
|
|
4530
|
-
|
|
4994
|
+
// branchSp is source text for a quoted literal, with backslashes and quotes
|
|
4995
|
+
// escaped; the ordinal is looked up by the path itself. Looked up by the
|
|
4996
|
+
// text, a path through a pattern such as `^\\d+$` found no node, or the
|
|
4997
|
+
// wrong one, and the collapsed error sorted out of place against the other
|
|
4998
|
+
// engines.
|
|
4999
|
+
// In the runtime shape there is no ordering key (the reader looks the path
|
|
5000
|
+
// up), so the collapsed error is in its final shape as it is built.
|
|
5001
|
+
const ord = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
|
|
5002
|
+
// In the combined function `_e` is created on the first error and every
|
|
5003
|
+
// error is collected (`_all` is true there), so the report only pushes.
|
|
5004
|
+
const report = ctx.inCombined && !ctx.inBranchFn
|
|
5005
|
+
? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
|
|
5006
|
+
: `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
4531
5007
|
if (keyword === 'anyOf') {
|
|
4532
5008
|
// anyOf reports nothing once a branch passes, so the branches are first
|
|
4533
5009
|
// only asked whether they pass. They are called with `_all` set to 0, a
|
|
@@ -4541,12 +5017,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4541
5017
|
// no one read. Only when no branch passes do they run again with the
|
|
4542
5018
|
// caller's `_all`, and the collapse sees them all, as before.
|
|
4543
5019
|
const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
|
|
4544
|
-
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors
|
|
5020
|
+
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
|
|
4545
5021
|
} else {
|
|
4546
5022
|
// Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
|
|
4547
5023
|
// the passing branches and answers; the branch errors of a probe are not
|
|
4548
5024
|
// errors anyone reads and are not collapsed.
|
|
4549
|
-
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){
|
|
5025
|
+
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
|
|
4550
5026
|
}
|
|
4551
5027
|
}
|
|
4552
5028
|
|
|
@@ -4574,6 +5050,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4574
5050
|
}
|
|
4575
5051
|
// $ref — resolve local and cross-schema refs
|
|
4576
5052
|
if (schema.$ref) {
|
|
5053
|
+
// The reference is followed and the rest of the node is not compiled; a
|
|
5054
|
+
// keyword beside it that validates would be dropped, so the node
|
|
5055
|
+
// declines (as in genCodeCNode). Reached since unevaluatedProperties
|
|
5056
|
+
// next to a $ref stopped declining the combined function, whose oneOf
|
|
5057
|
+
// and anyOf branches come from this generator: a branch was taken as
|
|
5058
|
+
// passing with its siblings unchecked.
|
|
5059
|
+
for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
|
|
4577
5060
|
// A self-reference cannot be expressed here yet: the root helper takes no
|
|
4578
5061
|
// path argument, so errors raised through it would carry the path of the
|
|
4579
5062
|
// root rather than of the value that failed. This used to fall through as
|
|
@@ -4674,7 +5157,9 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4674
5157
|
}
|
|
4675
5158
|
})
|
|
4676
5159
|
const expected = types.join(', ')
|
|
4677
|
-
|
|
5160
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
5161
|
+
// reports it.
|
|
5162
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
4678
5163
|
{
|
|
4679
5164
|
const typeSp = `${schemaPrefix}/type`
|
|
4680
5165
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
@@ -4705,7 +5190,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4705
5190
|
if (cv === null || typeof cv !== 'object') {
|
|
4706
5191
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4707
5192
|
} else {
|
|
4708
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
5193
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4709
5194
|
}
|
|
4710
5195
|
}
|
|
4711
5196
|
|
|
@@ -4875,6 +5360,11 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4875
5360
|
// to emit. The error keeps the keyword's own identity: the interpreted
|
|
4876
5361
|
// engine reports these as unevaluatedProperties with the key named, and the
|
|
4877
5362
|
// two engines are pinned to identical output.
|
|
5363
|
+
// Inside the combined function (a oneOf or anyOf branch, see
|
|
5364
|
+
// emitBranchCollapse) this generator only has the static form. A branch
|
|
5365
|
+
// whose evaluated set depends on its own branches would report the wrong
|
|
5366
|
+
// errors, and the collapse would pick the wrong closest branch; declined.
|
|
5367
|
+
if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
|
|
4878
5368
|
if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
4879
5369
|
|
|
4880
5370
|
// unevaluatedItems: false under the same rule. The interpreted engine
|
|
@@ -4985,18 +5475,10 @@ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4985
5475
|
|
|
4986
5476
|
function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
4987
5477
|
const si = ctx.varCounter++
|
|
4988
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
4989
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
4990
|
-
const maxItems = schema.maxItems
|
|
4991
5478
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
} else if (isPrim) {
|
|
4996
|
-
inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
|
|
4997
|
-
} else {
|
|
4998
|
-
inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
|
|
4999
|
-
}
|
|
5479
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
5480
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
5481
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
5000
5482
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
5001
5483
|
}
|
|
5002
5484
|
|
|
@@ -5211,29 +5693,80 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5211
5693
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
5212
5694
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
5213
5695
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
5696
|
+
// An error literal the combined function shares between calls, frozen. Two
|
|
5697
|
+
// non-enumerable fields ride along, invisible to Object.keys, spread and
|
|
5698
|
+
// JSON: `_s` says the object is shared, so the reader copies it before handing
|
|
5699
|
+
// it out (Object.isFrozen, the check it replaces, cost more than the copy),
|
|
5700
|
+
// and `_o` is its schema ordinal, known here and otherwise looked up by path
|
|
5701
|
+
// on every read. An `_o` the literal already carries, for standalone output,
|
|
5702
|
+
// is kept. `_t` is an empty slot lib/enrich-error.js fills once.
|
|
5703
|
+
function sharedErr (lit, rootSchema) {
|
|
5704
|
+
Object.defineProperty(lit, '_s', { value: true })
|
|
5705
|
+
// A slot the enrichment fills on the first read with the fields that depend
|
|
5706
|
+
// only on this literal (code, expected, rank), so later reads skip them.
|
|
5707
|
+
Object.defineProperty(lit, '_t', { value: {} })
|
|
5708
|
+
if (lit._o === undefined) {
|
|
5709
|
+
const o = ordinalFor(rootSchema, lit.schemaPath)
|
|
5710
|
+
if (typeof o === 'number') Object.defineProperty(lit, '_o', { value: o })
|
|
5711
|
+
}
|
|
5712
|
+
return Object.freeze(lit)
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
// The push for one error in the combined generator. With `ctx.rich` (the
|
|
5716
|
+
// validator's errors are being read and enriched, see validator-core) the
|
|
5717
|
+
// error is built in its final enriched shape where it happens, by makeRich in
|
|
5718
|
+
// lib/enrich-site.js with a site record made here at compile time; otherwise
|
|
5719
|
+
// it is the plain literal `lean` the generator always emitted. `valueExpr` is
|
|
5720
|
+
// the value at `pathExpr`, which enrichment otherwise finds again by walking
|
|
5721
|
+
// the document. `paramsShared` names a frozen params object hoisted for a
|
|
5722
|
+
// static site, so a static error's params stay one shared object as before.
|
|
5723
|
+
function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
|
|
5724
|
+
if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
|
|
5725
|
+
if (!ctx.richMk) {
|
|
5726
|
+
ctx.richMk = true
|
|
5727
|
+
const { makeRich } = require('./enrich-site')
|
|
5728
|
+
ctx.closureVars.push('_mk'); ctx.closureVals.push(makeRich)
|
|
5729
|
+
}
|
|
5730
|
+
let paramsVal
|
|
5731
|
+
try { paramsVal = Function('return ' + paramsCode)() } catch { paramsVal = undefined }
|
|
5732
|
+
const fmt = keyword === 'format' && paramsVal ? paramsVal.format : undefined
|
|
5733
|
+
const { errorSite } = require('./enrich-site')
|
|
5734
|
+
const siteVar = `_S${ctx.varCounter++}`
|
|
5735
|
+
ctx.closureVars.push(siteVar)
|
|
5736
|
+
ctx.closureVals.push(errorSite(keyword, unescapeSp(sp), paramsVal, fmt))
|
|
5737
|
+
return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d))`
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5214
5740
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
5215
5741
|
const inputSchema = schema
|
|
5216
|
-
schema =
|
|
5742
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
5217
5743
|
// Same provably-local rule as the error generator above.
|
|
5218
5744
|
if (typeof schema === 'object' && schema !== null) {
|
|
5219
5745
|
const s = JSON.stringify(schema)
|
|
5220
|
-
|
|
5746
|
+
// unevaluatedProperties that is not provably local is built at run time
|
|
5747
|
+
// (genUnevaluatedDynamicC); unevaluatedItems still has to be local.
|
|
5748
|
+
if (s.includes('unevaluatedItems') && !unevalAllProvablyLocal(schema, 'items')) return null
|
|
5221
5749
|
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
5222
5750
|
if (s.includes('"$ref":"#"')) return null
|
|
5223
5751
|
// Defs on a $ref cycle compile as named functions in the boolean and
|
|
5224
5752
|
// errors generators; this generator has no preamble for them, so it
|
|
5225
5753
|
// declines and the router uses the errors generator.
|
|
5226
5754
|
if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
|
|
5227
|
-
// Bail on additionalProperties as a schema — combined path doesn't emit the
|
|
5228
|
-
// per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
|
|
5229
|
-
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
5230
5755
|
// Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
|
|
5231
5756
|
// Letting genCodeC emit the legacy "must match exactly one" placeholder
|
|
5232
5757
|
// would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
|
|
5233
5758
|
// fast path plus errFn fallback handles these schemas with collapse.
|
|
5234
|
-
|
|
5759
|
+
// Branches are collapsed into one error by the error generator's emitter
|
|
5760
|
+
// (emitBranchCollapse). Its errors are plain, so the rich form, which
|
|
5761
|
+
// builds every error enriched where it fails, declines them.
|
|
5762
|
+
// In the rich form the collapsed error and its branch errors come out
|
|
5763
|
+
// plain (the branches are the error generator's) and are enriched when
|
|
5764
|
+
// presented; every other error is built enriched. See richBuilder.
|
|
5235
5765
|
}
|
|
5236
5766
|
if (typeof schema === 'boolean') {
|
|
5767
|
+
// A `false` root has one fixed error; the rich form leaves it to enrich.
|
|
5768
|
+
if (!schema && opts && (opts.rich || opts.resultShape)) return null
|
|
5769
|
+
if (opts && opts.resultShape) return null
|
|
5237
5770
|
return schema
|
|
5238
5771
|
? () => VALID_RESULT
|
|
5239
5772
|
: () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
@@ -5292,7 +5825,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5292
5825
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
5293
5826
|
|
|
5294
5827
|
// runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
|
|
5295
|
-
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5828
|
+
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5296
5829
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
|
|
5297
5830
|
const lines = []
|
|
5298
5831
|
try {
|
|
@@ -5309,14 +5842,49 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5309
5842
|
// helper compiles a pattern through it, which the boolean generator does for
|
|
5310
5843
|
// subschemas under not/if/contains; without the binding those threw a
|
|
5311
5844
|
// ReferenceError on the first document that reached them.
|
|
5845
|
+
if (opts && opts.resultShape) {
|
|
5846
|
+
ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
|
|
5847
|
+
ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
|
|
5848
|
+
}
|
|
5312
5849
|
const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
|
|
5313
5850
|
const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
|
|
5314
5851
|
const closureParams = closureNames.join(',')
|
|
5852
|
+
// A definition the error generator wrote as a named function inside a
|
|
5853
|
+
// branch (one on a cycle, or referenced from several places) reads the
|
|
5854
|
+
// cycle-guard state its own function declares, which this one does not:
|
|
5855
|
+
// the first document to reach it threw a ReferenceError. Declined; the
|
|
5856
|
+
// error function, which has that state, answers instead.
|
|
5857
|
+
if (ctx.usesBranchCollapse && ctx.defFns && ctx.defFns.size) return null
|
|
5858
|
+
// Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
|
|
5859
|
+
// the branch functions (this function collects everything), and the
|
|
5860
|
+
// runtime shape the error generator gives its literals, without code and
|
|
5861
|
+
// docUrl. The collapsed error itself keeps its code, as everywhere.
|
|
5862
|
+
if (ctx.usesBranchCollapse) ctx.helperCode.push(require('./branch-collapse').embedSource())
|
|
5863
|
+
if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
|
|
5864
|
+
const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
|
|
5865
|
+
for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
|
|
5866
|
+
for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
|
|
5867
|
+
}
|
|
5315
5868
|
const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
5316
5869
|
// Lazy error array: no allocation for valid data (the common case)
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5870
|
+
// resultShape: validate() itself for a validator whose errors are being read
|
|
5871
|
+
// (see validator-core): the result objects built here, so one pass decides,
|
|
5872
|
+
// collects and answers, with nothing between the caller and the checks.
|
|
5873
|
+
const rs = opts && opts.resultShape
|
|
5874
|
+
// In the result shape this function is validate() itself, so a document
|
|
5875
|
+
// that makes it throw is answered by the caller's previous path instead
|
|
5876
|
+
// (`_FB`). A thrown error reaching the caller is the one thing this form
|
|
5877
|
+
// must not add; a try that does not throw costs nothing measurable.
|
|
5878
|
+
const inner = rs
|
|
5879
|
+
// With collapsed branches, collecting runs every branch of every oneOf
|
|
5880
|
+
// and anyOf in full, which an accepted document does not need: the
|
|
5881
|
+
// verdict answers it first.
|
|
5882
|
+
? `try{${ctx.usesBranchCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};const _all=true;' : ''}let _e;\n ` + lines.join('\n ') +
|
|
5883
|
+
// A collapsed branch error is presented even alone (its ordinal and
|
|
5884
|
+
// raw branch errors stay inside); otherwise only a list of two or more
|
|
5885
|
+
// needs ordering.
|
|
5886
|
+
`\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
|
|
5887
|
+
: (ctx.usesBranchCollapse ? 'const _all=true;' : '') + `let _e;\n ` + lines.join('\n ') + `\n return _e?{valid:false,errors:_e}:R`
|
|
5320
5888
|
|
|
5321
5889
|
try {
|
|
5322
5890
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
@@ -5332,6 +5900,34 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5332
5900
|
// Combined code generator: type-aware like genCode, error-collecting like genCodeE.
|
|
5333
5901
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
5334
5902
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
5903
|
+
// What of a node still applies to a value that failed its `type`, for the
|
|
5904
|
+
// combined generator: the node without `type`, without what it emits outside
|
|
5905
|
+
// the type block anyway (enum, const, anyOf, oneOf), and without the keywords
|
|
5906
|
+
// of the declared types, which no value of another type meets. Null when
|
|
5907
|
+
// nothing that validates is left.
|
|
5908
|
+
const TYPE_KEYWORDS = {
|
|
5909
|
+
object: ['properties', 'patternProperties', 'additionalProperties', 'required', 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired', 'dependentSchemas', 'dependencies', 'unevaluatedProperties'],
|
|
5910
|
+
array: ['items', 'prefixItems', 'additionalItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems'],
|
|
5911
|
+
string: ['minLength', 'maxLength', 'pattern'],
|
|
5912
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
5913
|
+
}
|
|
5914
|
+
TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
|
|
5915
|
+
const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
|
|
5916
|
+
function typeMismatchRest(schema, types) {
|
|
5917
|
+
const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
|
|
5918
|
+
for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
|
|
5919
|
+
// A non-integer number reaches the type block already (numberToo).
|
|
5920
|
+
let rest = null
|
|
5921
|
+
for (const k of Object.keys(schema)) {
|
|
5922
|
+
if (drop.has(k)) continue
|
|
5923
|
+
if (rest === null) rest = {}
|
|
5924
|
+
rest[k] = schema[k]
|
|
5925
|
+
}
|
|
5926
|
+
if (rest === null) return null
|
|
5927
|
+
for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
|
|
5928
|
+
return null
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5335
5931
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
5336
5932
|
return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
5337
5933
|
}
|
|
@@ -5342,7 +5938,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5342
5938
|
if (schema === false) {
|
|
5343
5939
|
// Leading semicolon: the previous generated line may end in an
|
|
5344
5940
|
// expression, and a line starting with `(` would be parsed as a call.
|
|
5345
|
-
lines.push(
|
|
5941
|
+
lines.push(';' + errPushC(ctx, 'not', schemaPrefix, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'}`))
|
|
5346
5942
|
return
|
|
5347
5943
|
}
|
|
5348
5944
|
if (schema === true) return
|
|
@@ -5350,6 +5946,12 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5350
5946
|
|
|
5351
5947
|
// $ref — resolve local, anchor, and cross-schema refs
|
|
5352
5948
|
if (schema.$ref) {
|
|
5949
|
+
// The reference is followed and the rest of the node is not compiled, so
|
|
5950
|
+
// a node with a keyword beside its $ref that validates (2020-12 applies
|
|
5951
|
+
// both) declines: compiled, the sibling was dropped and a document it
|
|
5952
|
+
// rejects was accepted. Only reachable since unevaluatedProperties next
|
|
5953
|
+
// to a $ref stopped declining the whole function.
|
|
5954
|
+
for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
|
|
5353
5955
|
// Self-reference "#" — no-op (permissive) to avoid infinite recursion
|
|
5354
5956
|
if (schema.$ref === '#') return
|
|
5355
5957
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
@@ -5403,7 +6005,26 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5403
6005
|
const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5404
6006
|
const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
|
|
5405
6007
|
const sp = schemaPrefix + '/' + schemaSuffix
|
|
5406
|
-
if (
|
|
6008
|
+
if (ctx.rich) {
|
|
6009
|
+
// A static site shares one frozen params object between calls, as its
|
|
6010
|
+
// hoisted literal does below; a dynamic one builds its params per call.
|
|
6011
|
+
let shared = null
|
|
6012
|
+
if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
6013
|
+
let pv
|
|
6014
|
+
try { pv = Function('return ' + paramsCode)() } catch { pv = undefined }
|
|
6015
|
+
if (pv !== undefined) {
|
|
6016
|
+
shared = `_P${ctx.varCounter++}`
|
|
6017
|
+
ctx.closureVars.push(shared); ctx.closureVals.push(Object.freeze(pv))
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
return errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, v, null, shared)
|
|
6021
|
+
}
|
|
6022
|
+
// Hoisting a static error as one shared literal keeps a function that
|
|
6023
|
+
// also answers accepted documents small. In the runtime shape this
|
|
6024
|
+
// function only runs for documents being rejected (the verdict is its own
|
|
6025
|
+
// function), and a shared literal has to be copied for the caller on
|
|
6026
|
+
// every read, so there it is written fresh.
|
|
6027
|
+
if (isStaticPath && !ctx.noOrdinal && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
5407
6028
|
// Try to evaluate paramsCode as a static constant
|
|
5408
6029
|
let paramsVal
|
|
5409
6030
|
try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
|
|
@@ -5430,7 +6051,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5430
6051
|
const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
|
|
5431
6052
|
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
5432
6053
|
if (o !== null) lit._o = o
|
|
5433
|
-
ctx.closureVals.push(
|
|
6054
|
+
ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
|
|
5434
6055
|
return `(_e||(_e=[])).push(${errVar})`
|
|
5435
6056
|
}
|
|
5436
6057
|
}
|
|
@@ -5455,7 +6076,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5455
6076
|
if (cv === null || typeof cv !== 'object') {
|
|
5456
6077
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5457
6078
|
} else {
|
|
5458
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
6079
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5459
6080
|
}
|
|
5460
6081
|
}
|
|
5461
6082
|
}
|
|
@@ -5474,7 +6095,9 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5474
6095
|
}
|
|
5475
6096
|
})
|
|
5476
6097
|
const expected = types.join(', ')
|
|
5477
|
-
|
|
6098
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
6099
|
+
// reports it.
|
|
6100
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
5478
6101
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
5479
6102
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
5480
6103
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
@@ -5487,7 +6110,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5487
6110
|
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
5488
6111
|
}
|
|
5489
6112
|
emitEnumConst()
|
|
5490
|
-
|
|
6113
|
+
// A number that is not an integer fails `integer` and is still a number:
|
|
6114
|
+
// minimum, multipleOf and the rest apply to it, and the other engines
|
|
6115
|
+
// report them next to the type error. The block opens for any number.
|
|
6116
|
+
const numberToo = types.includes('integer') && !types.includes('number')
|
|
6117
|
+
lines.push(numberToo ? `if(${typeOk}||typeof ${v}==='number'){` : `if(${typeOk}){`)
|
|
5491
6118
|
} else {
|
|
5492
6119
|
emitEnumConst()
|
|
5493
6120
|
}
|
|
@@ -5514,29 +6141,29 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5514
6141
|
}
|
|
5515
6142
|
for (const key of schema.required) {
|
|
5516
6143
|
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
|
|
5517
|
-
if (isStaticPath) {
|
|
6144
|
+
if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
|
|
5518
6145
|
const ei = ctx.varCounter++
|
|
5519
6146
|
const errVar = `_E${ei}`
|
|
5520
6147
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5521
6148
|
ctx.closureVars.push(errVar)
|
|
5522
|
-
ctx.closureVals.push(
|
|
6149
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
5523
6150
|
lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
|
|
5524
6151
|
} else {
|
|
5525
|
-
lines.push(`if(${check}){(
|
|
6152
|
+
lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
5526
6153
|
}
|
|
5527
6154
|
}
|
|
5528
6155
|
} else if (schema.required) {
|
|
5529
6156
|
for (const key of schema.required) {
|
|
5530
6157
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5531
|
-
if (isStatic) {
|
|
6158
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5532
6159
|
const ei = ctx.varCounter++
|
|
5533
6160
|
const errVar = `_E${ei}`
|
|
5534
6161
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5535
6162
|
ctx.closureVars.push(errVar)
|
|
5536
|
-
ctx.closureVals.push(
|
|
6163
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
|
|
5537
6164
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5538
6165
|
} else {
|
|
5539
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(
|
|
6166
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
|
|
5540
6167
|
}
|
|
5541
6168
|
}
|
|
5542
6169
|
}
|
|
@@ -5659,7 +6286,13 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5659
6286
|
// unevaluatedProperties: false where it is provably additionalProperties
|
|
5660
6287
|
// (the entry bail guaranteed that), keeping the keyword's own identity so
|
|
5661
6288
|
// the two engines report the same error.
|
|
5662
|
-
if (schema.unevaluatedProperties
|
|
6289
|
+
if (schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true) {
|
|
6290
|
+
if (unevalLocalOk(schema, 'unevaluatedProperties')) {
|
|
6291
|
+
if (schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6292
|
+
} else {
|
|
6293
|
+
genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
5663
6296
|
|
|
5664
6297
|
// unevaluatedItems: false under the same rule: one error for the array,
|
|
5665
6298
|
// the first uncovered position as the limit. Adjacent schema-form `items`
|
|
@@ -5671,15 +6304,15 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5671
6304
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
5672
6305
|
for (const dep of deps) {
|
|
5673
6306
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5674
|
-
if (isStatic) {
|
|
6307
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5675
6308
|
const ei = ctx.varCounter++
|
|
5676
6309
|
const errVar = `_E${ei}`
|
|
5677
6310
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5678
6311
|
ctx.closureVars.push(errVar)
|
|
5679
|
-
ctx.closureVals.push(
|
|
6312
|
+
ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
|
|
5680
6313
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5681
6314
|
} else {
|
|
5682
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(
|
|
6315
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
|
|
5683
6316
|
}
|
|
5684
6317
|
}
|
|
5685
6318
|
}
|
|
@@ -5712,6 +6345,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5712
6345
|
if (schema.patternProperties) {
|
|
5713
6346
|
if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
|
|
5714
6347
|
}
|
|
6348
|
+
// additionalProperties as a schema: each key that is neither declared nor
|
|
6349
|
+
// matched by a pattern, validated where it is (see genAdditionalSchemaC).
|
|
6350
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
6351
|
+
|
|
5715
6352
|
|
|
5716
6353
|
// dependentSchemas
|
|
5717
6354
|
if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
|
|
@@ -5741,11 +6378,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5741
6378
|
// allOf
|
|
5742
6379
|
if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
|
|
5743
6380
|
|
|
5744
|
-
// anyOf
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
if (schema.oneOf) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6381
|
+
// anyOf, oneOf. A collapsed branch applies to a value of any type, as enum
|
|
6382
|
+
// and const do: the other engines report it next to a failed `type`, so
|
|
6383
|
+
// in the combined function it goes after the type-success block below.
|
|
6384
|
+
if (schema.anyOf && !ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6385
|
+
if (schema.oneOf && !ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
5749
6386
|
|
|
5750
6387
|
// not
|
|
5751
6388
|
if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
@@ -5756,7 +6393,22 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5756
6393
|
// Close type-success block if opened
|
|
5757
6394
|
if (types) {
|
|
5758
6395
|
lines.push(`}`)
|
|
6396
|
+
// A value of another type still meets every keyword that applies to its
|
|
6397
|
+
// own type, and the applicators (allOf, not, if/then/else, $ref): the
|
|
6398
|
+
// other engines report those next to the type error, and the block above
|
|
6399
|
+
// skipped them all. The keywords of the declared types cannot apply to
|
|
6400
|
+
// it and are left out, so the else is small and does not repeat the
|
|
6401
|
+
// declared type's subtree.
|
|
6402
|
+
const rest = typeMismatchRest(schema, types)
|
|
6403
|
+
if (rest !== null) {
|
|
6404
|
+
lines.push(`else{`)
|
|
6405
|
+
genCodeC(rest, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6406
|
+
lines.push(`}`)
|
|
6407
|
+
}
|
|
5759
6408
|
}
|
|
6409
|
+
if (schema.anyOf && ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6410
|
+
if (schema.oneOf && ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6411
|
+
|
|
5760
6412
|
}
|
|
5761
6413
|
|
|
5762
6414
|
// Rarely used keyword families of genCodeCNode, kept out of it so their compile is
|
|
@@ -5782,20 +6434,158 @@ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5782
6434
|
|
|
5783
6435
|
function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
5784
6436
|
const si = ctx.varCounter++
|
|
5785
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
5786
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
5787
|
-
const maxItems = schema.maxItems
|
|
5788
6437
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
6438
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
6439
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
6440
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
6441
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
6442
|
+
}
|
|
6443
|
+
|
|
6444
|
+
// The verdict form of genUnevaluatedDynamicC: the same set, built the same
|
|
6445
|
+
// way, and the answer false at the first key outside it.
|
|
6446
|
+
function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
|
|
6447
|
+
if (schema.additionalProperties !== undefined) return
|
|
6448
|
+
const ui = ctx.varCounter++
|
|
6449
|
+
const S = `_ev${ui}`
|
|
6450
|
+
const body = [`const ${S}=new Set()`]
|
|
6451
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6452
|
+
addApplicatorAnnotations(schema, v, S, body, ctx, true)
|
|
6453
|
+
const kVar = `_uk${ui}`
|
|
6454
|
+
const up = schema.unevaluatedProperties
|
|
6455
|
+
if (up === false) {
|
|
6456
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar}))return false`)
|
|
6457
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6458
|
+
const sub = []
|
|
6459
|
+
genCode(up, `${v}[${kVar}]`, sub, ctx)
|
|
6460
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
|
|
5795
6461
|
} else {
|
|
5796
|
-
|
|
6462
|
+
throw DECLINE
|
|
5797
6463
|
}
|
|
5798
|
-
lines.push(
|
|
6464
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
// unevaluatedProperties where what is evaluated depends on which branches
|
|
6468
|
+
// pass: the set is built at run time with the interpreted engine's rules.
|
|
6469
|
+
// The node's own properties, patternProperties and additionalProperties
|
|
6470
|
+
// count whether or not their values pass; an allOf, anyOf or dependentSchemas
|
|
6471
|
+
// branch, an if, then or else, or a $ref target counts only when it passes,
|
|
6472
|
+
// a oneOf only when exactly one branch passes, and `not` never. Each branch is
|
|
6473
|
+
// asked through an annotation function (emitAnnot), which validates first and
|
|
6474
|
+
// only then adds what it evaluated, so nothing has to be rolled back.
|
|
6475
|
+
function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
6476
|
+
// additionalProperties evaluates every key the others do not, pass or fail.
|
|
6477
|
+
if (schema.additionalProperties !== undefined) return
|
|
6478
|
+
const ui = ctx.varCounter++
|
|
6479
|
+
const S = `_ev${ui}`
|
|
6480
|
+
const body = [`const ${S}=new Set()`]
|
|
6481
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6482
|
+
addApplicatorAnnotations(schema, v, S, body, ctx, true)
|
|
6483
|
+
const kVar = `_uk${ui}`
|
|
6484
|
+
const up = schema.unevaluatedProperties
|
|
6485
|
+
if (up === false) {
|
|
6486
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
|
|
6487
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6488
|
+
const pe = emitPtrEsc(ctx)
|
|
6489
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
6490
|
+
const sub = []
|
|
6491
|
+
genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
|
|
6492
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.has(${kVar})){${sub.join('\n ')}\n }`)
|
|
6493
|
+
} else {
|
|
6494
|
+
throw DECLINE
|
|
6495
|
+
}
|
|
6496
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6497
|
+
}
|
|
6498
|
+
|
|
6499
|
+
// The names a node's own keywords evaluate, added to `S`: present declared
|
|
6500
|
+
// properties, keys matching a pattern.
|
|
6501
|
+
function addLocalAnnotations(node, v, S, out, ctx) {
|
|
6502
|
+
for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.add(${JSON.stringify(k)})`)
|
|
6503
|
+
const pats = Object.keys(node.patternProperties || {})
|
|
6504
|
+
if (pats.length) {
|
|
6505
|
+
const tests = []
|
|
6506
|
+
const kv = `_pk${ctx.varCounter++}`
|
|
6507
|
+
for (const pat of pats) {
|
|
6508
|
+
const fast = fastPrefixCheck(pat, kv)
|
|
6509
|
+
if (fast) { tests.push(fast); continue }
|
|
6510
|
+
const ri = ctx.varCounter++
|
|
6511
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
6512
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
6513
|
+
tests.push(`_re${ri}.test(${kv})`)
|
|
6514
|
+
}
|
|
6515
|
+
out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.add(${kv})`)
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
// What a node's applicators add to `S`. At the node that owns the
|
|
6520
|
+
// unevaluatedProperties (`top`), a oneOf counts only with exactly one passing
|
|
6521
|
+
// branch; inside a branch that passed, its oneOf has exactly one.
|
|
6522
|
+
function addApplicatorAnnotations(node, v, S, out, ctx, top) {
|
|
6523
|
+
for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6524
|
+
for (const b of node.anyOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6525
|
+
if (Array.isArray(node.oneOf)) {
|
|
6526
|
+
if (top) {
|
|
6527
|
+
const t = `_one${ctx.varCounter++}`
|
|
6528
|
+
const calls = node.oneOf.map((b, i) => `{const _s=new Set();if(${emitAnnot(b, ctx)}(${v},_s)){${t}c++;${t}s=_s}}`).join('')
|
|
6529
|
+
out.push(`{let ${t}c=0,${t}s=null;${calls}if(${t}c===1)for(const _x of ${t}s)${S}.add(_x)}`)
|
|
6530
|
+
} else {
|
|
6531
|
+
for (const b of node.oneOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
if (node.if !== undefined) {
|
|
6535
|
+
const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
|
|
6536
|
+
const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
|
|
6537
|
+
out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
|
|
6538
|
+
}
|
|
6539
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
|
|
6540
|
+
for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
|
|
6541
|
+
}
|
|
6542
|
+
if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
|
|
6543
|
+
if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6546
|
+
function refTargetFor(ref, ctx) {
|
|
6547
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
6548
|
+
const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
|
|
6549
|
+
if (t === undefined) throw DECLINE
|
|
6550
|
+
return t
|
|
6551
|
+
}
|
|
6552
|
+
|
|
6553
|
+
// A function `(value, S) => valid` for one subschema: the subschema's verdict,
|
|
6554
|
+
// and when it passes, what it evaluated added to S. One per subschema object.
|
|
6555
|
+
function emitAnnot(sub, ctx) {
|
|
6556
|
+
if (sub === true) return '_anT'
|
|
6557
|
+
if (sub === false) return '_anF'
|
|
6558
|
+
if (sub === null || typeof sub !== 'object') throw DECLINE
|
|
6559
|
+
if (!ctx.annFns) {
|
|
6560
|
+
ctx.annFns = new Map()
|
|
6561
|
+
;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}')
|
|
6562
|
+
}
|
|
6563
|
+
const hit = ctx.annFns.get(sub)
|
|
6564
|
+
if (hit !== undefined) {
|
|
6565
|
+
if (hit === null) throw DECLINE // a reference cycle: not modelled here
|
|
6566
|
+
return hit
|
|
6567
|
+
}
|
|
6568
|
+
ctx.annFns.set(sub, null)
|
|
6569
|
+
const name = `_an${ctx.varCounter++}`
|
|
6570
|
+
const vl = []
|
|
6571
|
+
nestedGenCode(sub, '_av', vl, ctx)
|
|
6572
|
+
const body = []
|
|
6573
|
+
// additionalProperties, or an unevaluatedProperties that passed, evaluate
|
|
6574
|
+
// every remaining key.
|
|
6575
|
+
if (sub.additionalProperties !== undefined || (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== false)) {
|
|
6576
|
+
body.push(`for(const _ka in d)S.add(_ka)`)
|
|
6577
|
+
} else if (sub.unevaluatedProperties === false) {
|
|
6578
|
+
body.push(`for(const _ka in d)S.add(_ka)`)
|
|
6579
|
+
} else {
|
|
6580
|
+
addLocalAnnotations(sub, 'd', 'S', body, ctx)
|
|
6581
|
+
addApplicatorAnnotations(sub, 'd', 'S', body, ctx, false)
|
|
6582
|
+
}
|
|
6583
|
+
// The verdict generator keeps its declarations in the preamble (its
|
|
6584
|
+
// helperCode is only scanned for patterns); the error generators in
|
|
6585
|
+
// helperCode.
|
|
6586
|
+
;(ctx.preamble || ctx.helperCode).push(`function ${name}_v(_av){${vl.join('\n ')}\n return true}function ${name}(d,S){if(!${name}_v(d))return false;if(typeof d!=='object'||d===null||Array.isArray(d))return true;${body.join(';')};return true}`)
|
|
6587
|
+
ctx.annFns.set(sub, name)
|
|
6588
|
+
return name
|
|
5799
6589
|
}
|
|
5800
6590
|
|
|
5801
6591
|
function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
@@ -5825,6 +6615,41 @@ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix
|
|
|
5825
6615
|
}
|
|
5826
6616
|
}
|
|
5827
6617
|
|
|
6618
|
+
// The error generator's per-key loop (genAdditionalSchemaE), in the combined
|
|
6619
|
+
// generator: the subschema compiled by genCodeC at each additional key, with
|
|
6620
|
+
// that key's path. Patterns go through the same matchers patternProperties
|
|
6621
|
+
// uses here.
|
|
6622
|
+
function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
|
|
6623
|
+
const ki = ctx.varCounter++
|
|
6624
|
+
const kVar = `_ak${ki}`
|
|
6625
|
+
const known = Object.keys(schema.properties || {})
|
|
6626
|
+
const conds = []
|
|
6627
|
+
if (known.length) {
|
|
6628
|
+
const set = `_akn${ki}`
|
|
6629
|
+
ctx.closureVars.push(set)
|
|
6630
|
+
ctx.closureVals.push(new Set(known))
|
|
6631
|
+
conds.push(`!${set}.has(${kVar})`)
|
|
6632
|
+
}
|
|
6633
|
+
const pats = []
|
|
6634
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
6635
|
+
const fast = fastPrefixCheck(pat, kVar)
|
|
6636
|
+
if (fast) { pats.push(fast); continue }
|
|
6637
|
+
const ri = ctx.varCounter++
|
|
6638
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
6639
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
6640
|
+
pats.push(`_re${ri}.test(${kVar})`)
|
|
6641
|
+
}
|
|
6642
|
+
if (pats.length) conds.push(`!(${pats.join('||')})`)
|
|
6643
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
6644
|
+
const keep = conds.length ? `if(${conds.join('&&')})` : ''
|
|
6645
|
+
const pe = emitPtrEsc(ctx)
|
|
6646
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
6647
|
+
const sub = []
|
|
6648
|
+
genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
|
|
6649
|
+
if (sub.length === 0) return
|
|
6650
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
|
|
6651
|
+
}
|
|
6652
|
+
|
|
5828
6653
|
function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
5829
6654
|
let handledPropertyNames = false
|
|
5830
6655
|
const ppEntries = Object.entries(schema.patternProperties)
|
|
@@ -5943,7 +6768,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5943
6768
|
function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
5944
6769
|
const ki = ctx.varCounter++
|
|
5945
6770
|
const p = pathExpr || '""'
|
|
5946
|
-
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(
|
|
6771
|
+
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
|
|
5947
6772
|
}
|
|
5948
6773
|
|
|
5949
6774
|
function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
@@ -5997,12 +6822,18 @@ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
|
5997
6822
|
}
|
|
5998
6823
|
|
|
5999
6824
|
function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
6825
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
6826
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
6827
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6000
6828
|
const fi = ctx.varCounter++
|
|
6001
6829
|
const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
|
|
6002
6830
|
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
|
|
6003
6831
|
}
|
|
6004
6832
|
|
|
6005
6833
|
function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
6834
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
6835
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
6836
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6006
6837
|
const fi = ctx.varCounter++
|
|
6007
6838
|
const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
|
|
6008
6839
|
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
|
|
@@ -6162,3 +6993,5 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
6162
6993
|
}
|
|
6163
6994
|
|
|
6164
6995
|
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES, _setCodeBudget }
|
|
6996
|
+
// For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
|
|
6997
|
+
Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
|