ata-validator 1.13.2 → 1.14.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 CHANGED
@@ -216,6 +216,31 @@ function collectRemovals(schema, actions, path) {
216
216
  }
217
217
  }
218
218
 
219
+ // Emit the in-place strip for one schema node and everything under its
220
+ // `properties`. Scope matches collectRemovals(): object properties only, so
221
+ // the two paths keep the same answer.
222
+ function emitRemovals(node, access, lines, depth, seen) {
223
+ if (!node || typeof node !== 'object' || !node.properties) return;
224
+ if (seen.has(node)) return;
225
+ seen.add(node);
226
+ const keys = Object.keys(node.properties);
227
+ if (node.additionalProperties === false && keys.length > 0) {
228
+ const kv = '_k' + depth;
229
+ const checks = keys.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&');
230
+ const guard = depth === 0
231
+ ? ''
232
+ : `if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access}))`;
233
+ lines.push(`${guard}for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`);
234
+ }
235
+ for (const key of keys) {
236
+ const prop = node.properties[key];
237
+ if (prop && typeof prop === 'object' && prop.properties) {
238
+ emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, lines, depth + 1, seen);
239
+ }
240
+ }
241
+ seen.delete(node);
242
+ }
243
+
219
244
  // Generate a fast preprocess function via codegen instead of closure arrays
220
245
  function buildPreprocessCodegen(schema, options) {
221
246
  if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
@@ -223,10 +248,14 @@ function buildPreprocessCodegen(schema, options) {
223
248
  const props = schema.properties;
224
249
  const keys = Object.keys(props);
225
250
 
226
- // removeAdditional: inline key check
227
- if (options.removeAdditional && schema.additionalProperties === false) {
228
- const checks = keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&');
229
- lines.push(`for(var _k in d)if(${checks})delete d[_k]`);
251
+ // removeAdditional: strip unknown keys at every level the schema describes,
252
+ // not just the top one. The closure path below (collectRemovals) always
253
+ // recursed; this one did not, so the same schema and the same document got
254
+ // opposite verdicts depending on whether the runtime allowed code
255
+ // generation, and the codegen answer was the one that disagreed with both
256
+ // the interpreter and the default validator this aims to match.
257
+ if (options.removeAdditional) {
258
+ emitRemovals(schema, 'd', lines, 0, new Set());
230
259
  }
231
260
 
232
261
  // coerceTypes: inline per property
package/lib/aot-impl.js CHANGED
@@ -140,6 +140,84 @@ function emitFormatDecls(closures, mode, declKW) {
140
140
  return { decls: out, exportsSetFormats: false };
141
141
  }
142
142
 
143
+ // Emit an expression that rebuilds `access` from the properties the schema
144
+ // declares, dropping everything else. This is the sanitising counterpart to
145
+ // removeAdditional: instead of enumerating the input and deleting the keys
146
+ // that do not belong, it reads only the keys that do. That is O(schema)
147
+ // rather than O(input), it leaves the caller's object untouched, and it keeps
148
+ // one hidden class instead of walking the object through a transition per
149
+ // delete.
150
+ //
151
+ // Returns null when the clone cannot be proven exact, and the module then
152
+ // simply has no parse(). Declining is recoverable; a sanitiser that quietly
153
+ // drops a property the schema allows is not.
154
+ function emitClone(node, access, depth) {
155
+ if (!node || typeof node !== 'object') return null;
156
+ if (depth > 12) return null;
157
+ // Only plain object nodes are cloneable, and only when the schema names
158
+ // every property it keeps. A union type, a $ref, or any composition means
159
+ // the set of allowed keys is not this node's to decide.
160
+ if (node.$ref || node.allOf || node.anyOf || node.oneOf || node.not ||
161
+ node.if || node.patternProperties || node.unevaluatedProperties ||
162
+ node.additionalProperties === true ||
163
+ (node.additionalProperties && typeof node.additionalProperties === 'object')) {
164
+ return null;
165
+ }
166
+ if (node.type !== 'object' || !node.properties) return null;
167
+ const keys = Object.keys(node.properties);
168
+ if (keys.length === 0) return null;
169
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
170
+
171
+ // A property is copyable only when the emitter can name everything that
172
+ // may live under it: a primitive holds nothing, and an object with declared
173
+ // properties is rebuilt the same way. A $ref, a composition, an array of
174
+ // objects or an untyped node could all carry keys this code cannot see, and
175
+ // copying the reference would smuggle them into a value that claims to be
176
+ // sanitised, so the whole clone is declined instead.
177
+ const PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
178
+ const fixed = [];
179
+ const conditional = [];
180
+ for (const key of keys) {
181
+ const prop = node.properties[key];
182
+ if (!prop || typeof prop !== 'object') return null;
183
+ if (prop.$ref || prop.allOf || prop.anyOf || prop.oneOf || prop.not || prop.if) return null;
184
+ const read = `${access}[${JSON.stringify(key)}]`;
185
+ let value;
186
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type];
187
+ if (types.every((t) => PRIMITIVE.has(t))) {
188
+ value = read;
189
+ } else if (prop.type === 'object' && prop.properties) {
190
+ const nested = emitClone(prop, read, depth + 1);
191
+ if (!nested) return null;
192
+ // The nested schema may also admit null, in which case there is nothing
193
+ // to rebuild and the value passes through.
194
+ value = required.has(key) ? nested : nested;
195
+ } else if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
196
+ !Array.isArray(prop.items) &&
197
+ (Array.isArray(prop.items.type) ? prop.items.type : [prop.items.type])
198
+ .every((t) => PRIMITIVE.has(t))) {
199
+ // An array of primitives has nowhere to hide a key. The reference is
200
+ // shared with the input, exactly as a shallow strip would leave it.
201
+ value = read;
202
+ } else {
203
+ return null;
204
+ }
205
+ if (required.has(key)) fixed.push(`${JSON.stringify(key)}: ${value}`);
206
+ else conditional.push({ key, value });
207
+ }
208
+ if (fixed.length === 0 && conditional.length === 0) return null;
209
+
210
+ const literal = `{ ${fixed.join(', ')} }`;
211
+ if (conditional.length === 0) return literal;
212
+ // Optional properties are added only when present, so the result never
213
+ // gains a key the input did not have.
214
+ const tmp = `_c${depth}`;
215
+ const adds = conditional
216
+ .map(({ key, value }) => `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`)
217
+ .join(' ');
218
+ return `(function(){ const ${tmp} = ${literal}; ${adds} return ${tmp}; })()`;
219
+ }
220
+
143
221
  function toStandaloneModule(validator, opts) {
144
222
  validator._ensureCompiled();
145
223
  const jsFn = validator._jsFn;
@@ -219,10 +297,30 @@ function toStandaloneModule(validator, opts) {
219
297
  ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
220
298
  : 'return _fn(data) ? VALID : ABORT';
221
299
 
222
- const names = fmt.exportsSetFormats ? 'validate, isValid, setFormats' : 'validate, isValid';
300
+ // parse(): validate, then hand back a copy holding only the properties the
301
+ // schema declares. Off by default: it costs bytes in every emitted module,
302
+ // and the reason to compile a schema ahead of time is usually to ship as
303
+ // little as possible. Ask for it with { parse: true }.
304
+ const cloneExpr = !(opts && opts.parse) ? null : emitClone(
305
+ typeof validator._schemaObj === 'object' ? validator._schemaObj : null,
306
+ 'data',
307
+ 0,
308
+ );
309
+ // Named _ataParse rather than parse: the emitted module also carries the
310
+ // safe-regex prelude, which has a module-scope parse() of its own for
311
+ // reading patterns. A second declaration of that name shadowed it and the
312
+ // prelude called this one instead, reaching _fn before its initialiser ran.
313
+ const parseCore = cloneExpr
314
+ ? `function _ataParse(data) {\n if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
315
+ : '';
316
+
317
+ const baseNames = cloneExpr ? 'validate, isValid' : 'validate, isValid';
318
+ const names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
319
+ const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
320
+ const parseProp = cloneExpr ? ', parse: _ataParse' : '';
223
321
  const exports = format === 'esm'
224
- ? `export { ${names} };\nexport default { ${names} };\n`
225
- : `module.exports = { ${names} };\nmodule.exports.default = module.exports;\n`;
322
+ ? `export { ${names}${parseAlias} };\nexport default { ${names}${parseProp} };\n`
323
+ : `module.exports = { ${names}${parseProp} };\nmodule.exports.default = module.exports;\n`;
226
324
 
227
325
  return `// Auto-generated by ata-validator — do not edit.
228
326
  // Schema is embedded; runtime has zero dependency on ata-validator.
@@ -243,7 +341,7 @@ ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
243
341
  };
244
342
  ${errCore}function isValid(data) { return _fn(data); }
245
343
  function validate(data) { ${validBody}; }
246
- ${exports}`;
344
+ ${parseCore}${exports}`;
247
345
  }
248
346
 
249
347
  // Bundle multiple validators into a single JS file for fast startup.
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.13.2';
10
+ module.exports = '1.14.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.13.2",
3
+ "version": "1.14.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_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_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_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_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_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_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.13.2",
126
- "@ata-validator/native-darwin-x64": "1.13.2",
127
- "@ata-validator/native-linux-x64-gnu": "1.13.2",
128
- "@ata-validator/native-linux-arm64-gnu": "1.13.2",
129
- "@ata-validator/native-linux-x64-musl": "1.13.2",
130
- "@ata-validator/native-linux-arm64-musl": "1.13.2",
131
- "@ata-validator/native-win32-x64": "1.13.2"
125
+ "@ata-validator/native-darwin-arm64": "1.14.0",
126
+ "@ata-validator/native-darwin-x64": "1.14.0",
127
+ "@ata-validator/native-linux-x64-gnu": "1.14.0",
128
+ "@ata-validator/native-linux-arm64-gnu": "1.14.0",
129
+ "@ata-validator/native-linux-x64-musl": "1.14.0",
130
+ "@ata-validator/native-linux-arm64-musl": "1.14.0",
131
+ "@ata-validator/native-win32-x64": "1.14.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"