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 +2 -2
- package/index.d.ts +58 -15
- package/index.js +16 -0
- package/lib/js-compiler.js +51 -21
- package/lib/shape-classifier.js +96 -0
- package/lib/tier0.js +211 -0
- package/package.json +1 -1
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-win32-x64/node-napi-v10.node +0 -0
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
|
-
**
|
|
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
|
-
- **
|
|
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
|
|
45
|
+
/** Add a schema to the registry for cross-schema $ref resolution */
|
|
34
46
|
addSchema(schema: object): void;
|
|
35
47
|
|
|
36
|
-
/** Validate data
|
|
48
|
+
/** Validate data, returns result with errors. Applies defaults, coerceTypes, removeAdditional. */
|
|
37
49
|
validate(data: unknown): ValidationResult;
|
|
38
50
|
|
|
39
|
-
/** Fast boolean check
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
75
|
+
/** Multi-core parallel NDJSON validation. Returns boolean per line. Requires native addon. */
|
|
61
76
|
isValidParallel(ndjsonBuffer: Buffer): boolean[];
|
|
62
77
|
|
|
63
|
-
/**
|
|
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
|
-
/**
|
|
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() {
|
package/lib/js-compiler.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
918
|
-
if (schema.maxLength !== undefined) conds.push(`_v
|
|
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}
|
|
1163
|
-
if (schema.maxLength !== undefined) lines.push(isStr ? `if(${v}
|
|
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
|
|
1180
|
-
ctx.
|
|
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 ?
|
|
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 ?
|
|
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
|
|
2457
|
-
ctx.
|
|
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
|
|
2543
|
-
ctx.
|
|
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
|
|
2574
|
-
ctx.
|
|
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 ?
|
|
2967
|
-
if (schema.maxLength !== undefined) { const c = isStr ?
|
|
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.
|
|
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",
|
|
Binary file
|
|
Binary file
|