ata-validator 0.9.3 → 0.10.1

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/README.md CHANGED
@@ -102,7 +102,7 @@ Self-recursive named functions for $dynamicRef, compile-time cross-schema resolu
102
102
 
103
103
  ### JSON Schema Test Suite
104
104
 
105
- **95.3%** pass rate (1170/1227) on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12). **95.3%** on [@exodus/schemasafe](https://github.com/ExodusMovement/schemasafe) test suite.
105
+ **98.5%** pass rate (1172/1190) on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12), excluding remote refs and vocabulary (intentionally unsupported). **95.3%** on [@exodus/schemasafe](https://github.com/ExodusMovement/schemasafe) test suite.
106
106
 
107
107
  ## When to use ata
108
108
 
@@ -135,7 +135,7 @@ Self-recursive named functions for $dynamicRef, compile-time cross-schema resolu
135
135
  - **Zero-copy paths**: Buffer and pre-padded input support - no unnecessary copies
136
136
  - **Defaults + coercion**: `default` values, `coerceTypes`, `removeAdditional` support
137
137
  - **C/C++ library**: Native API for non-Node.js environments
138
- - **96.9% spec compliant**: Draft 2020-12
138
+ - **98.5% spec compliant**: Draft 2020-12
139
139
 
140
140
  ## Installation
141
141
 
package/index.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface ValidationResult {
11
11
  errors: ValidationError[];
12
12
  }
13
13
 
14
+ export interface ValidateAndParseResult {
15
+ valid: boolean;
16
+ value: unknown;
17
+ errors: ValidationError[];
18
+ }
19
+
14
20
  export interface ValidatorOptions {
15
21
  coerceTypes?: boolean;
16
22
  removeAdditional?: boolean;
@@ -27,56 +33,93 @@ export interface StandardSchemaV1Props {
27
33
  | { issues: Array<{ message: string; path?: ReadonlyArray<{ key: PropertyKey }> }> };
28
34
  }
29
35
 
36
+ export interface StandaloneModule {
37
+ boolFn: (data: unknown) => boolean;
38
+ hybridFactory: (validResult: object, errFn: Function) => (data: unknown) => ValidationResult;
39
+ errFn: ((data: unknown, allErrors?: boolean) => ValidationResult) | null;
40
+ }
41
+
30
42
  export class Validator {
31
43
  constructor(schema: object | string, options?: ValidatorOptions);
32
44
 
33
- /** Add a schema to the validator */
45
+ /** Add a schema to the registry for cross-schema $ref resolution */
34
46
  addSchema(schema: object): void;
35
47
 
36
- /** Validate data returns result with errors. Applies defaults, coerceTypes, removeAdditional. */
48
+ /** Validate data, returns result with errors. Applies defaults, coerceTypes, removeAdditional. */
37
49
  validate(data: unknown): ValidationResult;
38
50
 
39
- /** Fast boolean check JS codegen, no error collection */
51
+ /** Fast boolean check via JS codegen or tier 0 interpreter. No error collection. */
40
52
  isValidObject(data: unknown): boolean;
41
53
 
42
- /** Validate JSON string simdjson fast path for large docs */
54
+ /** Validate a JSON string. Uses simdjson fast path for large documents. */
43
55
  validateJSON(jsonString: string): ValidationResult;
44
56
 
45
- /** Fast boolean check for JSON string */
57
+ /** Fast boolean check for a JSON string */
46
58
  isValidJSON(jsonString: string): boolean;
47
59
 
48
- /** Ultra-fast buffer validation via V8 CFunction zero NAPI overhead */
60
+ /** Parse JSON with simdjson + validate against schema. Returns parsed value and validation result. Requires native addon. */
61
+ validateAndParse(jsonString: string | Buffer): ValidateAndParseResult;
62
+
63
+ /** Ultra-fast buffer validation via native addon */
49
64
  isValid(input: Buffer | Uint8Array | string): boolean;
50
65
 
51
- /** Count valid documents in an NDJSON buffer */
66
+ /** Count valid documents in an NDJSON buffer. Requires native addon. */
52
67
  countValid(ndjsonBuf: Buffer | Uint8Array | string): number;
53
68
 
54
- /** Count valid documents from an array of buffers */
69
+ /** Validate an array of buffers, returns count of valid ones. Requires native addon. */
55
70
  batchIsValid(buffers: (Buffer | Uint8Array)[]): number;
56
71
 
57
- /** Zero-copy validation with pre-padded buffer */
72
+ /** Zero-copy validation with pre-padded buffer. Requires native addon. */
58
73
  isValidPrepadded(paddedBuffer: Buffer, jsonLength: number): boolean;
59
74
 
60
- /** Multi-core parallel NDJSON validation returns boolean per line */
75
+ /** Multi-core parallel NDJSON validation. Returns boolean per line. Requires native addon. */
61
76
  isValidParallel(ndjsonBuffer: Buffer): boolean[];
62
77
 
63
- /** Multi-core parallel NDJSON count returns number of valid items */
64
- countValid(ndjsonBuffer: Buffer): number;
65
-
66
- /** Single-thread NDJSON batch validation */
78
+ /** Single-thread NDJSON batch validation. Requires native addon. */
67
79
  isValidNDJSON(ndjsonBuffer: Buffer): boolean[];
68
80
 
69
- /** Standard Schema V1 interface compatible with Fastify, tRPC, TanStack, etc. */
81
+ /** Generate a standalone JS module string for zero-compile loading. Returns null if schema can't be standalone-compiled. */
82
+ toStandalone(): string | null;
83
+
84
+ /** Load a pre-compiled standalone module. Zero schema compilation at startup. */
85
+ static fromStandalone(mod: StandaloneModule, schema: object | string, options?: ValidatorOptions): Validator;
86
+
87
+ /** Bundle multiple schemas into a single JS module string. Load with Validator.loadBundle(). */
88
+ static bundle(schemas: object[], options?: ValidatorOptions): string;
89
+
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;
92
+
93
+ /** Bundle multiple schemas with deduplicated shared templates. Smaller output than bundle(). */
94
+ static bundleCompact(schemas: object[], options?: ValidatorOptions): string;
95
+
96
+ /** Load a bundle created by Validator.bundle(). Returns array of Validator instances. */
97
+ static loadBundle(mods: object[], schemas: object[], options?: ValidatorOptions): Validator[];
98
+
99
+ /** Standard Schema V1 interface, compatible with Fastify, tRPC, TanStack, etc. */
70
100
  readonly "~standard": StandardSchemaV1Props;
71
101
  }
72
102
 
103
+ /** One-shot validate: creates a Validator, validates data, returns result. */
73
104
  export function validate(
74
105
  schema: object | string,
75
106
  data: unknown
76
107
  ): ValidationResult;
77
108
 
109
+ /** Fast compile: returns a validate function directly. WeakMap cached, second call with same schema is near-zero cost. */
110
+ export function compile(
111
+ schema: object | string,
112
+ options?: ValidatorOptions
113
+ ): (data: unknown) => ValidationResult;
114
+
115
+ /** Parse JSON using simdjson (native addon) or JSON.parse (fallback). */
116
+ export function parseJSON(jsonString: string | Buffer): unknown;
117
+
118
+ /** Returns ata-validator version string. */
78
119
  export function version(): string;
79
120
 
121
+ /** Create a simdjson-compatible padded buffer from a JSON string. */
80
122
  export function createPaddedBuffer(jsonStr: string): { buffer: Buffer; length: number };
81
123
 
124
+ /** Required padding size for simdjson buffers. */
82
125
  export const SIMDJSON_PADDING: number;
package/index.js CHANGED
@@ -9,6 +9,8 @@ const {
9
9
  compileToJSCombined,
10
10
  } = require("./lib/js-compiler");
11
11
  const { normalizeDraft7 } = require("./lib/draft7");
12
+ const { classify } = require("./lib/shape-classifier");
13
+ const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
12
14
 
13
15
  // Extract default values from a schema tree. Returns a function that applies
14
16
  // defaults to an object in-place (mutates), or null if no defaults exist.
@@ -413,6 +415,20 @@ class Validator {
413
415
  enumerable: false,
414
416
  configurable: false,
415
417
  });
418
+
419
+ // Tier 0 fast path: override isValidObject with a direct bound validator.
420
+ // All other methods (validate, validateJSON, etc.) stay on the lazy stubs above.
421
+ // Tier 0/1 are boolean-only; error paths continue through codegen.
422
+ const _tier = classify(schemaObj);
423
+ if (_tier.tier === 0) {
424
+ const _plan = buildTier0Plan(schemaObj);
425
+ this.isValidObject = (data) => tier0Validate(_plan, data);
426
+ }
427
+
428
+ // Populate identity cache so repeated `new Validator(sameSchema)` short-circuits.
429
+ if (!opts && typeof schema === "object" && schema !== null) {
430
+ _identityCache.set(schema, this);
431
+ }
416
432
  }
417
433
 
418
434
  _ensureCompiled() {
@@ -4,6 +4,10 @@
4
4
  // Closure-based validator — no new Function() or eval().
5
5
  // Returns null if the schema is too complex for JS compilation.
6
6
 
7
+ // Count Unicode code points, not UTF-16 code units (surrogate pairs).
8
+ // JSON Schema: minLength/maxLength count characters per RFC 8259.
9
+ function _cpLen(s) { let n = 0; for (const _ of s) n++; return n; }
10
+
7
11
  // AJV-compatible error message templates (compile-time, not runtime)
8
12
  const AJV_MESSAGES = {
9
13
  type: (p) => `must be ${p.type}`,
@@ -271,11 +275,11 @@ function compileToJS(schema, defs, schemaMap) {
271
275
  // string
272
276
  if (schema.minLength !== undefined) {
273
277
  const min = schema.minLength
274
- checks.push((d) => typeof d !== 'string' || d.length >= min)
278
+ checks.push((d) => typeof d !== 'string' || _cpLen(d) >= min)
275
279
  }
276
280
  if (schema.maxLength !== undefined) {
277
281
  const max = schema.maxLength
278
- checks.push((d) => typeof d !== 'string' || d.length <= max)
282
+ checks.push((d) => typeof d !== 'string' || _cpLen(d) <= max)
279
283
  }
280
284
  if (schema.pattern) {
281
285
  try {
@@ -775,7 +779,7 @@ function compileToJSCodegen(schema, schemaMap) {
775
779
  }
776
780
  }
777
781
 
778
- const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
782
+ const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema }
779
783
  const lines = []
780
784
  genCode(schema, 'd', lines, ctx)
781
785
 
@@ -914,8 +918,8 @@ function tryGenCombined(schema, access, ctx) {
914
918
 
915
919
  if (t === 'string') {
916
920
  const conds = [`typeof _v!=='string'`]
917
- if (schema.minLength !== undefined) conds.push(`_v.length<${schema.minLength}`)
918
- if (schema.maxLength !== undefined) conds.push(`_v.length>${schema.maxLength}`)
921
+ if (schema.minLength !== undefined) conds.push(`_cpLen(_v)<${schema.minLength}`)
922
+ if (schema.maxLength !== undefined) conds.push(`_cpLen(_v)>${schema.maxLength}`)
919
923
  if (conds.length < 2 && !schema.pattern && !schema.format) return null // not worth combining
920
924
  // pattern and format need separate statements, fall back if present
921
925
  if (schema.pattern || schema.format) return null
@@ -956,6 +960,9 @@ function tryGenCombined(schema, access, ctx) {
956
960
  // 'string' / 'number' / 'integer' = we know the primitive type
957
961
  function genCode(schema, v, lines, ctx, knownType) {
958
962
  if (typeof schema !== 'object' || schema === null) return
963
+ if (!ctx.regExpMap) {
964
+ ctx.regExpMap = new Map();
965
+ }
959
966
 
960
967
  // $ref — guard against circular references
961
968
  // In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
@@ -1159,8 +1166,8 @@ function genCode(schema, v, lines, ctx, knownType) {
1159
1166
  if (schema.multipleOf !== undefined) lines.push(isNum ? `if(${v}%${schema.multipleOf}!==0)return false` : `if(typeof ${v}==='number'&&${v}%${schema.multipleOf}!==0)return false`)
1160
1167
 
1161
1168
  // string — skip type guard if known string
1162
- if (schema.minLength !== undefined) lines.push(isStr ? `if(${v}.length<${schema.minLength})return false` : `if(typeof ${v}==='string'&&${v}.length<${schema.minLength})return false`)
1163
- if (schema.maxLength !== undefined) lines.push(isStr ? `if(${v}.length>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&${v}.length>${schema.maxLength})return false`)
1169
+ if (schema.minLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})<${schema.minLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength})return false`)
1170
+ if (schema.maxLength !== undefined) lines.push(isStr ? `if(_cpLen(${v})>${schema.maxLength})return false` : `if(typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength})return false`)
1164
1171
 
1165
1172
  // array size — skip guard if known array
1166
1173
  if (schema.minItems !== undefined) lines.push(isArr ? `if(${v}.length<${schema.minItems})return false` : `if(Array.isArray(${v})&&${v}.length<${schema.minItems})return false`)
@@ -1176,8 +1183,13 @@ function genCode(schema, v, lines, ctx, knownType) {
1176
1183
  if (inlineCheck) {
1177
1184
  lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
1178
1185
  } else {
1179
- const ri = ctx.varCounter++
1180
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
1186
+ const pattern = JSON.stringify(schema.pattern);
1187
+ if (!ctx.regExpMap.has(pattern)) {
1188
+ const ri = ctx.varCounter++
1189
+ ctx.regExpMap.set(pattern, ri)
1190
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
1191
+ }
1192
+ const ri = ctx.regExpMap.get(pattern);
1181
1193
  lines.push(isStr ? `if(!_re${ri}.test(${v}))return false` : `if(typeof ${v}==='string'&&!_re${ri}.test(${v}))return false`)
1182
1194
  }
1183
1195
  }
@@ -2264,6 +2276,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
2264
2276
  }
2265
2277
 
2266
2278
  const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootSchema: schema }
2279
+ ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
2267
2280
  const lines = []
2268
2281
  genCodeE(schema, 'd', '', lines, ctx, '#')
2269
2282
  if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
@@ -2296,7 +2309,9 @@ function compileToJSCodegenWithErrors(schema, schemaMap) {
2296
2309
  function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2297
2310
  if (!schemaPrefix) schemaPrefix = '#'
2298
2311
  if (typeof schema !== 'object' || schema === null) return
2299
-
2312
+ if (!ctx.regExpMap) {
2313
+ ctx.regExpMap = new Map();
2314
+ }
2300
2315
  // $ref — resolve local and cross-schema refs
2301
2316
  if (schema.$ref) {
2302
2317
  // Self-reference "#" — no-op (permissive) to avoid infinite recursion
@@ -2440,11 +2455,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2440
2455
 
2441
2456
  // string
2442
2457
  if (schema.minLength !== undefined) {
2443
- const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`
2458
+ const c = isStr ? `_cpLen(${v})<${schema.minLength}` : `typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength}`
2444
2459
  lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`)
2445
2460
  }
2446
2461
  if (schema.maxLength !== undefined) {
2447
- const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`
2462
+ const c = isStr ? `_cpLen(${v})>${schema.maxLength}` : `typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength}`
2448
2463
  lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`)
2449
2464
  }
2450
2465
  if (schema.pattern) {
@@ -2453,8 +2468,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2453
2468
  const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
2454
2469
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
2455
2470
  } else {
2456
- const ri = ctx.varCounter++
2457
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(schema.pattern)})`)
2471
+ const pattern = JSON.stringify(schema.pattern);
2472
+ if (!ctx.regExpMap.has(pattern)) {
2473
+ const ri = ctx.varCounter++
2474
+ ctx.regExpMap.set(pattern, ri)
2475
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
2476
+ }
2477
+ const ri = ctx.regExpMap.get(pattern);
2458
2478
  const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
2459
2479
  lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, `'must match pattern "${schema.pattern}"'`)}}`)
2460
2480
  }
@@ -2539,8 +2559,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2539
2559
  // patternProperties
2540
2560
  if (schema.patternProperties) {
2541
2561
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
2542
- const ri = ctx.varCounter++
2543
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pat)})`)
2562
+ const pattern = JSON.stringify(pat);
2563
+ if (!ctx.regExpMap.has(pattern)) {
2564
+ const ri = ctx.varCounter++
2565
+ ctx.regExpMap.set(pattern, ri)
2566
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
2567
+ }
2568
+ const ri = ctx.regExpMap.get(pattern);
2544
2569
  const ki = ctx.varCounter++
2545
2570
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
2546
2571
  const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
@@ -2570,8 +2595,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2570
2595
  lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
2571
2596
  }
2572
2597
  if (pn.pattern) {
2573
- const ri = ctx.varCounter++
2574
- ctx.helperCode.push(`const _re${ri}=new RegExp(${JSON.stringify(pn.pattern)})`)
2598
+ const pattern = JSON.stringify(pn.pattern);
2599
+ if (!ctx.regExpMap.has(pattern)) {
2600
+ const ri = ctx.varCounter++
2601
+ ctx.regExpMap.set(pattern, ri)
2602
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
2603
+ }
2604
+ const ri = ctx.regExpMap.get(pattern);
2575
2605
  lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, `'must match pattern "${pn.pattern}"'`)}}`)
2576
2606
  }
2577
2607
  if (pn.const !== undefined) {
@@ -2746,7 +2776,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
2746
2776
  }
2747
2777
  }
2748
2778
 
2749
- const ctx = { varCounter: 0, helperCode: [], closureVars: [], closureVals: [],
2779
+ const ctx = { varCounter: 0, helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
2750
2780
  rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema }
2751
2781
  const lines = []
2752
2782
  genCodeC(schema, 'd', '', lines, ctx, '#')
@@ -2963,8 +2993,8 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2963
2993
  }
2964
2994
 
2965
2995
  // string — skip guard if known
2966
- if (schema.minLength !== undefined) { const c = isStr ? `${v}.length<${schema.minLength}` : `typeof ${v}==='string'&&${v}.length<${schema.minLength}`; lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`) }
2967
- if (schema.maxLength !== undefined) { const c = isStr ? `${v}.length>${schema.maxLength}` : `typeof ${v}==='string'&&${v}.length>${schema.maxLength}`; lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`) }
2996
+ if (schema.minLength !== undefined) { const c = isStr ? `_cpLen(${v})<${schema.minLength}` : `typeof ${v}==='string'&&_cpLen(${v})<${schema.minLength}`; lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${schema.minLength}}`, `'must NOT have fewer than ${schema.minLength} characters'`)}}`) }
2997
+ if (schema.maxLength !== undefined) { const c = isStr ? `_cpLen(${v})>${schema.maxLength}` : `typeof ${v}==='string'&&_cpLen(${v})>${schema.maxLength}`; lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${schema.maxLength}}`, `'must NOT have more than ${schema.maxLength} characters'`)}}`) }
2968
2998
  if (schema.pattern) {
2969
2999
  const inlineCheck = compilePatternInline(schema.pattern, v)
2970
3000
  if (inlineCheck) {
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ // Classifies a JSON Schema into one of three execution tiers:
4
+ // 0 - simple object or top-level primitive, fast-path validator
5
+ // 1 - nested objects/arrays, no composition, generic interpreter
6
+ // 2 - composition, $ref, dynamic, etc. -> existing codegen
7
+ //
8
+ // Tier 0/1 validators are BOOLEAN only. Error-returning paths stay on codegen.
9
+
10
+ const PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'boolean']);
11
+
12
+ // Meta keywords that are always safe to see at any node (annotations, no validation impact)
13
+ const META_KEYS = new Set([
14
+ '$schema', '$id', '$comment',
15
+ 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly',
16
+ ]);
17
+
18
+ const TIER0_OBJECT_ALLOWED = new Set([
19
+ 'type', 'properties', 'required', 'additionalProperties',
20
+ ...META_KEYS,
21
+ ]);
22
+
23
+ const TIER0_PRIMITIVE_ALLOWED = new Set([
24
+ 'type', 'enum', 'const',
25
+ 'minLength', 'maxLength',
26
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum',
27
+ 'multipleOf', 'format',
28
+ ...META_KEYS,
29
+ ]);
30
+
31
+ const MAX_TIER0_PROPS = 10;
32
+ const MAX_TIER0_ENUM = 256;
33
+
34
+ function isPrimitiveType(t) {
35
+ return typeof t === 'string' && PRIMITIVE_TYPES.has(t);
36
+ }
37
+
38
+ function isPrimitiveEnumValue(v) {
39
+ const t = typeof v;
40
+ return v === null || t === 'string' || t === 'number' || t === 'boolean';
41
+ }
42
+
43
+ function isTier0Primitive(schema) {
44
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
45
+ if (!isPrimitiveType(schema.type)) return false;
46
+ for (const k of Object.keys(schema)) {
47
+ if (!TIER0_PRIMITIVE_ALLOWED.has(k)) return false;
48
+ }
49
+ if (schema.enum !== undefined) {
50
+ if (!Array.isArray(schema.enum)) return false;
51
+ if (schema.enum.length === 0 || schema.enum.length > MAX_TIER0_ENUM) return false;
52
+ for (const v of schema.enum) {
53
+ if (!isPrimitiveEnumValue(v)) return false;
54
+ }
55
+ }
56
+ if (schema.const !== undefined && !isPrimitiveEnumValue(schema.const)) return false;
57
+ return true;
58
+ }
59
+
60
+ function isTier0Object(schema) {
61
+ if (schema.type !== 'object') return false;
62
+ for (const k of Object.keys(schema)) {
63
+ if (!TIER0_OBJECT_ALLOWED.has(k)) return false;
64
+ }
65
+ const ap = schema.additionalProperties;
66
+ if (ap !== undefined && ap !== true && ap !== false) return false;
67
+ if (schema.required !== undefined) {
68
+ if (!Array.isArray(schema.required)) return false;
69
+ for (const r of schema.required) if (typeof r !== 'string') return false;
70
+ }
71
+ const props = schema.properties;
72
+ if (props === undefined) return true;
73
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) return false;
74
+ const keys = Object.keys(props);
75
+ if (keys.length > MAX_TIER0_PROPS) return false;
76
+ for (const k of keys) {
77
+ if (!isTier0Primitive(props[k])) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ function classify(schema) {
83
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
84
+ return { tier: 2, plan: null };
85
+ }
86
+ if (isTier0Primitive(schema)) return { tier: 0, plan: null };
87
+ if (isTier0Object(schema)) return { tier: 0, plan: null };
88
+ return { tier: 2, plan: null };
89
+ }
90
+
91
+ module.exports = {
92
+ classify,
93
+ MAX_TIER0_PROPS,
94
+ MAX_TIER0_ENUM,
95
+ PRIMITIVE_TYPES,
96
+ };
package/lib/tier0.js ADDED
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ // Tier 0 fast path: shared parametric validator for simple schemas.
4
+ // All tier-0 Validators call the same tier0Validate() function;
5
+ // the per-instance difference is the plan object.
6
+ // V8 sees one function with monomorphic hidden classes and JIT-compiles it once.
7
+
8
+ const TYPE_MASK = {
9
+ string: 1,
10
+ number: 2,
11
+ integer: 4,
12
+ boolean: 8,
13
+ };
14
+
15
+ const T_STRING = TYPE_MASK.string;
16
+ const T_NUMBER = TYPE_MASK.number;
17
+ const T_INTEGER = TYPE_MASK.integer;
18
+ const T_BOOLEAN = TYPE_MASK.boolean;
19
+
20
+ // Count Unicode code points, not UTF-16 code units.
21
+ // JSON Schema spec: minLength/maxLength count characters (RFC 8259 = code points).
22
+ function codePointLength(s) {
23
+ let n = 0;
24
+ for (const _ of s) n++;
25
+ return n;
26
+ }
27
+
28
+ // Numeric constraint flags, packed into constraint.numFlags.
29
+ // Using bit flags means the validator does a cheap bitwise-and instead of
30
+ // five Number.isNaN() calls per numeric property when only one bound is set.
31
+ const F_MIN = 1;
32
+ const F_MAX = 2;
33
+ const F_EXCL_MIN = 4;
34
+ const F_EXCL_MAX = 8;
35
+ const F_MULT = 16;
36
+
37
+ // Build a constraint tuple for one primitive-typed property.
38
+ // All fields have the same layout so every constraint shares one hidden class.
39
+ function primConstraint(key, propSchema) {
40
+ const t = propSchema.type;
41
+ const hasEnum = Array.isArray(propSchema.enum);
42
+ const hasConst = propSchema.const !== undefined;
43
+ let numFlags = 0;
44
+ if (typeof propSchema.minimum === 'number') numFlags |= F_MIN;
45
+ if (typeof propSchema.maximum === 'number') numFlags |= F_MAX;
46
+ if (typeof propSchema.exclusiveMinimum === 'number') numFlags |= F_EXCL_MIN;
47
+ if (typeof propSchema.exclusiveMaximum === 'number') numFlags |= F_EXCL_MAX;
48
+ if (typeof propSchema.multipleOf === 'number') numFlags |= F_MULT;
49
+ return {
50
+ key,
51
+ typeMask: TYPE_MASK[t] | 0,
52
+ numFlags,
53
+ hasEnum,
54
+ hasConst,
55
+ enumSet: hasEnum ? new Set(propSchema.enum) : null,
56
+ constVal: hasConst ? propSchema.const : undefined,
57
+ minLen: typeof propSchema.minLength === 'number' ? propSchema.minLength : -1,
58
+ maxLen: typeof propSchema.maxLength === 'number' ? propSchema.maxLength : -1,
59
+ min: typeof propSchema.minimum === 'number' ? propSchema.minimum : 0,
60
+ max: typeof propSchema.maximum === 'number' ? propSchema.maximum : 0,
61
+ exclMin: typeof propSchema.exclusiveMinimum === 'number' ? propSchema.exclusiveMinimum : 0,
62
+ exclMax: typeof propSchema.exclusiveMaximum === 'number' ? propSchema.exclusiveMaximum : 0,
63
+ multipleOf: typeof propSchema.multipleOf === 'number' ? propSchema.multipleOf : 0,
64
+ };
65
+ }
66
+
67
+ function buildTier0Plan(schema) {
68
+ if (schema.type !== 'object') {
69
+ return {
70
+ isPrimitive: true,
71
+ constraints: [primConstraint('__root__', schema)],
72
+ requiredMask: 0,
73
+ additionalAllowed: true,
74
+ knownKeys: null,
75
+ };
76
+ }
77
+ const props = schema.properties || {};
78
+ const keys = Object.keys(props);
79
+ const required = schema.required ? new Set(schema.required) : null;
80
+ const constraints = new Array(keys.length);
81
+ const knownKeys = new Set();
82
+ let requiredMask = 0;
83
+ for (let i = 0; i < keys.length; i++) {
84
+ const k = keys[i];
85
+ constraints[i] = primConstraint(k, props[k]);
86
+ knownKeys.add(k);
87
+ if (required && required.has(k)) requiredMask |= (1 << i);
88
+ }
89
+ return {
90
+ isPrimitive: false,
91
+ constraints,
92
+ requiredMask,
93
+ additionalAllowed: schema.additionalProperties !== false,
94
+ knownKeys,
95
+ };
96
+ }
97
+
98
+ // checkPrimitive stays exported for Tier 1 reuse.
99
+ function checkPrimitive(c, v) {
100
+ const m = c.typeMask;
101
+ if (m === T_STRING) {
102
+ if (typeof v !== 'string') return false;
103
+ const minLen = c.minLen;
104
+ const maxLen = c.maxLen;
105
+ if (minLen >= 0 && codePointLength(v) < minLen) return false;
106
+ if (maxLen >= 0 && codePointLength(v) > maxLen) return false;
107
+ } else if (m === T_INTEGER) {
108
+ if (typeof v !== 'number' || !Number.isInteger(v)) return false;
109
+ const f = c.numFlags;
110
+ if (f !== 0) {
111
+ if ((f & F_MIN) && v < c.min) return false;
112
+ if ((f & F_MAX) && v > c.max) return false;
113
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
114
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
115
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
116
+ }
117
+ } else if (m === T_NUMBER) {
118
+ if (typeof v !== 'number') return false;
119
+ const f = c.numFlags;
120
+ if (f !== 0) {
121
+ if ((f & F_MIN) && v < c.min) return false;
122
+ if ((f & F_MAX) && v > c.max) return false;
123
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
124
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
125
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
126
+ }
127
+ } else if (m === T_BOOLEAN) {
128
+ if (typeof v !== 'boolean') return false;
129
+ } else {
130
+ return false;
131
+ }
132
+ if (c.hasEnum && !c.enumSet.has(v)) return false;
133
+ if (c.hasConst && v !== c.constVal) return false;
134
+ return true;
135
+ }
136
+
137
+ // Inlined object validator. Separating the primitive path removes a dead
138
+ // branch from the hot object path.
139
+ function tier0ValidateObject(plan, data) {
140
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
141
+ const cs = plan.constraints;
142
+ const n = cs.length;
143
+ const reqMask = plan.requiredMask;
144
+ let seenMask = 0;
145
+ for (let i = 0; i < n; i++) {
146
+ const c = cs[i];
147
+ const v = data[c.key];
148
+ if (v === undefined) {
149
+ if (reqMask & (1 << i)) return false;
150
+ continue;
151
+ }
152
+ seenMask |= (1 << i);
153
+ // Inlined type + constraint check
154
+ const m = c.typeMask;
155
+ if (m === T_STRING) {
156
+ if (typeof v !== 'string') return false;
157
+ const minLen = c.minLen;
158
+ const maxLen = c.maxLen;
159
+ if (minLen >= 0 && v.length < minLen) return false;
160
+ if (maxLen >= 0 && v.length > maxLen) return false;
161
+ } else if (m === T_INTEGER) {
162
+ if (typeof v !== 'number' || !Number.isInteger(v)) return false;
163
+ const f = c.numFlags;
164
+ if (f !== 0) {
165
+ if ((f & F_MIN) && v < c.min) return false;
166
+ if ((f & F_MAX) && v > c.max) return false;
167
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
168
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
169
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
170
+ }
171
+ } else if (m === T_NUMBER) {
172
+ if (typeof v !== 'number') return false;
173
+ const f = c.numFlags;
174
+ if (f !== 0) {
175
+ if ((f & F_MIN) && v < c.min) return false;
176
+ if ((f & F_MAX) && v > c.max) return false;
177
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
178
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
179
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
180
+ }
181
+ } else if (m === T_BOOLEAN) {
182
+ if (typeof v !== 'boolean') return false;
183
+ } else {
184
+ return false;
185
+ }
186
+ if (c.hasEnum && !c.enumSet.has(v)) return false;
187
+ if (c.hasConst && v !== c.constVal) return false;
188
+ }
189
+ if ((seenMask & reqMask) !== reqMask) return false;
190
+ if (!plan.additionalAllowed) {
191
+ const known = plan.knownKeys;
192
+ for (const k in data) {
193
+ if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
194
+ if (!known.has(k)) return false;
195
+ }
196
+ }
197
+ return true;
198
+ }
199
+
200
+ function tier0Validate(plan, data) {
201
+ if (plan.isPrimitive) return checkPrimitive(plan.constraints[0], data);
202
+ return tier0ValidateObject(plan, data);
203
+ }
204
+
205
+ module.exports = {
206
+ buildTier0Plan,
207
+ tier0Validate,
208
+ tier0ValidateObject,
209
+ checkPrimitive,
210
+ TYPE_MASK,
211
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.9.3",
3
+ "version": "0.10.1",
4
4
  "description": "Ultra-fast JSON Schema validator. 4.7x faster validation, 1,800x 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",