ata-validator 1.18.1 → 1.20.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/index.js +20 -4
- package/lib/aot-impl.js +116 -21
- package/lib/branch-collapse.js +72 -47
- package/lib/interpreter.js +43 -14
- package/lib/js-compiler.js +114 -43
- package/lib/output-format.js +140 -0
- package/lib/plan-compiler.js +66 -16
- package/lib/tier0.js +14 -4
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -25,6 +25,12 @@ const UQ_HELPERS = "function _cn(x){if(x===null||typeof x!=='object')return type
|
|
|
25
25
|
function hoistOnce(ctx, key, code) {
|
|
26
26
|
if (ctx[key]) return
|
|
27
27
|
ctx[key] = true
|
|
28
|
+
// Recorded as shared: the text is a constant and the name it declares is
|
|
29
|
+
// fixed, so several compiled schemas in one bundle can use one copy. The
|
|
30
|
+
// rest of the preamble cannot move, because emitConstant and the generated
|
|
31
|
+
// branch checks name things per compilation (_kv0, _av3) and two schemas
|
|
32
|
+
// would collide.
|
|
33
|
+
if (ctx.shared) ctx.shared.push(code)
|
|
28
34
|
if (ctx.preamble) ctx.preamble.push(code)
|
|
29
35
|
else if (ctx.helperCode) ctx.helperCode.push(code)
|
|
30
36
|
}
|
|
@@ -34,6 +40,18 @@ function emitDeq(ctx) {
|
|
|
34
40
|
return '_deq'
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
// RFC 6901: a property key reaching an instancePath has to have `~` and `/`
|
|
44
|
+
// escaped, or the pointer splits into the wrong segments. Static keys are
|
|
45
|
+
// escaped at compile time with ptrSeg; a key only known at run time needs
|
|
46
|
+
// this. It mirrors interpreter.js escapePointer, including skipping the two
|
|
47
|
+
// regex passes for the keys that need nothing, which is almost all of them.
|
|
48
|
+
const PTR_ESC_HELPER = "function _pe(s){for(let i=0;i<s.length;i++){const c=s.charCodeAt(i);if(c===126||c===47)return s.replace(/~/g,'~0').replace(/\\//g,'~1')}return s}"
|
|
49
|
+
|
|
50
|
+
function emitPtrEsc(ctx) {
|
|
51
|
+
hoistOnce(ctx, '_peHoisted', PTR_ESC_HELPER)
|
|
52
|
+
return '_pe'
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
// `_uq` compares items with `_deq`, so it can never be hoisted alone.
|
|
38
56
|
function emitUq(ctx) {
|
|
39
57
|
emitDeq(ctx)
|
|
@@ -404,29 +422,33 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
404
422
|
})
|
|
405
423
|
}
|
|
406
424
|
|
|
407
|
-
// numeric
|
|
425
|
+
// numeric. A non-finite number carries no type here, so an empty schema
|
|
426
|
+
// accepts it and these must let it through for the same reason: otherwise
|
|
427
|
+
// `{ minimum: 3 }` turns away a value `{}` admits, and the two engines
|
|
428
|
+
// answer differently for it.
|
|
408
429
|
if (schema.minimum !== undefined) {
|
|
409
430
|
const min = schema.minimum
|
|
410
|
-
checks.push((d) => typeof d !== 'number' || d >= min)
|
|
431
|
+
checks.push((d) => typeof d !== 'number' || !isFinite(d) || d >= min)
|
|
411
432
|
}
|
|
412
433
|
if (schema.maximum !== undefined) {
|
|
413
434
|
const max = schema.maximum
|
|
414
|
-
checks.push((d) => typeof d !== 'number' || d <= max)
|
|
435
|
+
checks.push((d) => typeof d !== 'number' || !isFinite(d) || d <= max)
|
|
415
436
|
}
|
|
416
437
|
if (schema.exclusiveMinimum !== undefined) {
|
|
417
438
|
const min = schema.exclusiveMinimum
|
|
418
|
-
checks.push((d) => typeof d !== 'number' || d > min)
|
|
439
|
+
checks.push((d) => typeof d !== 'number' || !isFinite(d) || d > min)
|
|
419
440
|
}
|
|
420
441
|
if (schema.exclusiveMaximum !== undefined) {
|
|
421
442
|
const max = schema.exclusiveMaximum
|
|
422
|
-
checks.push((d) => typeof d !== 'number' || d < max)
|
|
443
|
+
checks.push((d) => typeof d !== 'number' || !isFinite(d) || d < max)
|
|
423
444
|
}
|
|
424
445
|
if (schema.multipleOf !== undefined) {
|
|
425
446
|
const div = schema.multipleOf
|
|
426
447
|
checks.push((d) => {
|
|
427
|
-
if (typeof d !== 'number') return true
|
|
428
|
-
|
|
429
|
-
|
|
448
|
+
if (typeof d !== 'number' || !isFinite(d)) return true
|
|
449
|
+
if (div === 0) return false
|
|
450
|
+
const q = d / div
|
|
451
|
+
return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9
|
|
430
452
|
})
|
|
431
453
|
}
|
|
432
454
|
|
|
@@ -750,6 +772,21 @@ function buildTypeCheck(types) {
|
|
|
750
772
|
}
|
|
751
773
|
}
|
|
752
774
|
|
|
775
|
+
// `multipleOf` as the interpreter reads it: divide and ask whether the
|
|
776
|
+
// quotient is a whole number. The remainder form this replaces answered -1 %
|
|
777
|
+
// 0.1 with -0.09999999999999995, which is neither near zero nor near the
|
|
778
|
+
// divisor, so it called -1 not a multiple of 0.1 when -1 / 0.1 is exactly -10.
|
|
779
|
+
// Emitted from one place because the expression stood in six.
|
|
780
|
+
function multipleOfBad(v, m) {
|
|
781
|
+
if (m === 0) return 'true'
|
|
782
|
+
// Negated rather than written as "the distance is too large": a quotient
|
|
783
|
+
// that overflows to Infinity makes the distance NaN, and every comparison
|
|
784
|
+
// with NaN is false, so the direct form called 1e308 a multiple of
|
|
785
|
+
// 0.123456789. Asking whether it is good and negating keeps that case a
|
|
786
|
+
// rejection, which is what the suite wants and what the closure form does.
|
|
787
|
+
return `!(Number.isInteger(${v}/${m})||Math.abs(${v}/${m}-Math.round(${v}/${m}))<1e-9)`
|
|
788
|
+
}
|
|
789
|
+
|
|
753
790
|
const TYPE_CHECKS = {
|
|
754
791
|
string: (d) => typeof d === 'string',
|
|
755
792
|
number: (d) => typeof d === 'number' && isFinite(d),
|
|
@@ -768,12 +805,30 @@ function _canonical(x) {
|
|
|
768
805
|
}
|
|
769
806
|
|
|
770
807
|
const _formats = require('./formats')
|
|
808
|
+
// The closure path's formats. It carried its own idea of an email and knew
|
|
809
|
+
// five formats in all, so a schema that reached it was told "valid" for
|
|
810
|
+
// anything the other engines turned away; the first call on a fresh validator
|
|
811
|
+
// is the one that lands here. Every entry now names the same function the
|
|
812
|
+
// other two paths use, and the list is the list they implement.
|
|
771
813
|
const FORMAT_CHECKS = {
|
|
772
|
-
email:
|
|
814
|
+
email: _formats.email,
|
|
815
|
+
'idn-email': _formats.idnEmail,
|
|
773
816
|
date: _formats.date,
|
|
817
|
+
'date-time': _formats.dateTime,
|
|
818
|
+
time: _formats.time,
|
|
819
|
+
duration: _formats.duration,
|
|
774
820
|
uuid: _formats.uuid,
|
|
821
|
+
uri: _formats.uri,
|
|
822
|
+
'uri-reference': _formats.uriReference,
|
|
823
|
+
'uri-template': _formats.uriTemplate,
|
|
824
|
+
iri: _formats.iri,
|
|
825
|
+
'iri-reference': _formats.iriReference,
|
|
775
826
|
ipv4: _formats.ipv4,
|
|
827
|
+
ipv6: _formats.ipv6,
|
|
776
828
|
hostname: _formats.hostname,
|
|
829
|
+
'json-pointer': _formats.jsonPointer,
|
|
830
|
+
'relative-json-pointer': _formats.relativeJsonPointer,
|
|
831
|
+
regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
|
|
777
832
|
}
|
|
778
833
|
|
|
779
834
|
// Dangerous JS property names that exist on Object.prototype
|
|
@@ -1437,7 +1492,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1437
1492
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
1438
1493
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
1439
1494
|
|
|
1440
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1495
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1441
1496
|
const lines = []
|
|
1442
1497
|
genCode(schema, 'd', lines, ctx)
|
|
1443
1498
|
|
|
@@ -1519,6 +1574,9 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1519
1574
|
const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
1520
1575
|
boolFn._source = helperStr + body
|
|
1521
1576
|
boolFn._preambleSource = preambleStr
|
|
1577
|
+
boolFn._preambleGuard = guardStr
|
|
1578
|
+
boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
|
|
1579
|
+
boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
1522
1580
|
boolFn._hybridSource = helperStr + hybridBody
|
|
1523
1581
|
boolFn._usesSafeRe = !!ctx.usesSafeRe
|
|
1524
1582
|
// Custom-format closure entries that the bundle output needs to recreate.
|
|
@@ -1685,7 +1743,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1685
1743
|
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
1686
1744
|
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
1687
1745
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
1688
|
-
if (schema.multipleOf !== undefined) conds.push(`(
|
|
1746
|
+
if (schema.multipleOf !== undefined) conds.push(`(${multipleOfBad('_v', schema.multipleOf)})`)
|
|
1689
1747
|
if (conds.length < 2) return null
|
|
1690
1748
|
return bind(conds)
|
|
1691
1749
|
}
|
|
@@ -1696,7 +1754,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1696
1754
|
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
1697
1755
|
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
1698
1756
|
if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
|
|
1699
|
-
if (schema.multipleOf !== undefined) conds.push(`(
|
|
1757
|
+
if (schema.multipleOf !== undefined) conds.push(`(${multipleOfBad('_v', schema.multipleOf)})`)
|
|
1700
1758
|
if (conds.length < 2) return null
|
|
1701
1759
|
return bind(conds)
|
|
1702
1760
|
}
|
|
@@ -1956,14 +2014,19 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1956
2014
|
}
|
|
1957
2015
|
|
|
1958
2016
|
// numeric — skip type guard if known numeric
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2017
|
+
// Without `type: number` beside it, a numeric keyword has to let a
|
|
2018
|
+
// non-finite value through: it carries no type, an empty schema accepts it,
|
|
2019
|
+
// and the interpreted engine reads it the same way. With `type: number` the
|
|
2020
|
+
// type check has already turned it away, so the guard is only needed here.
|
|
2021
|
+
const numGuard = isNum ? '' : `typeof ${v}==='number'&&isFinite(${v})&&`
|
|
2022
|
+
if (schema.minimum !== undefined) lines.push(`if(${numGuard}${v}<${schema.minimum})return false`)
|
|
2023
|
+
if (schema.maximum !== undefined) lines.push(`if(${numGuard}${v}>${schema.maximum})return false`)
|
|
2024
|
+
if (schema.exclusiveMinimum !== undefined) lines.push(`if(${numGuard}${v}<=${schema.exclusiveMinimum})return false`)
|
|
2025
|
+
if (schema.exclusiveMaximum !== undefined) lines.push(`if(${numGuard}${v}>=${schema.exclusiveMaximum})return false`)
|
|
1963
2026
|
if (schema.multipleOf !== undefined) {
|
|
1964
2027
|
const m = schema.multipleOf
|
|
1965
|
-
const bad = `(
|
|
1966
|
-
lines.push(
|
|
2028
|
+
const bad = `(${multipleOfBad(v, m)})`
|
|
2029
|
+
lines.push(`if(${numGuard}${bad})return false`)
|
|
1967
2030
|
}
|
|
1968
2031
|
|
|
1969
2032
|
// string length — skip type guard if known string.
|
|
@@ -3202,7 +3265,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3202
3265
|
if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
|
|
3203
3266
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
3204
3267
|
|
|
3205
|
-
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
|
|
3268
|
+
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null,
|
|
3206
3269
|
// Custom format checkers referenced by the body, bound as
|
|
3207
3270
|
// closure parameters below the way the other entry points do.
|
|
3208
3271
|
closureVars: [], closureVals: [],
|
|
@@ -3222,7 +3285,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3222
3285
|
// AOT output small for schemas that don't compose. Standalone modules can't
|
|
3223
3286
|
// require('./branch-collapse'), so the helper body is embedded verbatim.
|
|
3224
3287
|
if (ctx.usesBranchCollapse) {
|
|
3225
|
-
|
|
3288
|
+
ctx.helperCode.push(require('./branch-collapse').embedSource())
|
|
3226
3289
|
}
|
|
3227
3290
|
if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
|
|
3228
3291
|
|
|
@@ -3272,6 +3335,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3272
3335
|
fn = new Function('d', '_all', body)
|
|
3273
3336
|
}
|
|
3274
3337
|
fn._errSource = body
|
|
3338
|
+
fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
3275
3339
|
fn._usesSafeRe = !!ctx.usesSafeRe
|
|
3276
3340
|
if (cvars.length) {
|
|
3277
3341
|
fn._formatClosures = cvars.map((name, i) => {
|
|
@@ -3448,10 +3512,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3448
3512
|
}
|
|
3449
3513
|
})
|
|
3450
3514
|
const expected = types.join(', ')
|
|
3515
|
+
const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
|
|
3451
3516
|
{
|
|
3452
3517
|
const typeSp = `${schemaPrefix}/type`
|
|
3453
3518
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
3454
|
-
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type
|
|
3519
|
+
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
3455
3520
|
}
|
|
3456
3521
|
}
|
|
3457
3522
|
|
|
@@ -3514,7 +3579,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3514
3579
|
const m = schema.multipleOf
|
|
3515
3580
|
const ci = ctx.varCounter++
|
|
3516
3581
|
// Use tolerance-based check for floating point (matches C++ behavior)
|
|
3517
|
-
lines.push(`{
|
|
3582
|
+
lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
|
|
3518
3583
|
}
|
|
3519
3584
|
|
|
3520
3585
|
// string length — same s.length fast paths as the boolean codegen above.
|
|
@@ -3661,9 +3726,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3661
3726
|
// properties — always guard (error mode, data may not be an object or may be array)
|
|
3662
3727
|
if (schema.properties) {
|
|
3663
3728
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3664
|
-
const childPath = childPathExpr(pathExpr,
|
|
3729
|
+
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
3665
3730
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
3666
|
-
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
3731
|
+
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
3667
3732
|
lines.push(`}`)
|
|
3668
3733
|
}
|
|
3669
3734
|
}
|
|
@@ -3685,7 +3750,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3685
3750
|
const ri = ctx.regExpMap.get(pattern);
|
|
3686
3751
|
const ki = ctx.varCounter++
|
|
3687
3752
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
|
|
3688
|
-
const
|
|
3753
|
+
const _peFn = emitPtrEsc(ctx)
|
|
3754
|
+
const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
|
|
3689
3755
|
// The real pointer, so source maps can locate the failing keyword.
|
|
3690
3756
|
genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + pat.replace(/~/g, '~0').replace(/\//g, '~1'))
|
|
3691
3757
|
lines.push(`}}}`)
|
|
@@ -3721,7 +3787,8 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3721
3787
|
if (patTests.length) conds.push(`!(${patTests.join('||')})`)
|
|
3722
3788
|
const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
|
|
3723
3789
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
|
|
3724
|
-
const
|
|
3790
|
+
const _peFn = emitPtrEsc(ctx)
|
|
3791
|
+
const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
|
|
3725
3792
|
genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
|
|
3726
3793
|
lines.push(`}}}`)
|
|
3727
3794
|
}
|
|
@@ -3730,7 +3797,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3730
3797
|
if (schema.dependentSchemas) {
|
|
3731
3798
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
3732
3799
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
3733
|
-
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
3800
|
+
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
3734
3801
|
lines.push(`}`)
|
|
3735
3802
|
}
|
|
3736
3803
|
}
|
|
@@ -3739,7 +3806,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3739
3806
|
// interpreter does (plan-compiler: instancePath + '/' + key).
|
|
3740
3807
|
if (schema.propertyNames === false) {
|
|
3741
3808
|
const ki = ctx.varCounter++
|
|
3742
|
-
|
|
3809
|
+
// The subschema is applied to the property NAME, so the failure belongs to
|
|
3810
|
+
// the object that carries the name, not to `/<key>`, which is where the
|
|
3811
|
+
// value lives. Same placement as the object-valued propertyNames branch.
|
|
3812
|
+
const p = pathExpr || '""'
|
|
3743
3813
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
|
|
3744
3814
|
}
|
|
3745
3815
|
|
|
@@ -3811,9 +3881,9 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3811
3881
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
3812
3882
|
const maxC = schema.maxContains
|
|
3813
3883
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
3814
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{
|
|
3884
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
3815
3885
|
if (maxC !== undefined) {
|
|
3816
|
-
lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{
|
|
3886
|
+
lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
3817
3887
|
}
|
|
3818
3888
|
lines.push(`}`)
|
|
3819
3889
|
}
|
|
@@ -3949,7 +4019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
3949
4019
|
if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
|
|
3950
4020
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
3951
4021
|
|
|
3952
|
-
const ctx = { varCounter: 0, helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
4022
|
+
const ctx = { varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
3953
4023
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema, userFormats: userFormats || null }
|
|
3954
4024
|
const lines = []
|
|
3955
4025
|
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
@@ -4095,10 +4165,11 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4095
4165
|
}
|
|
4096
4166
|
})
|
|
4097
4167
|
const expected = types.join(', ')
|
|
4168
|
+
const expectedParam = types.length === 1 ? `'${types[0]}'` : JSON.stringify(types)
|
|
4098
4169
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
4099
4170
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
4100
4171
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
4101
|
-
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type
|
|
4172
|
+
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
|
|
4102
4173
|
// Subsequent optimized code runs inside if(typeOk){...}
|
|
4103
4174
|
if (types.length === 1) {
|
|
4104
4175
|
isObj = types[0] === 'object'
|
|
@@ -4174,7 +4245,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4174
4245
|
if (schema.multipleOf !== undefined) {
|
|
4175
4246
|
const m = schema.multipleOf
|
|
4176
4247
|
const ci = ctx.varCounter++
|
|
4177
|
-
lines.push(`{
|
|
4248
|
+
lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
|
|
4178
4249
|
}
|
|
4179
4250
|
|
|
4180
4251
|
// string length — s.length fast paths, _cpLen only in the uncertain band.
|
|
@@ -4293,19 +4364,19 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4293
4364
|
if (schema.properties) {
|
|
4294
4365
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
4295
4366
|
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
4296
|
-
const childPath = childPathExpr(pathExpr,
|
|
4367
|
+
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
4297
4368
|
if (requiredSet.has(key) && isObj) {
|
|
4298
4369
|
lines.push(`if(${pv}!==undefined){`)
|
|
4299
|
-
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4370
|
+
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4300
4371
|
lines.push(`}`)
|
|
4301
4372
|
} else if (isObj) {
|
|
4302
4373
|
const oi = ctx.varCounter++
|
|
4303
4374
|
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
|
|
4304
|
-
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4375
|
+
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4305
4376
|
lines.push(`}}`)
|
|
4306
4377
|
} else {
|
|
4307
4378
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
4308
|
-
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+key)
|
|
4379
|
+
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4309
4380
|
lines.push(`}`)
|
|
4310
4381
|
}
|
|
4311
4382
|
}
|
|
@@ -4378,7 +4449,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4378
4449
|
// The subschema is generated in place so its own keywords report
|
|
4379
4450
|
// the real instance path and schema pointer.
|
|
4380
4451
|
lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4381
|
-
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4452
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4382
4453
|
lines.push(`}}`)
|
|
4383
4454
|
}
|
|
4384
4455
|
lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
|
|
@@ -4410,7 +4481,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4410
4481
|
}
|
|
4411
4482
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
4412
4483
|
lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
|
|
4413
|
-
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, kVar), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4484
|
+
genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
|
|
4414
4485
|
lines.push(`}`)
|
|
4415
4486
|
}
|
|
4416
4487
|
lines.push(`}}`)
|
|
@@ -4421,14 +4492,14 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4421
4492
|
if (schema.dependentSchemas) {
|
|
4422
4493
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
4423
4494
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${JSON.stringify(key)} in ${v}){`)
|
|
4424
|
-
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+key)
|
|
4495
|
+
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
4425
4496
|
lines.push(`}`)
|
|
4426
4497
|
}
|
|
4427
4498
|
}
|
|
4428
4499
|
|
|
4429
4500
|
if (schema.propertyNames === false) {
|
|
4430
4501
|
const ki = ctx.varCounter++
|
|
4431
|
-
const p = pathExpr
|
|
4502
|
+
const p = pathExpr || '""'
|
|
4432
4503
|
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'})}}`)
|
|
4433
4504
|
}
|
|
4434
4505
|
|
|
@@ -4491,8 +4562,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4491
4562
|
const minC = schema.minContains !== undefined ? schema.minContains : 1
|
|
4492
4563
|
const maxC = schema.maxContains
|
|
4493
4564
|
lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
|
|
4494
|
-
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{
|
|
4495
|
-
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{
|
|
4565
|
+
lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
|
|
4566
|
+
if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
|
|
4496
4567
|
lines.push(`}`)
|
|
4497
4568
|
}
|
|
4498
4569
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The standardised output format from draft 2019-09 and 2020-12.
|
|
4
|
+
//
|
|
5
|
+
// ata reports errors in its own shape, the one consumers and the compat entry
|
|
6
|
+
// expect. The specification also defines an output format of its own, with
|
|
7
|
+
// `keywordLocation`, `absoluteKeywordLocation` and `instanceLocation`, and the
|
|
8
|
+
// official test suite has a small set of tests for it under `output-tests/`.
|
|
9
|
+
// This produces that shape from a validation, so those tests can be run and so
|
|
10
|
+
// a caller that wants spec output can have it.
|
|
11
|
+
//
|
|
12
|
+
// `flag` and `basic` are implemented. `detailed` and `verbose` are not: they
|
|
13
|
+
// need the full applicator tree, including the subschemas that passed, which
|
|
14
|
+
// the engines do not keep. The v1 `list` format is not implemented either; it
|
|
15
|
+
// is still moving, and the suite's own v1 output schema does not yet define it.
|
|
16
|
+
//
|
|
17
|
+
// Annotations are collected only when validation succeeded. That is not a
|
|
18
|
+
// shortcut: a subschema that fails contributes no annotations, and the output
|
|
19
|
+
// is rooted at the root schema, so a failing root drops all of them.
|
|
20
|
+
|
|
21
|
+
// Keywords whose value is the annotation they produce.
|
|
22
|
+
const METADATA_KEYWORDS = [
|
|
23
|
+
'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly',
|
|
24
|
+
'examples', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function pointerSegment (s) {
|
|
28
|
+
return String(s).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The root $id, which absoluteKeywordLocation is resolved against.
|
|
32
|
+
function baseUri (schema) {
|
|
33
|
+
if (schema && typeof schema === 'object' && typeof schema.$id === 'string') {
|
|
34
|
+
return schema.$id.split('#')[0];
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unitFor (error, base) {
|
|
40
|
+
const keywordLocation = typeof error.schemaPath === 'string' && error.schemaPath.charAt(0) === '#'
|
|
41
|
+
? error.schemaPath.slice(1)
|
|
42
|
+
: (error.schemaPath || '');
|
|
43
|
+
const unit = {
|
|
44
|
+
valid: false,
|
|
45
|
+
keywordLocation,
|
|
46
|
+
instanceLocation: error.instancePath || '',
|
|
47
|
+
};
|
|
48
|
+
if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation;
|
|
49
|
+
if (typeof error.message === 'string') unit.error = error.message;
|
|
50
|
+
return unit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Walk the schema beside the instance, collecting metadata annotations from
|
|
54
|
+
// the subschemas that applied. Only reached when the whole validation passed,
|
|
55
|
+
// so every applicator followed here is one that succeeded, except the
|
|
56
|
+
// alternatives of anyOf/oneOf/if, which are checked before being followed.
|
|
57
|
+
function collectAnnotations (schema, data, keywordLocation, instanceLocation, base, out, isValid, depth) {
|
|
58
|
+
if (schema === true || schema === false || schema === null || typeof schema !== 'object' || depth > 64) return;
|
|
59
|
+
|
|
60
|
+
for (const kw of METADATA_KEYWORDS) {
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(schema, kw)) {
|
|
62
|
+
const unit = {
|
|
63
|
+
valid: true,
|
|
64
|
+
keywordLocation: keywordLocation + '/' + kw,
|
|
65
|
+
instanceLocation,
|
|
66
|
+
annotation: schema[kw],
|
|
67
|
+
};
|
|
68
|
+
if (base) unit.absoluteKeywordLocation = base + '#' + keywordLocation + '/' + kw;
|
|
69
|
+
out.push(unit);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const down = (sub, kwSeg, value, instSeg) => collectAnnotations(
|
|
74
|
+
sub, value, keywordLocation + kwSeg, instanceLocation + (instSeg || ''), base, out, isValid, depth + 1,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (schema.properties && data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
78
|
+
for (const key of Object.keys(schema.properties)) {
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
80
|
+
down(schema.properties[key], '/properties/' + pointerSegment(key), data[key], '/' + pointerSegment(key));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(schema.prefixItems) && Array.isArray(data)) {
|
|
85
|
+
for (let i = 0; i < schema.prefixItems.length && i < data.length; i++) {
|
|
86
|
+
down(schema.prefixItems[i], '/prefixItems/' + i, data[i], '/' + i);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (schema.items && typeof schema.items === 'object' && Array.isArray(data)) {
|
|
90
|
+
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0;
|
|
91
|
+
for (let i = start; i < data.length; i++) down(schema.items, '/items', data[i], '/' + i);
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(schema.allOf)) {
|
|
94
|
+
for (let i = 0; i < schema.allOf.length; i++) down(schema.allOf[i], '/allOf/' + i, data, '');
|
|
95
|
+
}
|
|
96
|
+
for (const kw of ['anyOf', 'oneOf']) {
|
|
97
|
+
if (Array.isArray(schema[kw])) {
|
|
98
|
+
for (let i = 0; i < schema[kw].length; i++) {
|
|
99
|
+
if (isValid(schema[kw][i], data)) down(schema[kw][i], '/' + kw + '/' + i, data, '');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (schema.if !== undefined) {
|
|
104
|
+
const taken = isValid(schema.if, data);
|
|
105
|
+
if (taken && schema.then !== undefined) down(schema.then, '/then', data, '');
|
|
106
|
+
if (!taken && schema.else !== undefined) down(schema.else, '/else', data, '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Produce spec output for `data` under `validator`.
|
|
111
|
+
// format: 'flag' | 'basic' (default 'basic')
|
|
112
|
+
function toOutput (validator, data, opts) {
|
|
113
|
+
const format = (opts && opts.format) || 'basic';
|
|
114
|
+
const result = validator.validate(data);
|
|
115
|
+
if (format === 'flag') return { valid: result.valid };
|
|
116
|
+
if (format !== 'basic') {
|
|
117
|
+
throw new Error(`toOutput: unsupported format "${format}". Supported: "flag", "basic".`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const schema = validator._schemaObj !== undefined ? validator._schemaObj : validator._rawSchema;
|
|
121
|
+
const base = baseUri(schema);
|
|
122
|
+
const out = { valid: result.valid };
|
|
123
|
+
|
|
124
|
+
if (!result.valid) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
for (const e of result.errors) errors.push(unitFor(e, base));
|
|
127
|
+
out.errors = errors;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const annotations = [];
|
|
132
|
+
const isValid = (sub, value) => {
|
|
133
|
+
try { return new validator.constructor(sub).isValidObject(value); } catch { return false; }
|
|
134
|
+
};
|
|
135
|
+
collectAnnotations(schema, data, '', '', base, annotations, isValid, 0);
|
|
136
|
+
if (annotations.length) out.annotations = annotations;
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { toOutput, METADATA_KEYWORDS };
|