ata-validator 0.12.2 → 0.12.4

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.d.ts CHANGED
@@ -4,8 +4,17 @@ export interface ValidationError {
4
4
  schemaPath: string;
5
5
  params: Record<string, unknown>;
6
6
  message: string;
7
+ /**
8
+ * The schema object that owns the failing keyword. Populated only when the
9
+ * Validator was constructed with `verbose: true`. Matches ajv's `verbose`
10
+ * behavior.
11
+ */
12
+ parentSchema?: object;
7
13
  }
8
14
 
15
+ /** A user-supplied format checker. Receives the candidate value, returns true if valid. */
16
+ export type FormatChecker = (value: string) => boolean;
17
+
9
18
  export interface ValidationResult {
10
19
  valid: boolean;
11
20
  errors: ValidationError[];
@@ -21,6 +30,26 @@ export interface ValidatorOptions {
21
30
  coerceTypes?: boolean;
22
31
  removeAdditional?: boolean;
23
32
  schemas?: Record<string, object> | object[];
33
+ /**
34
+ * Custom format checkers. Keys are format names referenced from `format` in
35
+ * the schema. Values are functions that return true when the input is valid.
36
+ */
37
+ formats?: Record<string, FormatChecker>;
38
+ /**
39
+ * When true, validation errors include `parentSchema` (the schema object
40
+ * that produced the error). Matches ajv's `verbose: true`.
41
+ */
42
+ verbose?: boolean;
43
+ /**
44
+ * When true, validate() returns a shared frozen result on the first failure
45
+ * instead of collecting full error details. Smaller hot-path allocation.
46
+ */
47
+ abortEarly?: boolean;
48
+ }
49
+
50
+ export interface BundleStandaloneOptions extends ValidatorOptions {
51
+ /** Module format for the emitted bundle. Default: 'cjs'. */
52
+ format?: 'esm' | 'cjs';
24
53
  }
25
54
 
26
55
  export interface StandardSchemaV1Props {
@@ -30,7 +59,13 @@ export interface StandardSchemaV1Props {
30
59
  value: unknown
31
60
  ):
32
61
  | { value: unknown }
33
- | { issues: Array<{ message: string; path?: ReadonlyArray<{ key: PropertyKey }> }> };
62
+ | {
63
+ issues: Array<{
64
+ message: string;
65
+ /** Array indices are emitted as numbers, object keys as strings. */
66
+ path?: ReadonlyArray<{ key: PropertyKey }>;
67
+ }>;
68
+ };
34
69
  }
35
70
 
36
71
  export interface StandaloneModule {
@@ -81,14 +116,29 @@ export class Validator {
81
116
  /** Generate a standalone JS module string for zero-compile loading. Returns null if schema can't be standalone-compiled. */
82
117
  toStandalone(): string | null;
83
118
 
119
+ /**
120
+ * Generate a self-contained module string with `validate`/`isValid` exports.
121
+ * The output has zero runtime dependency on ata-validator.
122
+ *
123
+ * - format: 'esm' (default) or 'cjs'.
124
+ * - abortEarly: if true, invalid results are a shared frozen stub (smaller output, no error details).
125
+ *
126
+ * Returns null if the schema cannot be compiled to a standalone module.
127
+ */
128
+ toStandaloneModule(options?: { format?: 'esm' | 'cjs'; abortEarly?: boolean }): string | null;
129
+
84
130
  /** Load a pre-compiled standalone module. Zero schema compilation at startup. */
85
131
  static fromStandalone(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator;
86
132
 
87
133
  /** Bundle multiple schemas into a single JS module string. Load with Validator.loadBundle(). */
88
134
  static bundle(schemas: object[], options?: ValidatorOptions): string;
89
135
 
90
- /** Bundle multiple schemas into a self-contained JS module. No ata-validator import needed at runtime. */
91
- static bundleStandalone(schemas: object[], options?: ValidatorOptions): string;
136
+ /**
137
+ * Bundle multiple schemas into a self-contained JS module with no
138
+ * ata-validator runtime dependency. Cross-schema `$ref` resolves between
139
+ * the supplied schemas. Set `format: 'esm'` for ESM output (default 'cjs').
140
+ */
141
+ static bundleStandalone(schemas: object[], options?: BundleStandaloneOptions): string;
92
142
 
93
143
  /** Bundle multiple schemas with deduplicated shared templates. Smaller output than bundle(). */
94
144
  static bundleCompact(schemas: object[], options?: ValidatorOptions): string;
package/index.js CHANGED
@@ -290,14 +290,41 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
290
290
  // (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
291
291
  const SIMDJSON_THRESHOLD = 8192;
292
292
 
293
+ // Resolve a JSON Schema path like "#/properties/name/type" to the schema object
294
+ // that *contains* the failing keyword. Used by verbose mode to populate
295
+ // `parentSchema` on validation errors. Returns undefined if the path can't be
296
+ // walked (malformed pointer or missing intermediate node).
297
+ function resolveSchemaByPath(rootSchema, schemaPath) {
298
+ if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) {
299
+ return undefined;
300
+ }
301
+ const stripped = schemaPath.slice(1);
302
+ if (!stripped || stripped === '/') return rootSchema;
303
+ const parts = stripped.split('/').filter(Boolean).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'));
304
+ // The last segment is the keyword that failed (e.g. "type"); parentSchema is
305
+ // the schema object that owns that keyword, so walk all but the last segment.
306
+ let target = rootSchema;
307
+ for (let i = 0; i < parts.length - 1; i++) {
308
+ if (target == null || typeof target !== 'object') return undefined;
309
+ target = target[parts[i]];
310
+ }
311
+ return target;
312
+ }
313
+
293
314
  function parsePointerPath(path) {
294
315
  if (!path) return [];
295
316
  return path
296
317
  .split("/")
297
318
  .filter(Boolean)
298
- .map((seg) => ({
299
- key: seg.replace(/~1/g, "/").replace(/~0/g, "~"),
300
- }));
319
+ .map((seg) => {
320
+ const decoded = seg.replace(/~1/g, "/").replace(/~0/g, "~");
321
+ // Per Standard Schema V1: array indices should be emitted as numbers,
322
+ // object keys as strings. Treat all-digit segments as numeric indices.
323
+ if (/^(0|[1-9][0-9]*)$/.test(decoded)) {
324
+ return { key: Number(decoded) };
325
+ }
326
+ return { key: decoded };
327
+ });
301
328
  }
302
329
 
303
330
  function createPaddedBuffer(jsonStr) {
@@ -366,6 +393,15 @@ class Validator {
366
393
  // Schema map for cross-schema $ref resolution
367
394
  this._schemaMap = buildSchemaMap(options.schemas) || new Map();
368
395
 
396
+ // User-supplied format checkers: { formatName: (value) => boolean }.
397
+ // Looked up at runtime when a schema references a format the built-in
398
+ // registry does not know about.
399
+ this._userFormats = options.formats || null;
400
+
401
+ // Verbose mode: when on, errors carry parentSchema (the schema object that
402
+ // produced the error). Matches ajv's `verbose: true` behavior.
403
+ this._verbose = !!options.verbose;
404
+
369
405
  // Lazy stubs: trigger compilation on first call, then re-dispatch
370
406
  this.validate = (data) => {
371
407
  this._ensureCompiled();
@@ -464,7 +500,9 @@ class Validator {
464
500
  const mapKey = this._schemaMap.size > 0
465
501
  ? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
466
502
  : this._schemaStr;
467
- const cached = _compileCache.get(mapKey);
503
+ // Custom formats are JS functions: bypass the compile cache since they can
504
+ // differ between validators that share the same schema string.
505
+ const cached = this._userFormats ? null : _compileCache.get(mapKey);
468
506
  let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
469
507
  var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
470
508
  if (cached && !_forceNapi) {
@@ -473,12 +511,15 @@ class Validator {
473
511
  jsErrFn = cached.errFn;
474
512
  _isCodegen = !!cached.isCodegen;
475
513
  } else if (!_forceNapi) {
476
- const _cgFn = compileToJSCodegen(schemaObj, sm);
514
+ const uf = this._userFormats;
515
+ const _cgFn = compileToJSCodegen(schemaObj, sm, uf);
477
516
  jsFn = _cgFn || compileToJS(schemaObj, null, sm);
478
- jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm);
479
- jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm);
517
+ jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, uf);
518
+ jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, uf);
480
519
  _isCodegen = !!_cgFn;
481
- _compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
520
+ if (!uf) {
521
+ _compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
522
+ }
482
523
  } else {
483
524
  jsFn = null; jsCombinedFn = null; jsErrFn = null;
484
525
  }
@@ -617,6 +658,24 @@ class Validator {
617
658
  }
618
659
  : (data) => (jsFn(data) ? VALID_RESULT : errFn(data));
619
660
  }
661
+ // Verbose mode: populate parentSchema on each error.
662
+ // Errors may be frozen, so clone them with the extra field.
663
+ if (this._verbose) {
664
+ const inner = this.validate;
665
+ const root = this._schemaObj;
666
+ this.validate = (data) => {
667
+ const result = inner(data);
668
+ if (result && !result.valid && result.errors) {
669
+ const enriched = result.errors.map((err) =>
670
+ err && err.parentSchema === undefined
671
+ ? { ...err, parentSchema: resolveSchemaByPath(root, err.schemaPath) }
672
+ : err
673
+ );
674
+ return { valid: false, errors: enriched };
675
+ }
676
+ return result;
677
+ };
678
+ }
620
679
  this.isValidObject = jsFn;
621
680
  const hybridFn = jsFn._hybridFactory
622
681
  ? jsFn._hybridFactory(VALID_RESULT, errFn)
@@ -821,19 +880,24 @@ class Validator {
821
880
  const mapKey = this._schemaMap.size > 0
822
881
  ? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
823
882
  : this._schemaStr;
824
- const cached = _compileCache.get(mapKey);
883
+ // Custom formats are JS functions: skip the shared cache so different
884
+ // validators with the same schema string but different formats don't collide.
885
+ const cached = this._userFormats ? null : _compileCache.get(mapKey);
825
886
  if (cached && cached.jsFn) {
826
887
  this._jsFn = cached.jsFn;
827
888
  this.isValidObject = cached.jsFn;
828
889
  return;
829
890
  }
830
- const jsFn = compileToJSCodegen(this._schemaObj, sm) || compileToJS(this._schemaObj, null, sm);
891
+ const uf = this._userFormats;
892
+ const jsFn = compileToJSCodegen(this._schemaObj, sm, uf) || compileToJS(this._schemaObj, null, sm);
831
893
  this._jsFn = jsFn;
832
894
  if (jsFn) {
833
895
  this.isValidObject = jsFn;
834
896
  // seed cache with codegen, combined/errFn filled later by _ensureCompiled
835
- if (!cached) _compileCache.set(mapKey, { jsFn, combined: null, errFn: null });
836
- else cached.jsFn = jsFn;
897
+ if (!uf) {
898
+ if (!cached) _compileCache.set(mapKey, { jsFn, combined: null, errFn: null });
899
+ else cached.jsFn = jsFn;
900
+ }
837
901
  }
838
902
  }
839
903
 
@@ -1122,15 +1186,21 @@ Validator.bundle = function (schemas, opts) {
1122
1186
  };
1123
1187
 
1124
1188
  // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
1189
+ // opts.format: 'cjs' (default) or 'esm'.
1125
1190
  Validator.bundleStandalone = function (schemas, opts) {
1191
+ // Cross-schema $ref resolution: every Validator in the bundle needs to know
1192
+ // about the others so $ref to a sibling $id can resolve at compile time.
1193
+ const bundleOpts = { ...(opts || {}), schemas };
1194
+ const format = (opts && opts.format) || 'cjs';
1126
1195
  const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
1127
1196
  const fns = schemas.map((schema) => {
1128
- const v = new Validator(schema, opts);
1197
+ const v = new Validator(schema, bundleOpts);
1129
1198
  v._ensureCompiled();
1130
1199
  const jsFn = v._jsFn;
1131
1200
  if (!jsFn || !jsFn._hybridSource) return "null";
1132
1201
  const jsErrFn = compileToJSCodegenWithErrors(
1133
1202
  typeof schema === "string" ? JSON.parse(schema) : schema,
1203
+ v._schemaMap,
1134
1204
  );
1135
1205
  const errBody =
1136
1206
  jsErrFn && jsErrFn._errSource
@@ -1138,6 +1208,10 @@ Validator.bundleStandalone = function (schemas, opts) {
1138
1208
  : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
1139
1209
  return `(function(R){var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
1140
1210
  });
1211
+ const arr = `[${fns.join(",")}]`;
1212
+ if (format === 'esm') {
1213
+ return `// Auto-generated by ata-validator — do not edit\nconst R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
1214
+ }
1141
1215
  return `'use strict';\nvar R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
1142
1216
  };
1143
1217
 
@@ -768,7 +768,7 @@ function hasAdditionalPropertiesSchema(schema) {
768
768
 
769
769
  // --- Codegen mode: generates a single Function (NOT CSP-safe) ---
770
770
  // This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
771
- function compileToJSCodegen(schema, schemaMap) {
771
+ function compileToJSCodegen(schema, schemaMap, userFormats) {
772
772
  if (typeof schema === 'boolean') return schema ? () => true : () => false
773
773
  if (typeof schema !== 'object' || schema === null) return null
774
774
 
@@ -827,7 +827,7 @@ function compileToJSCodegen(schema, schemaMap) {
827
827
  }
828
828
  }
829
829
 
830
- const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
830
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
831
831
  const lines = []
832
832
  genCode(schema, 'd', lines, ctx)
833
833
 
@@ -1320,7 +1320,19 @@ function genCode(schema, v, lines, ctx, knownType) {
1320
1320
 
1321
1321
  if (schema.format) {
1322
1322
  const fc = FORMAT_CODEGEN[schema.format]
1323
- if (fc) lines.push(fc(v, isStr))
1323
+ if (fc) {
1324
+ lines.push(fc(v, isStr))
1325
+ } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
1326
+ // User-supplied format checker: thread the function via closure and call at runtime.
1327
+ const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
1328
+ const closureName = `_uf_${safeName}`
1329
+ if (!ctx.closureVars.includes(closureName)) {
1330
+ ctx.closureVars.push(closureName)
1331
+ ctx.closureVals.push(ctx.userFormats[schema.format])
1332
+ }
1333
+ const guard = isStr ? '' : `typeof ${v}==='string'&&`
1334
+ lines.push(`if(${guard}!${closureName}(${v}))return false`)
1335
+ }
1324
1336
  }
1325
1337
 
1326
1338
  // uniqueItems — tiered strategy based on expected array size
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Ultra-fast JSON Schema validator. 5x faster validation, 159,000x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",