ata-validator 0.12.4 → 0.12.6

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
@@ -140,8 +140,12 @@ export class Validator {
140
140
  */
141
141
  static bundleStandalone(schemas: object[], options?: BundleStandaloneOptions): string;
142
142
 
143
- /** Bundle multiple schemas with deduplicated shared templates. Smaller output than bundle(). */
144
- static bundleCompact(schemas: object[], options?: ValidatorOptions): string;
143
+ /**
144
+ * Bundle multiple schemas with deduplicated shared templates. Smaller output
145
+ * than bundle(). Accepts the same options as bundleStandalone, including
146
+ * `format: 'esm' | 'cjs'` and cross-schema `$ref` resolution.
147
+ */
148
+ static bundleCompact(schemas: object[], options?: BundleStandaloneOptions): string;
145
149
 
146
150
  /** Load a bundle created by Validator.bundle(). Returns array of Validator instances. */
147
151
  static loadBundle(mods: object[], schemas: object[], options?: ValidatorOptions): Validator[];
package/index.js CHANGED
@@ -1187,10 +1187,12 @@ Validator.bundle = function (schemas, opts) {
1187
1187
 
1188
1188
  // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
1189
1189
  // opts.format: 'cjs' (default) or 'esm'.
1190
+ // opts.formats: { name: fn } — embedded in the output via Function#toString.
1190
1191
  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 };
1192
+ // Cross-schema $ref resolution: only meaningful when at least one schema has
1193
+ // an $id. Skip the schemas-as-map plumbing when none of them do.
1194
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1195
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1194
1196
  const format = (opts && opts.format) || 'cjs';
1195
1197
  const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
1196
1198
  const fns = schemas.map((schema) => {
@@ -1201,12 +1203,25 @@ Validator.bundleStandalone = function (schemas, opts) {
1201
1203
  const jsErrFn = compileToJSCodegenWithErrors(
1202
1204
  typeof schema === "string" ? JSON.parse(schema) : schema,
1203
1205
  v._schemaMap,
1206
+ v._userFormats,
1204
1207
  );
1205
1208
  const errBody =
1206
1209
  jsErrFn && jsErrFn._errSource
1207
1210
  ? jsErrFn._errSource
1208
1211
  : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
1209
- return `(function(R){var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
1212
+ // Serialize custom format closures so the bundle has no runtime dep on ata.
1213
+ let preamble = '';
1214
+ if (jsFn._formatClosures) {
1215
+ preamble = jsFn._formatClosures
1216
+ .map(({ name, fn }) => `var ${name}=${fn.toString()};`)
1217
+ .join('\n');
1218
+ }
1219
+ if (opts && opts.verbose) {
1220
+ // Embed the schema and a small resolver so errors carry parentSchema.
1221
+ const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
1222
+ return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
1223
+ }
1224
+ return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
1210
1225
  });
1211
1226
  const arr = `[${fns.join(",")}]`;
1212
1227
  if (format === 'esm') {
@@ -1217,15 +1232,20 @@ Validator.bundleStandalone = function (schemas, opts) {
1217
1232
 
1218
1233
  // Compact bundle: deduplicated code. Shared template functions + per-schema params.
1219
1234
  // Much smaller file → faster V8 parse → faster startup.
1235
+ // opts.format: 'cjs' (default) or 'esm'.
1220
1236
  Validator.bundleCompact = function (schemas, opts) {
1237
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1238
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1239
+ const format = (opts && opts.format) || 'cjs';
1221
1240
  // Analyze schemas and group by structure
1222
1241
  const entries = schemas.map((schema) => {
1223
- const v = new Validator(schema, opts);
1242
+ const v = new Validator(schema, bundleOpts);
1224
1243
  v._ensureCompiled();
1225
1244
  const jsFn = v._jsFn;
1226
1245
  if (!jsFn || !jsFn._hybridSource) return null;
1227
1246
  const jsErrFn = compileToJSCodegenWithErrors(
1228
1247
  typeof schema === "string" ? JSON.parse(schema) : schema,
1248
+ v._schemaMap,
1229
1249
  );
1230
1250
  return {
1231
1251
  hybrid: jsFn._hybridSource,
@@ -1260,31 +1280,38 @@ Validator.bundleCompact = function (schemas, opts) {
1260
1280
  });
1261
1281
 
1262
1282
  // Generate compact bundle
1263
- let out = "'use strict';\n";
1264
- out += "var R=Object.freeze({valid:true,errors:Object.freeze([])});\n";
1283
+ const isEsm = format === 'esm';
1284
+ let out = isEsm
1285
+ ? "// Auto-generated by ata-validator — do not edit\n"
1286
+ : "'use strict';\n";
1287
+ const declKW = isEsm ? "const" : "var";
1288
+ out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
1265
1289
 
1266
1290
  // Shared hybrid factories
1267
- out += "var H=[\n";
1291
+ out += `${declKW} H=[\n`;
1268
1292
  out += bodies
1269
1293
  .map((b) => `function(R,E){return function(d){${b}}}`)
1270
1294
  .join(",\n");
1271
1295
  out += "\n];\n";
1272
1296
 
1273
1297
  // Shared error functions
1274
- out += "var EF=[\n";
1298
+ out += `${declKW} EF=[\n`;
1275
1299
  out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(",\n");
1276
1300
  out += "\n];\n";
1277
1301
 
1278
1302
  // Build validators from shared templates
1279
- out += "module.exports=[";
1280
- out += indices
1303
+ const arrBody = indices
1281
1304
  .map(([hi, ei]) => {
1282
1305
  if (hi < 0) return "null";
1283
1306
  if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
1284
1307
  return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
1285
1308
  })
1286
1309
  .join(",");
1287
- out += "];\n";
1310
+ if (isEsm) {
1311
+ out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
1312
+ } else {
1313
+ out += `module.exports=[${arrBody}];\n`;
1314
+ }
1288
1315
 
1289
1316
  return out;
1290
1317
  };
@@ -496,6 +496,50 @@ function collectDefs(schema) {
496
496
  return defs
497
497
  }
498
498
 
499
+ // Walk a JSON-pointer fragment ("/foo/bar/0") into a schema object.
500
+ // Returns the target node, or null if any segment is missing.
501
+ function walkJsonPointer(root, fragment) {
502
+ if (!fragment || fragment === '/' || fragment === '#') return root
503
+ const path = fragment.startsWith('#') ? fragment.slice(1) : fragment
504
+ if (!path.startsWith('/')) return null
505
+ const parts = path.split('/').slice(1).map(s => s.replace(/~1/g, '/').replace(/~0/g, '~'))
506
+ let target = root
507
+ for (const p of parts) {
508
+ if (target == null || typeof target !== 'object') return null
509
+ target = target[p]
510
+ }
511
+ return target == null ? null : target
512
+ }
513
+
514
+ // Resolve a cross-schema $ref of the form "<id>#/<json-pointer>" (or just "<id>").
515
+ // Returns { schema, fullId } where fullId is the resolved $id of the host schema.
516
+ function resolveCrossSchemaRef(ref, schemaMap) {
517
+ if (!schemaMap) return null
518
+ const hashIdx = ref.indexOf('#')
519
+ const baseId = hashIdx >= 0 ? ref.slice(0, hashIdx) : ref
520
+ const fragment = hashIdx >= 0 ? ref.slice(hashIdx) : ''
521
+ if (!baseId) return null
522
+
523
+ let baseSchema = null
524
+ let fullId = null
525
+ if (schemaMap.has(baseId)) {
526
+ baseSchema = schemaMap.get(baseId)
527
+ fullId = baseId
528
+ } else if (!ref.includes('://')) {
529
+ for (const [id] of schemaMap) {
530
+ if (id.endsWith('/' + baseId)) {
531
+ baseSchema = schemaMap.get(id)
532
+ fullId = id
533
+ break
534
+ }
535
+ }
536
+ }
537
+ if (!baseSchema) return null
538
+ const target = fragment ? walkJsonPointer(baseSchema, fragment) : baseSchema
539
+ if (target == null) return null
540
+ return { schema: target, fullId }
541
+ }
542
+
499
543
  function resolveRef(ref, defs, schemaMap) {
500
544
  // Self-reference: "#" — treat as permissive to avoid infinite recursion
501
545
  if (ref === '#') return () => true
@@ -520,7 +564,15 @@ function resolveRef(ref, defs, schemaMap) {
520
564
  const fn = compileToJS(resolved, null, schemaMap)
521
565
  return fn || (() => true)
522
566
  }
523
- // 3. Cross-schema ref (relative URI resolution)
567
+ // 3. Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
568
+ if (schemaMap && ref.includes('#')) {
569
+ const r = resolveCrossSchemaRef(ref, schemaMap)
570
+ if (r) {
571
+ const fn = compileToJS(r.schema, null, schemaMap)
572
+ return fn || (() => true)
573
+ }
574
+ }
575
+ // 4. Cross-schema ref (relative URI resolution, no fragment)
524
576
  if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
525
577
  for (const [id] of schemaMap) {
526
578
  if (id.endsWith('/' + ref)) {
@@ -652,6 +704,11 @@ function codegenSafe(schema, schemaMap) {
652
704
  if (id.endsWith('/' + schema.$ref)) { isResolvable = true; resolvedTarget = schemaMap.get(id); break }
653
705
  }
654
706
  }
707
+ // Cross-schema ref with JSON pointer fragment: "<id>#/<path>"
708
+ if (!isLocal && !isResolvable && schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
709
+ const r = resolveCrossSchemaRef(schema.$ref, schemaMap)
710
+ if (r) { isResolvable = true; resolvedTarget = r.schema }
711
+ }
655
712
  // Anchor-style ref: #name (not #/path, not bare #) — resolvable at compile time via anchors map
656
713
  const isAnchorRef = !isLocal && !isResolvable && schema.$ref.length > 1 && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')
657
714
  if (!isLocal && !isResolvable && !isAnchorRef) return false
@@ -889,6 +946,17 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
889
946
  boolFn._source = helperStr + body
890
947
  boolFn._preambleSource = preambleStr
891
948
  boolFn._hybridSource = helperStr + hybridBody
949
+ // Custom-format closure entries that the bundle output needs to recreate.
950
+ // Stored as { name, fn } so consumers can serialize via Function#toString.
951
+ if (ctx.userFormats) {
952
+ const fmtEntries = []
953
+ for (let i = 0; i < closureNames.length; i++) {
954
+ if (closureNames[i].startsWith('_uf_')) {
955
+ fmtEntries.push({ name: closureNames[i], fn: closureValues[i] })
956
+ }
957
+ }
958
+ if (fmtEntries.length) boolFn._formatClosures = fmtEntries
959
+ }
892
960
 
893
961
  return boolFn
894
962
  } catch {
@@ -1093,13 +1161,17 @@ function genCode(schema, v, lines, ctx, knownType) {
1093
1161
  }
1094
1162
  }
1095
1163
  } else if (schema.$ref !== '#' && ctx.schemaMap) {
1096
- // 2. Cross-schema ref (exact match or relative URI)
1164
+ // 2. Cross-schema ref (exact match, relative URI, or JSON pointer fragment)
1097
1165
  let resolved = ctx.schemaMap.get(schema.$ref)
1098
1166
  if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
1099
1167
  for (const [id, s] of ctx.schemaMap) {
1100
1168
  if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
1101
1169
  }
1102
1170
  }
1171
+ if (!resolved && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
1172
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
1173
+ if (r) resolved = r.schema
1174
+ }
1103
1175
  if (resolved) {
1104
1176
  if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1105
1177
  else {
@@ -2387,7 +2459,7 @@ function genCharCodeSwitch(keys, v) {
2387
2459
  // --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
2388
2460
  // Returns a function: (data, allErrors) => { valid, errors }
2389
2461
  // Valid path is still fast — only error path does extra work.
2390
- function compileToJSCodegenWithErrors(schema, schemaMap) {
2462
+ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats) {
2391
2463
  // Bail on unevaluated keywords — error codegen doesn't support them yet
2392
2464
  if (typeof schema === 'object' && schema !== null) {
2393
2465
  const s = JSON.stringify(schema)
@@ -2448,7 +2520,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
2448
2520
  }
2449
2521
  }
2450
2522
 
2451
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema }
2523
+ const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema, userFormats: userFormats || null }
2452
2524
  ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
2453
2525
  const lines = []
2454
2526
  genCodeE(schema, 'd', '', lines, ctx, '#')
@@ -2516,6 +2588,17 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2516
2588
  ctx.refStack.delete(schema.$ref)
2517
2589
  return
2518
2590
  }
2591
+ // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
2592
+ if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
2593
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
2594
+ if (r) {
2595
+ if (ctx.refStack.has(schema.$ref)) return
2596
+ ctx.refStack.add(schema.$ref)
2597
+ genCodeE(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
2598
+ ctx.refStack.delete(schema.$ref)
2599
+ return
2600
+ }
2601
+ }
2519
2602
  }
2520
2603
 
2521
2604
  // $dynamicRef — resolve via anchors map
@@ -2661,15 +2744,23 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2661
2744
  }
2662
2745
  if (schema.format) {
2663
2746
  const fc = FORMAT_CODEGEN[schema.format]
2747
+ const failPush = `_e.push({keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/format',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"'});if(!_all)return{valid:false,errors:_e}`
2664
2748
  // Format errors use the boolean codegen — just wrap with error push
2665
2749
  if (fc) {
2666
2750
  const ri = ctx.varCounter++
2667
2751
  const boolLines = []
2668
2752
  boolLines.push(fc(v, isStr))
2669
2753
  // Replace `return false` with error push in the format check
2670
- const fmtCode = boolLines.join(';').replace(/return false/g,
2671
- `{_e.push({keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/format',params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"'});if(!_all)return{valid:false,errors:_e}}`)
2754
+ const fmtCode = boolLines.join(';').replace(/return false/g, `{${failPush}}`)
2672
2755
  lines.push(fmtCode)
2756
+ } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
2757
+ // User-supplied format checker on the error path. Same closure plumbing
2758
+ // as the boolean codegen — but error codegen has no closure factory, so
2759
+ // bundle output serializes the function via Function#toString separately.
2760
+ const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
2761
+ const closureName = `_uf_${safeName}`
2762
+ const guard = isStr ? '' : `typeof ${v}==='string'&&`
2763
+ lines.push(`if(${guard}!${closureName}(${v})){${failPush}}`)
2673
2764
  }
2674
2765
  }
2675
2766
 
@@ -3022,6 +3113,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3022
3113
  ctx.refStack.delete(schema.$ref)
3023
3114
  return
3024
3115
  }
3116
+ // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
3117
+ if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
3118
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
3119
+ if (r) {
3120
+ if (ctx.refStack.has(schema.$ref)) return
3121
+ ctx.refStack.add(schema.$ref)
3122
+ genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
3123
+ ctx.refStack.delete(schema.$ref)
3124
+ return
3125
+ }
3126
+ }
3025
3127
  }
3026
3128
 
3027
3129
  // $dynamicRef — resolve via anchors map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
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",