ata-validator 0.9.2 → 0.10.0

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.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() {
@@ -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,203 @@
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
+ // Numeric constraint flags, packed into constraint.numFlags.
21
+ // Using bit flags means the validator does a cheap bitwise-and instead of
22
+ // five Number.isNaN() calls per numeric property when only one bound is set.
23
+ const F_MIN = 1;
24
+ const F_MAX = 2;
25
+ const F_EXCL_MIN = 4;
26
+ const F_EXCL_MAX = 8;
27
+ const F_MULT = 16;
28
+
29
+ // Build a constraint tuple for one primitive-typed property.
30
+ // All fields have the same layout so every constraint shares one hidden class.
31
+ function primConstraint(key, propSchema) {
32
+ const t = propSchema.type;
33
+ const hasEnum = Array.isArray(propSchema.enum);
34
+ const hasConst = propSchema.const !== undefined;
35
+ let numFlags = 0;
36
+ if (typeof propSchema.minimum === 'number') numFlags |= F_MIN;
37
+ if (typeof propSchema.maximum === 'number') numFlags |= F_MAX;
38
+ if (typeof propSchema.exclusiveMinimum === 'number') numFlags |= F_EXCL_MIN;
39
+ if (typeof propSchema.exclusiveMaximum === 'number') numFlags |= F_EXCL_MAX;
40
+ if (typeof propSchema.multipleOf === 'number') numFlags |= F_MULT;
41
+ return {
42
+ key,
43
+ typeMask: TYPE_MASK[t] | 0,
44
+ numFlags,
45
+ hasEnum,
46
+ hasConst,
47
+ enumSet: hasEnum ? new Set(propSchema.enum) : null,
48
+ constVal: hasConst ? propSchema.const : undefined,
49
+ minLen: typeof propSchema.minLength === 'number' ? propSchema.minLength : -1,
50
+ maxLen: typeof propSchema.maxLength === 'number' ? propSchema.maxLength : -1,
51
+ min: typeof propSchema.minimum === 'number' ? propSchema.minimum : 0,
52
+ max: typeof propSchema.maximum === 'number' ? propSchema.maximum : 0,
53
+ exclMin: typeof propSchema.exclusiveMinimum === 'number' ? propSchema.exclusiveMinimum : 0,
54
+ exclMax: typeof propSchema.exclusiveMaximum === 'number' ? propSchema.exclusiveMaximum : 0,
55
+ multipleOf: typeof propSchema.multipleOf === 'number' ? propSchema.multipleOf : 0,
56
+ };
57
+ }
58
+
59
+ function buildTier0Plan(schema) {
60
+ if (schema.type !== 'object') {
61
+ return {
62
+ isPrimitive: true,
63
+ constraints: [primConstraint('__root__', schema)],
64
+ requiredMask: 0,
65
+ additionalAllowed: true,
66
+ knownKeys: null,
67
+ };
68
+ }
69
+ const props = schema.properties || {};
70
+ const keys = Object.keys(props);
71
+ const required = schema.required ? new Set(schema.required) : null;
72
+ const constraints = new Array(keys.length);
73
+ const knownKeys = new Set();
74
+ let requiredMask = 0;
75
+ for (let i = 0; i < keys.length; i++) {
76
+ const k = keys[i];
77
+ constraints[i] = primConstraint(k, props[k]);
78
+ knownKeys.add(k);
79
+ if (required && required.has(k)) requiredMask |= (1 << i);
80
+ }
81
+ return {
82
+ isPrimitive: false,
83
+ constraints,
84
+ requiredMask,
85
+ additionalAllowed: schema.additionalProperties !== false,
86
+ knownKeys,
87
+ };
88
+ }
89
+
90
+ // checkPrimitive stays exported for Tier 1 reuse.
91
+ function checkPrimitive(c, v) {
92
+ const m = c.typeMask;
93
+ if (m === T_STRING) {
94
+ if (typeof v !== 'string') return false;
95
+ const minLen = c.minLen;
96
+ const maxLen = c.maxLen;
97
+ if (minLen >= 0 && v.length < minLen) return false;
98
+ if (maxLen >= 0 && v.length > maxLen) return false;
99
+ } else if (m === T_INTEGER) {
100
+ if (typeof v !== 'number' || !Number.isInteger(v)) return false;
101
+ const f = c.numFlags;
102
+ if (f !== 0) {
103
+ if ((f & F_MIN) && v < c.min) return false;
104
+ if ((f & F_MAX) && v > c.max) return false;
105
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
106
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
107
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
108
+ }
109
+ } else if (m === T_NUMBER) {
110
+ if (typeof v !== 'number') return false;
111
+ const f = c.numFlags;
112
+ if (f !== 0) {
113
+ if ((f & F_MIN) && v < c.min) return false;
114
+ if ((f & F_MAX) && v > c.max) return false;
115
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
116
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
117
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
118
+ }
119
+ } else if (m === T_BOOLEAN) {
120
+ if (typeof v !== 'boolean') return false;
121
+ } else {
122
+ return false;
123
+ }
124
+ if (c.hasEnum && !c.enumSet.has(v)) return false;
125
+ if (c.hasConst && v !== c.constVal) return false;
126
+ return true;
127
+ }
128
+
129
+ // Inlined object validator. Separating the primitive path removes a dead
130
+ // branch from the hot object path.
131
+ function tier0ValidateObject(plan, data) {
132
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return false;
133
+ const cs = plan.constraints;
134
+ const n = cs.length;
135
+ const reqMask = plan.requiredMask;
136
+ let seenMask = 0;
137
+ for (let i = 0; i < n; i++) {
138
+ const c = cs[i];
139
+ const v = data[c.key];
140
+ if (v === undefined) {
141
+ if (reqMask & (1 << i)) return false;
142
+ continue;
143
+ }
144
+ seenMask |= (1 << i);
145
+ // Inlined type + constraint check
146
+ const m = c.typeMask;
147
+ if (m === T_STRING) {
148
+ if (typeof v !== 'string') return false;
149
+ const minLen = c.minLen;
150
+ const maxLen = c.maxLen;
151
+ if (minLen >= 0 && v.length < minLen) return false;
152
+ if (maxLen >= 0 && v.length > maxLen) return false;
153
+ } else if (m === T_INTEGER) {
154
+ if (typeof v !== 'number' || !Number.isInteger(v)) return false;
155
+ const f = c.numFlags;
156
+ if (f !== 0) {
157
+ if ((f & F_MIN) && v < c.min) return false;
158
+ if ((f & F_MAX) && v > c.max) return false;
159
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
160
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
161
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
162
+ }
163
+ } else if (m === T_NUMBER) {
164
+ if (typeof v !== 'number') return false;
165
+ const f = c.numFlags;
166
+ if (f !== 0) {
167
+ if ((f & F_MIN) && v < c.min) return false;
168
+ if ((f & F_MAX) && v > c.max) return false;
169
+ if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
170
+ if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
171
+ if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
172
+ }
173
+ } else if (m === T_BOOLEAN) {
174
+ if (typeof v !== 'boolean') return false;
175
+ } else {
176
+ return false;
177
+ }
178
+ if (c.hasEnum && !c.enumSet.has(v)) return false;
179
+ if (c.hasConst && v !== c.constVal) return false;
180
+ }
181
+ if ((seenMask & reqMask) !== reqMask) return false;
182
+ if (!plan.additionalAllowed) {
183
+ const known = plan.knownKeys;
184
+ for (const k in data) {
185
+ if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
186
+ if (!known.has(k)) return false;
187
+ }
188
+ }
189
+ return true;
190
+ }
191
+
192
+ function tier0Validate(plan, data) {
193
+ if (plan.isPrimitive) return checkPrimitive(plan.constraints[0], data);
194
+ return tier0ValidateObject(plan, data);
195
+ }
196
+
197
+ module.exports = {
198
+ buildTier0Plan,
199
+ tier0Validate,
200
+ tier0ValidateObject,
201
+ checkPrimitive,
202
+ TYPE_MASK,
203
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
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",