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.
@@ -1,4 +1,9 @@
1
1
  'use strict';
2
+ const { collapseBranches } = require('./branch-collapse');
3
+
4
+ // One branch title, for the collapsed anyOf/oneOf error. The compiled node
5
+ // keeps the schema it came from, which is where a `title` lives.
6
+ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string' ? n.schema.title : '');
2
7
 
3
8
  // Compiles interpreter Plans into a tree of specialized closures. No source
4
9
  // generation, no `new Function`: this is plain closure composition, so it
@@ -173,7 +178,11 @@ function install(deps) {
173
178
  if (P.exclusiveMaximum !== undefined) { const m = P.exclusiveMaximum; nums.push((d) => d < m); }
174
179
  if (P.multipleOf !== undefined) { const m = P.multipleOf; nums.push((d) => multipleOfOk(d, m)); }
175
180
  const run = combineValue(nums);
176
- if (run !== null) checks.push((data) => typeof data !== 'number' || run(data));
181
+ // A non-finite number is not of type `number` here: dataBits() gives it
182
+ // no type bit, and an empty schema accepts it. So the numeric keywords
183
+ // must not apply to it either, or `{ minimum: 3 }` rejects a value that
184
+ // `{}` accepts and the compiled engine admits.
185
+ if (run !== null) checks.push((data) => typeof data !== 'number' || !isFinite(data) || run(data));
177
186
  }
178
187
  if (P.hasString) {
179
188
  const strs = [];
@@ -330,13 +339,13 @@ function install(deps) {
330
339
  if (collect) {
331
340
  cstep = (data, errors, instancePath, schemaPath, stack, rec) => {
332
341
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
333
- const ok = target.c(data, errors, instancePath, schemaPath + seg, stack, rec);
342
+ const ok = target.c(data, errors, instancePath, schemaPath, stack, rec);
334
343
  if (!ok) undo(rec, pl, n0, xl);
335
344
  return ok;
336
345
  };
337
346
  vstep = (data, stack, rec) => target.v(data, stack, rec);
338
347
  } else {
339
- cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath + seg, stack);
348
+ cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath, stack);
340
349
  vstep = target.v;
341
350
  }
342
351
  if (ctx.guard) {
@@ -443,7 +452,10 @@ function install(deps) {
443
452
  ok = false;
444
453
  }
445
454
  if (maxC !== undefined && matched > maxC) {
446
- if (errors !== NOERRORS) errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: maxC }, `must NOT contain more than ${maxC} valid item(s)`));
455
+ // Reported as `contains` at `#/contains`, the way the generated code
456
+ // and the reference implementation both do it: maxContains bounds the
457
+ // same keyword rather than being one of its own.
458
+ if (errors !== NOERRORS) errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: maxC }, `must NOT contain more than ${maxC} valid item(s)`));
447
459
  ok = false;
448
460
  }
449
461
  return ok;
@@ -489,6 +501,13 @@ function install(deps) {
489
501
  ? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: child(e.node) }))
490
502
  : null;
491
503
  const apFn = P.additionalProperties !== undefined ? child(P.additionalProperties) : null;
504
+ // `additionalProperties: false` is a boolean subschema, but its error is
505
+ // not the generic "boolean schema is false": every consumer switches on
506
+ // keyword === 'additionalProperties' and reads params.additionalProperty
507
+ // to learn which key was rejected. Reporting it as `not` at the key's own
508
+ // path loses both, and that shape is what the generated code emits, so
509
+ // the two engines have to agree here.
510
+ const apFalse = P.additionalProperties === false;
492
511
  const pnFn = P.propertyNames !== undefined ? child(P.propertyNames) : null;
493
512
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
494
513
  if (dataBits(data) !== T_OBJECT) return true;
@@ -496,7 +515,12 @@ function install(deps) {
496
515
  const keys = collect ? keysOf(rec, data) : Object.keys(data);
497
516
  if (pnFn !== null) {
498
517
  for (const k of keys) {
499
- if (!pnFn.c(k, errors, instancePath + '/' + escapePointer(k), schemaPath + '/propertyNames', stack)) {
518
+ // propertyNames applies its subschema to the property NAME, not to
519
+ // the value sitting at that key, so the failure is reported against
520
+ // the object that carries the name rather than against `/<key>`.
521
+ // The generated code and the compat shim both do it this way, and
522
+ // the shim has to because that is where the reference puts it.
523
+ if (!pnFn.c(k, errors, instancePath, schemaPath + '/propertyNames', stack)) {
500
524
  ok = false;
501
525
  if (errors === NOERRORS) return false;
502
526
  }
@@ -526,7 +550,11 @@ function install(deps) {
526
550
  }
527
551
  }
528
552
  if (!evaluated && apFn !== null) {
529
- if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
553
+ if (apFalse) {
554
+ ok = false;
555
+ if (errors === NOERRORS) return false;
556
+ errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
557
+ } else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
530
558
  ok = false;
531
559
  if (errors === NOERRORS) return false;
532
560
  }
@@ -713,20 +741,31 @@ function install(deps) {
713
741
  }
714
742
  if (P.anyOf !== null) {
715
743
  const fns = P.anyOf.map((v) => inPlace(v, base));
744
+ const titles = P.anyOf.map(branchTitle);
716
745
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
717
746
  const scratch = errors === NOERRORS ? NOERRORS : [];
718
747
  let any = false;
748
+ // Per-branch errors, taken as slices of the shared scratch rather than
749
+ // an array each, so a branch that passes costs nothing extra.
750
+ const branches = errors === NOERRORS ? null : [];
719
751
  for (let i = 0; i < fns.length; i++) {
752
+ const mark = branches === null ? 0 : scratch.length;
753
+ let ok;
720
754
  if (collect) {
721
755
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
722
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec)) any = true;
756
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec);
757
+ if (ok) any = true;
723
758
  else undo(rec, pl, n0, xl);
724
- } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack)) any = true;
759
+ } else {
760
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack);
761
+ if (ok) any = true;
762
+ }
763
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
725
764
  }
726
765
  if (!any) {
727
- if (errors !== NOERRORS) {
728
- for (const e of scratch) errors.push(e);
729
- errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
766
+ if (branches !== null) {
767
+ const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
768
+ if (collapsed !== null) errors.push(collapsed);
730
769
  }
731
770
  return false;
732
771
  }
@@ -752,25 +791,36 @@ function install(deps) {
752
791
  }
753
792
  if (P.oneOf !== null) {
754
793
  const fns = P.oneOf.map((v) => inPlace(v, base));
794
+ const titles = P.oneOf.map(branchTitle);
755
795
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
756
796
  const scratch = errors === NOERRORS ? NOERRORS : [];
757
797
  let count = 0;
798
+ // Per-branch results, so a failing oneOf collapses to the branch that
799
+ // came closest instead of every branch's complaints at once.
800
+ const branches = errors === NOERRORS ? null : [];
758
801
  // With a record: the winner's annotations stay, every other branch's
759
802
  // are undone, and a second winner undoes everything since the node
760
803
  // fails and merges nothing.
761
804
  const pl0 = collect ? pLen(rec) : 0, n00 = collect ? rec.n : 0, xl0 = collect ? xLen(rec) : 0;
762
805
  for (let i = 0; i < fns.length; i++) {
806
+ const mark = branches === null ? 0 : scratch.length;
807
+ let ok;
763
808
  if (collect) {
764
809
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
765
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec)) count++;
810
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec);
811
+ if (ok) count++;
766
812
  else undo(rec, pl, n0, xl);
767
- } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack)) count++;
813
+ } else {
814
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack);
815
+ if (ok) count++;
816
+ }
817
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
768
818
  }
769
819
  if (count !== 1) {
770
820
  if (collect) undo(rec, pl0, n00, xl0);
771
- if (errors !== NOERRORS) {
772
- if (count === 0) for (const e of scratch) errors.push(e);
773
- errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
821
+ if (branches !== null) {
822
+ const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
823
+ if (collapsed !== null) errors.push(collapsed);
774
824
  }
775
825
  return false;
776
826
  }
package/lib/tier0.js CHANGED
@@ -100,6 +100,16 @@ function buildTier0Plan(schema) {
100
100
  };
101
101
  }
102
102
 
103
+ // The same reading of `multipleOf` the other two engines use: divide and ask
104
+ // whether the quotient is whole. An exact remainder test called 1 not a
105
+ // multiple of 0.1, because 1 % 0.1 is 0.09999999999999995, and this file
106
+ // answers a fresh validator's first calls, so that was the answer users saw.
107
+ function _multipleOk(v, m) {
108
+ if (m === 0) return false;
109
+ const q = v / m;
110
+ return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9;
111
+ }
112
+
103
113
  // checkPrimitive stays exported for Tier 1 reuse.
104
114
  function checkPrimitive(c, v) {
105
115
  const m = c.typeMask;
@@ -117,7 +127,7 @@ function checkPrimitive(c, v) {
117
127
  if ((f & F_MAX) && v > c.max) return false;
118
128
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
119
129
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
120
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
130
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
121
131
  }
122
132
  } else if (m === T_NUMBER) {
123
133
  if (typeof v !== 'number' || !isFinite(v)) return false;
@@ -127,7 +137,7 @@ function checkPrimitive(c, v) {
127
137
  if ((f & F_MAX) && v > c.max) return false;
128
138
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
129
139
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
130
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
140
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
131
141
  }
132
142
  } else if (m === T_BOOLEAN) {
133
143
  if (typeof v !== 'boolean') return false;
@@ -171,7 +181,7 @@ function tier0ValidateObject(plan, data) {
171
181
  if ((f & F_MAX) && v > c.max) return false;
172
182
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
173
183
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
174
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
184
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
175
185
  }
176
186
  } else if (m === T_NUMBER) {
177
187
  if (typeof v !== 'number' || !isFinite(v)) return false;
@@ -181,7 +191,7 @@ function tier0ValidateObject(plan, data) {
181
191
  if ((f & F_MAX) && v > c.max) return false;
182
192
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
183
193
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
184
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
194
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
185
195
  }
186
196
  } else if (m === T_BOOLEAN) {
187
197
  if (typeof v !== 'boolean') return false;
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.18.1';
10
+ module.exports = '1.20.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -51,7 +51,7 @@
51
51
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.18.1",
126
- "@ata-validator/native-darwin-x64": "1.18.1",
127
- "@ata-validator/native-linux-arm64-gnu": "1.18.1",
128
- "@ata-validator/native-linux-arm64-musl": "1.18.1",
129
- "@ata-validator/native-linux-x64-gnu": "1.18.1",
130
- "@ata-validator/native-linux-x64-musl": "1.18.1",
131
- "@ata-validator/native-win32-x64": "1.18.1"
125
+ "@ata-validator/native-darwin-arm64": "1.20.0",
126
+ "@ata-validator/native-darwin-x64": "1.20.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.20.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.20.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.20.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.20.0",
131
+ "@ata-validator/native-win32-x64": "1.20.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"