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.
@@ -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
- const r = d % div
429
- return Math.abs(r) <= 1e-8 || Math.abs(r - div) <= 1e-8
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: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
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(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
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(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
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
- if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
1960
- if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
1961
- if (schema.exclusiveMinimum !== undefined) lines.push(isNum ? `if(${v}<=${schema.exclusiveMinimum})return false` : `if(typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum})return false`)
1962
- if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
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 = `(Math.abs(${v}%${m})>1e-8&&Math.abs(${v}%${m}-${m})>1e-8)`
1966
- lines.push(isNum ? `if${bad}return false` : `if(typeof ${v}==='number'&&${bad})return false`)
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
- ctx.helperCode.push("const __ATA_SEVERITY={type:10,const:8,enum:8,required:5,format:3,minLength:3,maxLength:3,minimum:3,maximum:3,pattern:3,additionalProperties:2,unevaluatedProperties:2,unevaluatedItems:2};function __ataScore(errs){if(!errs||!errs.length)return 0;let s=0;for(const e of errs)s+=__ATA_SEVERITY[e.keyword]||4;return errs.length*100+s}function __ataCollapse(kw,br,pp,sp,o){const pass=br.filter(b=>b.valid);if(kw==='oneOf'){if(pass.length===1)return null;if(pass.length>1)return{code:'ATA4002',keyword:'oneOf',instancePath:pp||'',path:pp||'',schemaPath:sp,_o:o,message:'value matched '+pass.length+' of '+br.length+' oneOf variants, expected exactly one',params:{matched:pass.length,total:br.length}}}else{if(pass.length>=1)return null}let bi=0,bs=Infinity;for(let i=0;i<br.length;i++){const s=__ataScore(br[i].errors);if(s<bs){bs=s;bi=i}}const code=kw==='oneOf'?'ATA4001':'ATA4003';const best=br[bi];return{code,keyword:kw,instancePath:pp||'',path:pp||'',schemaPath:sp,_o:o,message:'value matched 0 of '+br.length+' '+kw+' variants',params:{variants:br.length,closest:bi,closestName:best.title||('variant '+(bi+1))},branchErrors:best.errors}}")
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:'${expected}'},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
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(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
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, esc(key))
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 p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
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 p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
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
- const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
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', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
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', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
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:'${expected}'}`, `'must be ${expected}'`)}}`)
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(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
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, esc(key))
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 ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
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', `{limit:${minC}}`, `'contains: need at least ${minC} match(es)'`)}}`)
4495
- if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{limit:${maxC}}`, `'contains: at most ${maxC} match(es)'`)}}`)
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 };