ata-validator 0.12.3 → 0.12.5

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 {
@@ -98,8 +133,12 @@ export class Validator {
98
133
  /** Bundle multiple schemas into a single JS module string. Load with Validator.loadBundle(). */
99
134
  static bundle(schemas: object[], options?: ValidatorOptions): string;
100
135
 
101
- /** Bundle multiple schemas into a self-contained JS module. No ata-validator import needed at runtime. */
102
- 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;
103
142
 
104
143
  /** Bundle multiple schemas with deduplicated shared templates. Smaller output than bundle(). */
105
144
  static bundleCompact(schemas: object[], options?: ValidatorOptions): string;
@@ -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
@@ -1093,13 +1150,17 @@ function genCode(schema, v, lines, ctx, knownType) {
1093
1150
  }
1094
1151
  }
1095
1152
  } else if (schema.$ref !== '#' && ctx.schemaMap) {
1096
- // 2. Cross-schema ref (exact match or relative URI)
1153
+ // 2. Cross-schema ref (exact match, relative URI, or JSON pointer fragment)
1097
1154
  let resolved = ctx.schemaMap.get(schema.$ref)
1098
1155
  if (!resolved && !schema.$ref.includes('://') && !schema.$ref.startsWith('#')) {
1099
1156
  for (const [id, s] of ctx.schemaMap) {
1100
1157
  if (id.endsWith('/' + schema.$ref)) { resolved = s; break }
1101
1158
  }
1102
1159
  }
1160
+ if (!resolved && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
1161
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
1162
+ if (r) resolved = r.schema
1163
+ }
1103
1164
  if (resolved) {
1104
1165
  if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1105
1166
  else {
@@ -2516,6 +2577,17 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2516
2577
  ctx.refStack.delete(schema.$ref)
2517
2578
  return
2518
2579
  }
2580
+ // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
2581
+ if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
2582
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
2583
+ if (r) {
2584
+ if (ctx.refStack.has(schema.$ref)) return
2585
+ ctx.refStack.add(schema.$ref)
2586
+ genCodeE(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
2587
+ ctx.refStack.delete(schema.$ref)
2588
+ return
2589
+ }
2590
+ }
2519
2591
  }
2520
2592
 
2521
2593
  // $dynamicRef — resolve via anchors map
@@ -3022,6 +3094,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3022
3094
  ctx.refStack.delete(schema.$ref)
3023
3095
  return
3024
3096
  }
3097
+ // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
3098
+ if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
3099
+ const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
3100
+ if (r) {
3101
+ if (ctx.refStack.has(schema.$ref)) return
3102
+ ctx.refStack.add(schema.$ref)
3103
+ genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
3104
+ ctx.refStack.delete(schema.$ref)
3105
+ return
3106
+ }
3107
+ }
3025
3108
  }
3026
3109
 
3027
3110
  // $dynamicRef — resolve via anchors map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
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",