ata-validator 0.4.10 → 0.4.12
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 +43 -21
- package/index.js +405 -186
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,16 +10,16 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
10
10
|
|
|
11
11
|
| Scenario | ata | ajv | |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| **validate(obj)** valid |
|
|
14
|
-
| **validate(obj)** invalid |
|
|
13
|
+
| **validate(obj)** valid | 68M ops/sec | 8M ops/sec | **ata 8.5x faster** |
|
|
14
|
+
| **validate(obj)** invalid | 17M ops/sec | 8M ops/sec | **ata 2.1x faster** |
|
|
15
15
|
| **isValidObject(obj)** | 15.4M ops/sec | 9.2M ops/sec | **ata 1.7x faster** |
|
|
16
|
-
| **validateJSON(str)** valid |
|
|
17
|
-
| **validateJSON(str)** invalid | 2.
|
|
16
|
+
| **validateJSON(str)** valid | 3.0M ops/sec | 1.9M ops/sec | **ata 1.6x faster** |
|
|
17
|
+
| **validateJSON(str)** invalid | 2.7M ops/sec | 2.3M ops/sec | **ata 1.2x faster** |
|
|
18
18
|
| **Schema compilation** | 113K ops/sec | 818 ops/sec | **ata 138x faster** |
|
|
19
19
|
|
|
20
20
|
> validate(obj) numbers are isolated single-schema benchmarks. Multi-schema benchmark overhead reduces throughput; real-world numbers depend on workload.
|
|
21
21
|
|
|
22
|
-
### Large Data
|
|
22
|
+
### Large Data - JS Object Validation
|
|
23
23
|
|
|
24
24
|
| Size | ata | ajv | |
|
|
25
25
|
|---|---|---|---|
|
|
@@ -35,18 +35,19 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
35
35
|
| **ReDoS protection** (`^(a+)+$`) | 0.3ms | 765ms | **ata immune (RE2)** |
|
|
36
36
|
| **Batch NDJSON** (10K items, multi-core) | 13.4M/sec | 5.1M/sec | **ata 2.6x faster** |
|
|
37
37
|
| **Fastify HTTP** (100 users POST) | 24.6K req/sec | 22.6K req/sec | **ata 9% faster** |
|
|
38
|
+
| **Fastify startup** (500 routes) | 46ms | 77ms (standalone) | **ata 1.7x faster** |
|
|
38
39
|
|
|
39
|
-
>
|
|
40
|
+
> Isolated single-schema benchmarks. Results vary by workload and hardware.
|
|
40
41
|
|
|
41
42
|
### How it works
|
|
42
43
|
|
|
43
|
-
**Hybrid validator**: ata compiles schemas into monolithic JS functions identical to the boolean fast path, but returning `VALID_RESULT` on success and calling the error collector on failure. V8 TurboFan optimizes it identically to a pure boolean function
|
|
44
|
+
**Hybrid validator**: ata compiles schemas into monolithic JS functions identical to the boolean fast path, but returning `VALID_RESULT` on success and calling the error collector on failure. V8 TurboFan optimizes it identically to a pure boolean function - error code is dead code on the valid path. No try/catch (3.3x V8 deopt), no lazy arrays, no double-pass.
|
|
44
45
|
|
|
45
46
|
**JS codegen**: Schemas are compiled to monolithic JS functions (like ajv). Supported keywords: `type`, `required`, `properties`, `items`, `enum`, `const`, `allOf`, `anyOf`, `oneOf`, `not`, `if/then/else`, `uniqueItems`, `contains`, `prefixItems`, `additionalProperties`, `dependentRequired`, `$ref` (local), `minimum/maximum`, `minLength/maxLength`, `pattern`, `format`.
|
|
46
47
|
|
|
47
48
|
**V8 TurboFan optimizations**: Destructuring batch reads, `undefined` checks instead of `in` operator, context-aware type guard elimination, property hoisting to local variables, tiered uniqueItems (nested loop for small arrays).
|
|
48
49
|
|
|
49
|
-
**Adaptive simdjson**: For large documents (>8KB) with selective schemas, simdjson On Demand seeks only the needed fields
|
|
50
|
+
**Adaptive simdjson**: For large documents (>8KB) with selective schemas, simdjson On Demand seeks only the needed fields - skipping irrelevant data at GB/s speeds.
|
|
50
51
|
|
|
51
52
|
### JSON Schema Test Suite
|
|
52
53
|
|
|
@@ -54,27 +55,27 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
54
55
|
|
|
55
56
|
## When to use ata
|
|
56
57
|
|
|
57
|
-
- **
|
|
58
|
-
- **Serverless / cold starts**
|
|
59
|
-
- **Security-sensitive apps**
|
|
60
|
-
- **Batch/streaming validation**
|
|
61
|
-
- **Standard Schema V1**
|
|
62
|
-
- **C/C++ embedding**
|
|
58
|
+
- **High-throughput `validate(obj)`** - 68M ops/sec valid, 17M ops/sec invalid
|
|
59
|
+
- **Serverless / cold starts** - 12.5x faster schema compilation
|
|
60
|
+
- **Security-sensitive apps** - RE2 regex, immune to ReDoS attacks
|
|
61
|
+
- **Batch/streaming validation** - NDJSON log processing, data pipelines (2.6x faster)
|
|
62
|
+
- **Standard Schema V1** - native support for Fastify v5, tRPC, TanStack
|
|
63
|
+
- **C/C++ embedding** - native library, no JS runtime needed
|
|
63
64
|
|
|
64
65
|
## When to use ajv
|
|
65
66
|
|
|
66
|
-
- **Schemas with `patternProperties`, `dependentSchemas`**
|
|
67
|
-
- **100% spec compliance needed**
|
|
67
|
+
- **Schemas with `patternProperties`, `dependentSchemas`** - these bypass JS codegen and hit the slower NAPI path
|
|
68
|
+
- **100% spec compliance needed** - ajv covers more edge cases (ata: 98.4%)
|
|
68
69
|
|
|
69
70
|
## Features
|
|
70
71
|
|
|
71
|
-
- **Hybrid validator**:
|
|
72
|
-
- **Multi-core**: Parallel validation across all CPU cores
|
|
72
|
+
- **Hybrid validator**: 68M ops/sec - same function body as boolean check, returns result or calls error collector. No try/catch, no double pass
|
|
73
|
+
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
|
73
74
|
- **simdjson**: SIMD-accelerated JSON parsing at GB/s speeds, adaptive On Demand for large docs
|
|
74
75
|
- **RE2 regex**: Linear-time guarantees, immune to ReDoS attacks (2391x faster on pathological input)
|
|
75
76
|
- **V8-optimized codegen**: Destructuring batch reads, type guard elimination, property hoisting
|
|
76
77
|
- **Standard Schema V1**: Compatible with Fastify, tRPC, TanStack, Drizzle
|
|
77
|
-
- **Zero-copy paths**: Buffer and pre-padded input support
|
|
78
|
+
- **Zero-copy paths**: Buffer and pre-padded input support - no unnecessary copies
|
|
78
79
|
- **Defaults + coercion**: `default` values, `coerceTypes`, `removeAdditional` support
|
|
79
80
|
- **C/C++ library**: Native API for non-Node.js environments
|
|
80
81
|
- **98.4% spec compliant**: Draft 2020-12
|
|
@@ -103,7 +104,7 @@ const v = new Validator({
|
|
|
103
104
|
required: ['name', 'email']
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
// Fast boolean check
|
|
107
|
+
// Fast boolean check - JS codegen, 68M ops/sec
|
|
107
108
|
v.isValidObject({ name: 'Mert', email: 'mert@example.com', age: 26 }); // true
|
|
108
109
|
|
|
109
110
|
// Full validation with error details + defaults applied
|
|
@@ -117,7 +118,7 @@ v.isValidJSON('{"name": "Mert", "email": "mert@example.com"}'); // true
|
|
|
117
118
|
// Buffer input (zero-copy, raw NAPI)
|
|
118
119
|
v.isValid(Buffer.from('{"name": "Mert", "email": "mert@example.com"}'));
|
|
119
120
|
|
|
120
|
-
// Parallel batch
|
|
121
|
+
// Parallel batch - multi-core, NDJSON, 13.4M items/sec
|
|
121
122
|
const ndjson = Buffer.from(lines.join('\n'));
|
|
122
123
|
v.isValidParallel(ndjson); // bool[]
|
|
123
124
|
v.countValid(ndjson); // number
|
|
@@ -132,6 +133,27 @@ const v = new Validator(schema, {
|
|
|
132
133
|
});
|
|
133
134
|
```
|
|
134
135
|
|
|
136
|
+
### Standalone Pre-compilation
|
|
137
|
+
|
|
138
|
+
Pre-compile schemas to JS files for near-zero startup. No native addon needed at runtime.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
const fs = require('fs');
|
|
142
|
+
|
|
143
|
+
// Build phase (once)
|
|
144
|
+
const v = new Validator(schema);
|
|
145
|
+
fs.writeFileSync('./compiled.js', v.toStandalone());
|
|
146
|
+
|
|
147
|
+
// Read phase (every startup) - 0.6μs per schema, pure JS
|
|
148
|
+
const v2 = Validator.fromStandalone(require('./compiled.js'), schema);
|
|
149
|
+
|
|
150
|
+
// Bundle multiple schemas - deduplicated, single file
|
|
151
|
+
fs.writeFileSync('./bundle.js', Validator.bundleCompact(schemas));
|
|
152
|
+
const validators = Validator.loadBundle(require('./bundle.js'), schemas);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Fastify startup (500 routes): ajv standalone 77ms → ata standalone 46ms (1.7x faster)**
|
|
156
|
+
|
|
135
157
|
### Standard Schema V1
|
|
136
158
|
|
|
137
159
|
```javascript
|
package/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const native = require("node-gyp-build")(__dirname);
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
compileToJS,
|
|
4
|
+
compileToJSCodegen,
|
|
5
|
+
compileToJSCodegenWithErrors,
|
|
6
|
+
compileToJSCombined,
|
|
7
|
+
} = require("./lib/js-compiler");
|
|
3
8
|
|
|
4
9
|
// Extract default values from a schema tree. Returns a function that applies
|
|
5
10
|
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
6
11
|
function buildDefaultsApplier(schema) {
|
|
7
|
-
if (typeof schema !==
|
|
12
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
8
13
|
const actions = [];
|
|
9
14
|
collectDefaults(schema, actions);
|
|
10
15
|
if (actions.length === 0) return null;
|
|
@@ -14,17 +19,19 @@ function buildDefaultsApplier(schema) {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function collectDefaults(schema, actions, path) {
|
|
17
|
-
if (typeof schema !==
|
|
22
|
+
if (typeof schema !== "object" || schema === null) return;
|
|
18
23
|
const props = schema.properties;
|
|
19
24
|
if (!props) return;
|
|
20
25
|
for (const [key, prop] of Object.entries(props)) {
|
|
21
|
-
if (prop && typeof prop ===
|
|
26
|
+
if (prop && typeof prop === "object" && prop.default !== undefined) {
|
|
22
27
|
const defaultVal = prop.default;
|
|
23
28
|
if (!path) {
|
|
24
29
|
actions.push((data) => {
|
|
25
|
-
if (typeof data ===
|
|
26
|
-
data[key] =
|
|
27
|
-
|
|
30
|
+
if (typeof data === "object" && data !== null && !(key in data)) {
|
|
31
|
+
data[key] =
|
|
32
|
+
typeof defaultVal === "object" && defaultVal !== null
|
|
33
|
+
? JSON.parse(JSON.stringify(defaultVal))
|
|
34
|
+
: defaultVal;
|
|
28
35
|
}
|
|
29
36
|
});
|
|
30
37
|
} else {
|
|
@@ -32,18 +39,24 @@ function collectDefaults(schema, actions, path) {
|
|
|
32
39
|
actions.push((data) => {
|
|
33
40
|
let target = data;
|
|
34
41
|
for (let j = 0; j < parentPath.length; j++) {
|
|
35
|
-
if (typeof target !==
|
|
42
|
+
if (typeof target !== "object" || target === null) return;
|
|
36
43
|
target = target[parentPath[j]];
|
|
37
44
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
if (
|
|
46
|
+
typeof target === "object" &&
|
|
47
|
+
target !== null &&
|
|
48
|
+
!(key in target)
|
|
49
|
+
) {
|
|
50
|
+
target[key] =
|
|
51
|
+
typeof defaultVal === "object" && defaultVal !== null
|
|
52
|
+
? JSON.parse(JSON.stringify(defaultVal))
|
|
53
|
+
: defaultVal;
|
|
41
54
|
}
|
|
42
55
|
});
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
// Recurse into nested object schemas
|
|
46
|
-
if (prop && typeof prop ===
|
|
59
|
+
if (prop && typeof prop === "object" && prop.properties) {
|
|
47
60
|
collectDefaults(prop, actions, (path || []).concat(key));
|
|
48
61
|
}
|
|
49
62
|
}
|
|
@@ -52,7 +65,7 @@ function collectDefaults(schema, actions, path) {
|
|
|
52
65
|
// Build a function that coerces property values to match schema types in-place.
|
|
53
66
|
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
54
67
|
function buildCoercer(schema) {
|
|
55
|
-
if (typeof schema !==
|
|
68
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
56
69
|
const actions = [];
|
|
57
70
|
collectCoercions(schema, actions);
|
|
58
71
|
if (actions.length === 0) return null;
|
|
@@ -62,11 +75,11 @@ function buildCoercer(schema) {
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
function collectCoercions(schema, actions, path) {
|
|
65
|
-
if (typeof schema !==
|
|
78
|
+
if (typeof schema !== "object" || schema === null) return;
|
|
66
79
|
const props = schema.properties;
|
|
67
80
|
if (!props) return;
|
|
68
81
|
for (const [key, prop] of Object.entries(props)) {
|
|
69
|
-
if (!prop || typeof prop !==
|
|
82
|
+
if (!prop || typeof prop !== "object" || !prop.type) continue;
|
|
70
83
|
const targetType = Array.isArray(prop.type) ? null : prop.type;
|
|
71
84
|
if (!targetType) continue;
|
|
72
85
|
|
|
@@ -75,7 +88,7 @@ function collectCoercions(schema, actions, path) {
|
|
|
75
88
|
|
|
76
89
|
if (!path) {
|
|
77
90
|
actions.push((data) => {
|
|
78
|
-
if (typeof data ===
|
|
91
|
+
if (typeof data === "object" && data !== null && key in data) {
|
|
79
92
|
const coerced = coerce(data[key]);
|
|
80
93
|
if (coerced !== undefined) data[key] = coerced;
|
|
81
94
|
}
|
|
@@ -85,10 +98,10 @@ function collectCoercions(schema, actions, path) {
|
|
|
85
98
|
actions.push((data) => {
|
|
86
99
|
let target = data;
|
|
87
100
|
for (let j = 0; j < parentPath.length; j++) {
|
|
88
|
-
if (typeof target !==
|
|
101
|
+
if (typeof target !== "object" || target === null) return;
|
|
89
102
|
target = target[parentPath[j]];
|
|
90
103
|
}
|
|
91
|
-
if (typeof target ===
|
|
104
|
+
if (typeof target === "object" && target !== null && key in target) {
|
|
92
105
|
const coerced = coerce(target[key]);
|
|
93
106
|
if (coerced !== undefined) target[key] = coerced;
|
|
94
107
|
}
|
|
@@ -104,29 +117,40 @@ function collectCoercions(schema, actions, path) {
|
|
|
104
117
|
|
|
105
118
|
function buildSingleCoercion(targetType) {
|
|
106
119
|
switch (targetType) {
|
|
107
|
-
case
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
case
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
case "number":
|
|
121
|
+
return (v) => {
|
|
122
|
+
if (typeof v === "string") {
|
|
123
|
+
const n = Number(v);
|
|
124
|
+
if (v !== "" && !isNaN(n)) return n;
|
|
125
|
+
}
|
|
126
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
127
|
+
};
|
|
128
|
+
case "integer":
|
|
129
|
+
return (v) => {
|
|
130
|
+
if (typeof v === "string") {
|
|
131
|
+
const n = Number(v);
|
|
132
|
+
if (v !== "" && Number.isInteger(n)) return n;
|
|
133
|
+
}
|
|
134
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
135
|
+
};
|
|
136
|
+
case "string":
|
|
137
|
+
return (v) => {
|
|
138
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
139
|
+
};
|
|
140
|
+
case "boolean":
|
|
141
|
+
return (v) => {
|
|
142
|
+
if (v === "true" || v === "1") return true;
|
|
143
|
+
if (v === "false" || v === "0") return false;
|
|
144
|
+
};
|
|
145
|
+
default:
|
|
146
|
+
return null;
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
// Build a function that removes properties not defined in schema.properties.
|
|
127
151
|
// Walks nested objects recursively.
|
|
128
152
|
function buildRemover(schema) {
|
|
129
|
-
if (typeof schema !==
|
|
153
|
+
if (typeof schema !== "object" || schema === null) return null;
|
|
130
154
|
const actions = [];
|
|
131
155
|
collectRemovals(schema, actions);
|
|
132
156
|
if (actions.length === 0) return null;
|
|
@@ -136,14 +160,16 @@ function buildRemover(schema) {
|
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
function collectRemovals(schema, actions, path) {
|
|
139
|
-
if (typeof schema !==
|
|
163
|
+
if (typeof schema !== "object" || schema === null || !schema.properties)
|
|
164
|
+
return;
|
|
140
165
|
|
|
141
166
|
// If this level has additionalProperties: false, add a removal action
|
|
142
167
|
if (schema.additionalProperties === false) {
|
|
143
168
|
const allowed = new Set(Object.keys(schema.properties));
|
|
144
169
|
if (!path) {
|
|
145
170
|
actions.push((data) => {
|
|
146
|
-
if (typeof data !==
|
|
171
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
172
|
+
return;
|
|
147
173
|
const keys = Object.keys(data);
|
|
148
174
|
for (let i = 0; i < keys.length; i++) {
|
|
149
175
|
if (!allowed.has(keys[i])) delete data[keys[i]];
|
|
@@ -154,10 +180,15 @@ function collectRemovals(schema, actions, path) {
|
|
|
154
180
|
actions.push((data) => {
|
|
155
181
|
let target = data;
|
|
156
182
|
for (let j = 0; j < parentPath.length; j++) {
|
|
157
|
-
if (typeof target !==
|
|
183
|
+
if (typeof target !== "object" || target === null) return;
|
|
158
184
|
target = target[parentPath[j]];
|
|
159
185
|
}
|
|
160
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
typeof target !== "object" ||
|
|
188
|
+
target === null ||
|
|
189
|
+
Array.isArray(target)
|
|
190
|
+
)
|
|
191
|
+
return;
|
|
161
192
|
const keys = Object.keys(target);
|
|
162
193
|
for (let i = 0; i < keys.length; i++) {
|
|
163
194
|
if (!allowed.has(keys[i])) delete target[keys[i]];
|
|
@@ -168,7 +199,7 @@ function collectRemovals(schema, actions, path) {
|
|
|
168
199
|
|
|
169
200
|
// Always recurse into nested properties (they may have their own additionalProperties: false)
|
|
170
201
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
171
|
-
if (prop && typeof prop ===
|
|
202
|
+
if (prop && typeof prop === "object" && prop.properties) {
|
|
172
203
|
collectRemovals(prop, actions, (path || []).concat(key));
|
|
173
204
|
}
|
|
174
205
|
}
|
|
@@ -204,147 +235,277 @@ class Validator {
|
|
|
204
235
|
const options = opts || {};
|
|
205
236
|
const schemaStr =
|
|
206
237
|
typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
207
|
-
const compiled = new native.CompiledSchema(schemaStr);
|
|
208
|
-
this._compiled = compiled;
|
|
209
|
-
this._fastSlot = native.fastRegister(schemaStr);
|
|
210
|
-
|
|
211
|
-
// Pure JS fast path — no NAPI, runs in V8 JIT
|
|
212
|
-
// Set ATA_FORCE_NAPI=1 to disable JS codegen (for correctness testing)
|
|
213
238
|
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
239
|
+
|
|
240
|
+
this._schemaStr = schemaStr;
|
|
214
241
|
this._schemaObj = schemaObj;
|
|
242
|
+
this._options = options;
|
|
243
|
+
this._initialized = false;
|
|
244
|
+
this._nativeReady = false;
|
|
245
|
+
this._compiled = null;
|
|
246
|
+
this._fastSlot = -1;
|
|
247
|
+
this._jsFn = null;
|
|
248
|
+
this._preprocess = null;
|
|
249
|
+
this._applyDefaults = null;
|
|
250
|
+
|
|
251
|
+
// Lazy stubs: trigger compilation on first call, then re-dispatch
|
|
252
|
+
this.validate = (data) => {
|
|
253
|
+
this._ensureCompiled();
|
|
254
|
+
return this.validate(data);
|
|
255
|
+
};
|
|
256
|
+
this.isValidObject = (data) => {
|
|
257
|
+
this._ensureCompiled();
|
|
258
|
+
return this.isValidObject(data);
|
|
259
|
+
};
|
|
260
|
+
this.validateJSON = (jsonStr) => {
|
|
261
|
+
this._ensureCompiled();
|
|
262
|
+
return this.validateJSON(jsonStr);
|
|
263
|
+
};
|
|
264
|
+
this.isValidJSON = (jsonStr) => {
|
|
265
|
+
this._ensureCompiled();
|
|
266
|
+
return this.isValidJSON(jsonStr);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ~standard uses self.validate() -- works with lazy because it goes through
|
|
270
|
+
// the instance property which gets swapped after compilation
|
|
271
|
+
const self = this;
|
|
272
|
+
Object.defineProperty(this, "~standard", {
|
|
273
|
+
value: Object.freeze({
|
|
274
|
+
version: 1,
|
|
275
|
+
vendor: "ata-validator",
|
|
276
|
+
validate(value) {
|
|
277
|
+
const result = self.validate(value);
|
|
278
|
+
if (result.valid) {
|
|
279
|
+
return { value };
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
issues: result.errors.map((err) => ({
|
|
283
|
+
message: err.message,
|
|
284
|
+
path: parsePointerPath(err.path),
|
|
285
|
+
})),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
writable: false,
|
|
290
|
+
enumerable: false,
|
|
291
|
+
configurable: false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
_ensureCompiled() {
|
|
296
|
+
if (this._initialized) return;
|
|
297
|
+
this._initialized = true;
|
|
298
|
+
|
|
299
|
+
const schemaObj = this._schemaObj;
|
|
300
|
+
const options = this._options;
|
|
301
|
+
|
|
302
|
+
// Pure JS fast path -- no NAPI, runs in V8 JIT
|
|
303
|
+
// Set ATA_FORCE_NAPI=1 to disable JS codegen (for correctness testing)
|
|
215
304
|
const jsFn = process.env.ATA_FORCE_NAPI
|
|
216
305
|
? null
|
|
217
|
-
:
|
|
218
|
-
// Combined validator: single pass, validates + collects errors, all optimized
|
|
306
|
+
: compileToJSCodegen(schemaObj) || compileToJS(schemaObj);
|
|
219
307
|
const jsCombinedFn = process.env.ATA_FORCE_NAPI
|
|
220
308
|
? null
|
|
221
309
|
: compileToJSCombined(schemaObj, VALID_RESULT);
|
|
222
|
-
// Fallback error-collecting codegen (less optimized, for schemas combined can't handle)
|
|
223
310
|
const jsErrFn = process.env.ATA_FORCE_NAPI
|
|
224
311
|
? null
|
|
225
312
|
: compileToJSCodegenWithErrors(schemaObj);
|
|
226
313
|
this._jsFn = jsFn;
|
|
227
314
|
|
|
228
|
-
// Data mutators
|
|
315
|
+
// Data mutators -- applied in-place before validation
|
|
229
316
|
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
230
317
|
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
231
|
-
const applyRemove = options.removeAdditional
|
|
318
|
+
const applyRemove = options.removeAdditional
|
|
319
|
+
? buildRemover(schemaObj)
|
|
320
|
+
: null;
|
|
232
321
|
this._applyDefaults = applyDefaults;
|
|
233
322
|
|
|
234
323
|
// Combine all mutators into a single pre-validation step
|
|
235
324
|
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
236
|
-
const preprocess =
|
|
237
|
-
|
|
238
|
-
|
|
325
|
+
const preprocess =
|
|
326
|
+
mutators.length === 0
|
|
327
|
+
? null
|
|
328
|
+
: mutators.length === 1
|
|
329
|
+
? mutators[0]
|
|
330
|
+
: (data) => {
|
|
331
|
+
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
332
|
+
};
|
|
239
333
|
this._preprocess = preprocess;
|
|
240
334
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
335
|
+
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
336
|
+
const hasArrayTraversal =
|
|
337
|
+
schemaObj &&
|
|
338
|
+
(schemaObj.items ||
|
|
339
|
+
schemaObj.prefixItems ||
|
|
340
|
+
schemaObj.contains ||
|
|
341
|
+
(schemaObj.properties &&
|
|
342
|
+
Object.values(schemaObj.properties).some(
|
|
343
|
+
(p) => p && (p.items || p.prefixItems || p.contains),
|
|
344
|
+
)));
|
|
251
345
|
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
252
346
|
|
|
253
347
|
if (jsFn) {
|
|
254
|
-
// Best path: combined validator (single pass, lazy error array)
|
|
255
|
-
// Valid data: no array allocation, returns VALID_RESULT
|
|
256
|
-
// Invalid data: collects errors in one pass (no double validation)
|
|
257
|
-
// Fallback: jsFn + errFn for schemas combined can't handle
|
|
258
|
-
// errFn: JS error codegen or NAPI fallback. No try/catch (V8 3.3x deopt).
|
|
259
|
-
// jsErrFn tested at compile time — if it throws, don't use it.
|
|
260
348
|
let safeErrFn = null;
|
|
261
349
|
if (jsErrFn) {
|
|
262
|
-
try {
|
|
350
|
+
try {
|
|
351
|
+
jsErrFn({}, true);
|
|
352
|
+
safeErrFn = (d) => jsErrFn(d, true);
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
// errFn: use JS codegen if safe, else lazy-native fallback
|
|
356
|
+
const errFn =
|
|
357
|
+
safeErrFn ||
|
|
358
|
+
((d) => {
|
|
359
|
+
this._ensureNative();
|
|
360
|
+
return this._compiled.validate(d);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Best path: combined validator (single pass, validates + collects errors)
|
|
364
|
+
// Valid data: returns VALID_RESULT, no allocation
|
|
365
|
+
// Invalid data: collects errors in one pass (no double validation)
|
|
366
|
+
// Fallback: hybridFn or jsFn + errFn for schemas combined can't handle
|
|
367
|
+
// Test combined at compile time -- some schemas produce broken combined code
|
|
368
|
+
// Test combined at compile time -- some schemas (e.g. if/then/else)
|
|
369
|
+
// produce broken combined code that crashes on certain inputs.
|
|
370
|
+
// We probe with diverse data; if any throws, fall back to hybrid.
|
|
371
|
+
let safeCombinedFn = null;
|
|
372
|
+
if (jsCombinedFn) {
|
|
373
|
+
try {
|
|
374
|
+
const probe = {};
|
|
375
|
+
// Populate probe with one key per known property to trigger nested paths
|
|
376
|
+
if (schemaObj && schemaObj.properties) {
|
|
377
|
+
for (const k of Object.keys(schemaObj.properties)) probe[k] = "";
|
|
378
|
+
}
|
|
379
|
+
if (schemaObj && schemaObj.if && schemaObj.if.properties) {
|
|
380
|
+
for (const k of Object.keys(schemaObj.if.properties)) probe[k] = "";
|
|
381
|
+
}
|
|
382
|
+
jsCombinedFn(probe);
|
|
383
|
+
jsCombinedFn({});
|
|
384
|
+
jsCombinedFn(null);
|
|
385
|
+
jsCombinedFn(0);
|
|
386
|
+
safeCombinedFn = jsCombinedFn;
|
|
387
|
+
} catch {}
|
|
263
388
|
}
|
|
264
|
-
const errFn = safeErrFn || ((d) => compiled.validate(d));
|
|
265
389
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
390
|
+
if (safeCombinedFn) {
|
|
391
|
+
this.validate = preprocess
|
|
392
|
+
? (data) => {
|
|
393
|
+
preprocess(data);
|
|
394
|
+
return safeCombinedFn(data);
|
|
395
|
+
}
|
|
396
|
+
: safeCombinedFn;
|
|
397
|
+
} else {
|
|
398
|
+
const hybridFn = jsFn._hybridFactory
|
|
399
|
+
? jsFn._hybridFactory(VALID_RESULT, errFn)
|
|
400
|
+
: null;
|
|
401
|
+
this.validate = hybridFn
|
|
402
|
+
? preprocess
|
|
403
|
+
? (data) => {
|
|
404
|
+
preprocess(data);
|
|
405
|
+
return hybridFn(data);
|
|
406
|
+
}
|
|
407
|
+
: hybridFn
|
|
408
|
+
: preprocess
|
|
409
|
+
? (data) => {
|
|
410
|
+
preprocess(data);
|
|
411
|
+
return jsFn(data) ? VALID_RESULT : errFn(data);
|
|
412
|
+
}
|
|
413
|
+
: (data) => (jsFn(data) ? VALID_RESULT : errFn(data));
|
|
414
|
+
}
|
|
415
|
+
this.isValidObject = jsFn;
|
|
270
416
|
const hybridFn = jsFn._hybridFactory
|
|
271
417
|
? jsFn._hybridFactory(VALID_RESULT, errFn)
|
|
272
418
|
: null;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
: (
|
|
276
|
-
? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : errFn(data); }
|
|
277
|
-
: (data) => jsFn(data) ? VALID_RESULT : errFn(data));
|
|
278
|
-
this.isValidObject = jsFn;
|
|
279
|
-
const jsonValidateFn = hybridFn || ((obj) => jsFn(obj) ? VALID_RESULT : errFn(obj));
|
|
419
|
+
const jsonValidateFn = safeCombinedFn
|
|
420
|
+
|| hybridFn
|
|
421
|
+
|| ((obj) => (jsFn(obj) ? VALID_RESULT : errFn(obj)));
|
|
280
422
|
this.validateJSON = useSimdjsonForLarge
|
|
281
423
|
? (jsonStr) => {
|
|
282
424
|
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
425
|
+
this._ensureNative();
|
|
283
426
|
const buf = Buffer.from(jsonStr);
|
|
284
|
-
if (native.rawFastValidate(
|
|
285
|
-
|
|
427
|
+
if (native.rawFastValidate(this._fastSlot, buf))
|
|
428
|
+
return VALID_RESULT;
|
|
429
|
+
return this._compiled.validateJSON(jsonStr);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
return jsonValidateFn(JSON.parse(jsonStr));
|
|
433
|
+
} catch (e) {
|
|
434
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
286
435
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return compiled.validateJSON(jsonStr);
|
|
436
|
+
this._ensureNative();
|
|
437
|
+
return this._compiled.validateJSON(jsonStr);
|
|
290
438
|
}
|
|
291
439
|
: (jsonStr) => {
|
|
292
|
-
try {
|
|
293
|
-
|
|
294
|
-
|
|
440
|
+
try {
|
|
441
|
+
return jsonValidateFn(JSON.parse(jsonStr));
|
|
442
|
+
} catch (e) {
|
|
443
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
444
|
+
}
|
|
445
|
+
this._ensureNative();
|
|
446
|
+
return this._compiled.validateJSON(jsonStr);
|
|
295
447
|
};
|
|
296
448
|
this.isValidJSON = useSimdjsonForLarge
|
|
297
449
|
? (jsonStr) => {
|
|
298
450
|
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
299
|
-
|
|
451
|
+
this._ensureNative();
|
|
452
|
+
return native.rawFastValidate(
|
|
453
|
+
this._fastSlot,
|
|
454
|
+
Buffer.from(jsonStr),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
return jsFn(JSON.parse(jsonStr));
|
|
459
|
+
} catch (e) {
|
|
460
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
461
|
+
return false;
|
|
300
462
|
}
|
|
301
|
-
try { return jsFn(JSON.parse(jsonStr)); }
|
|
302
|
-
catch (e) { if (!(e instanceof SyntaxError)) throw e; return false; }
|
|
303
463
|
}
|
|
304
464
|
: (jsonStr) => {
|
|
305
|
-
try {
|
|
306
|
-
|
|
465
|
+
try {
|
|
466
|
+
return jsFn(JSON.parse(jsonStr));
|
|
467
|
+
} catch (e) {
|
|
468
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
307
471
|
};
|
|
472
|
+
} else {
|
|
473
|
+
// ATA_FORCE_NAPI path: no JS codegen, use native for everything
|
|
474
|
+
this._ensureNative();
|
|
475
|
+
this.validate = preprocess
|
|
476
|
+
? (data) => {
|
|
477
|
+
preprocess(data);
|
|
478
|
+
return this._compiled.validate(data);
|
|
479
|
+
}
|
|
480
|
+
: (data) => this._compiled.validate(data);
|
|
481
|
+
this.isValidObject = (data) => this._compiled.validate(data).valid;
|
|
482
|
+
this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
|
|
483
|
+
this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
|
|
308
484
|
}
|
|
485
|
+
}
|
|
309
486
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
validate(value) {
|
|
316
|
-
const result = self.validate(value);
|
|
317
|
-
if (result.valid) {
|
|
318
|
-
return { value };
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
issues: result.errors.map((err) => ({
|
|
322
|
-
message: err.message,
|
|
323
|
-
path: parsePointerPath(err.path),
|
|
324
|
-
})),
|
|
325
|
-
};
|
|
326
|
-
},
|
|
327
|
-
}),
|
|
328
|
-
writable: false,
|
|
329
|
-
enumerable: false,
|
|
330
|
-
configurable: false,
|
|
331
|
-
});
|
|
487
|
+
_ensureNative() {
|
|
488
|
+
if (this._nativeReady) return;
|
|
489
|
+
this._nativeReady = true;
|
|
490
|
+
this._compiled = new native.CompiledSchema(this._schemaStr);
|
|
491
|
+
this._fastSlot = native.fastRegister(this._schemaStr);
|
|
332
492
|
}
|
|
333
493
|
|
|
334
494
|
// --- Standalone pre-compilation ---
|
|
335
495
|
// Generate a JS module string that can be written to a file.
|
|
336
|
-
// On next startup, load with Validator.fromStandalone()
|
|
496
|
+
// On next startup, load with Validator.fromStandalone() -- zero compile time.
|
|
337
497
|
toStandalone() {
|
|
498
|
+
this._ensureCompiled();
|
|
338
499
|
const jsFn = this._jsFn;
|
|
339
500
|
if (!jsFn || !jsFn._source) return null;
|
|
340
501
|
const src = jsFn._source;
|
|
341
|
-
const hybridSrc = jsFn._hybridSource ||
|
|
502
|
+
const hybridSrc = jsFn._hybridSource || "";
|
|
342
503
|
|
|
343
504
|
// Also capture error function source for zero-compile standalone load
|
|
344
505
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
345
|
-
typeof this._schemaObj ===
|
|
506
|
+
typeof this._schemaObj === "object" ? this._schemaObj : {},
|
|
346
507
|
);
|
|
347
|
-
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource :
|
|
508
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : "";
|
|
348
509
|
|
|
349
510
|
return `// Auto-generated by ata-validator — do not edit
|
|
350
511
|
'use strict';
|
|
@@ -356,7 +517,7 @@ const hybridFactory = function(R, E) {
|
|
|
356
517
|
${hybridSrc}
|
|
357
518
|
};
|
|
358
519
|
};
|
|
359
|
-
${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` :
|
|
520
|
+
${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : "const errFn = null;"}
|
|
360
521
|
module.exports = { boolFn, hybridFactory, errFn };
|
|
361
522
|
`;
|
|
362
523
|
}
|
|
@@ -377,21 +538,36 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
377
538
|
// Mutators
|
|
378
539
|
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
379
540
|
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
380
|
-
const applyRemove = options.removeAdditional
|
|
541
|
+
const applyRemove = options.removeAdditional
|
|
542
|
+
? buildRemover(schemaObj)
|
|
543
|
+
: null;
|
|
381
544
|
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
382
|
-
const preprocess =
|
|
383
|
-
|
|
384
|
-
|
|
545
|
+
const preprocess =
|
|
546
|
+
mutators.length === 0
|
|
547
|
+
? null
|
|
548
|
+
: mutators.length === 1
|
|
549
|
+
? mutators[0]
|
|
550
|
+
: (data) => {
|
|
551
|
+
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
552
|
+
};
|
|
385
553
|
v._preprocess = preprocess;
|
|
386
554
|
|
|
387
555
|
// Error function — use pre-compiled from standalone if available, else compile
|
|
388
|
-
let errFn = (d) => ({
|
|
556
|
+
let errFn = (d) => ({
|
|
557
|
+
valid: false,
|
|
558
|
+
errors: [
|
|
559
|
+
{ code: "validation_failed", path: "", message: "validation failed" },
|
|
560
|
+
],
|
|
561
|
+
});
|
|
389
562
|
if (mod.errFn) {
|
|
390
563
|
errFn = (d) => mod.errFn(d, true);
|
|
391
564
|
} else {
|
|
392
565
|
const jsErrFn = compileToJSCodegenWithErrors(schemaObj);
|
|
393
566
|
if (jsErrFn) {
|
|
394
|
-
try {
|
|
567
|
+
try {
|
|
568
|
+
jsErrFn({}, true);
|
|
569
|
+
errFn = (d) => jsErrFn(d, true);
|
|
570
|
+
} catch {}
|
|
395
571
|
}
|
|
396
572
|
}
|
|
397
573
|
|
|
@@ -401,77 +577,93 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
401
577
|
: null;
|
|
402
578
|
|
|
403
579
|
v.validate = hybridFn
|
|
404
|
-
?
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
580
|
+
? preprocess
|
|
581
|
+
? (data) => {
|
|
582
|
+
preprocess(data);
|
|
583
|
+
return hybridFn(data);
|
|
584
|
+
}
|
|
585
|
+
: hybridFn
|
|
586
|
+
: preprocess
|
|
587
|
+
? (data) => {
|
|
588
|
+
preprocess(data);
|
|
589
|
+
return mod.boolFn(data) ? VALID_RESULT : errFn(data);
|
|
590
|
+
}
|
|
591
|
+
: (data) => (mod.boolFn(data) ? VALID_RESULT : errFn(data));
|
|
408
592
|
v.isValidObject = mod.boolFn;
|
|
409
593
|
v.isValidJSON = (jsonStr) => {
|
|
410
|
-
try {
|
|
594
|
+
try {
|
|
595
|
+
return mod.boolFn(JSON.parse(jsonStr));
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
411
599
|
};
|
|
412
600
|
v.validateJSON = (jsonStr) => {
|
|
413
601
|
try {
|
|
414
602
|
const obj = JSON.parse(jsonStr);
|
|
415
|
-
return hybridFn
|
|
416
|
-
|
|
603
|
+
return hybridFn
|
|
604
|
+
? hybridFn(obj)
|
|
605
|
+
: mod.boolFn(obj)
|
|
606
|
+
? VALID_RESULT
|
|
607
|
+
: errFn(obj);
|
|
608
|
+
} catch {
|
|
609
|
+
return {
|
|
610
|
+
valid: false,
|
|
611
|
+
errors: [{ code: "invalid_json", path: "", message: "invalid JSON" }],
|
|
612
|
+
};
|
|
613
|
+
}
|
|
417
614
|
};
|
|
418
615
|
|
|
419
616
|
// Standard Schema V1
|
|
420
617
|
Object.defineProperty(v, "~standard", {
|
|
421
618
|
value: Object.freeze({
|
|
422
|
-
version: 1,
|
|
619
|
+
version: 1,
|
|
620
|
+
vendor: "ata-validator",
|
|
423
621
|
validate(value) {
|
|
424
622
|
const result = v.validate(value);
|
|
425
623
|
if (result.valid) return { value };
|
|
426
|
-
return {
|
|
624
|
+
return {
|
|
625
|
+
issues: result.errors.map((e) => ({
|
|
626
|
+
message: e.message,
|
|
627
|
+
path: parsePointerPath(e.path),
|
|
628
|
+
})),
|
|
629
|
+
};
|
|
427
630
|
},
|
|
428
631
|
}),
|
|
429
|
-
writable: false,
|
|
632
|
+
writable: false,
|
|
633
|
+
enumerable: false,
|
|
634
|
+
configurable: false,
|
|
430
635
|
});
|
|
431
636
|
|
|
432
637
|
return v;
|
|
433
638
|
}
|
|
434
639
|
|
|
435
|
-
// Fallback methods — only used when JS codegen is unavailable
|
|
436
|
-
validate(data) {
|
|
437
|
-
if (this._preprocess) this._preprocess(data);
|
|
438
|
-
return this._compiled.validate(data);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
isValidObject(data) {
|
|
442
|
-
return this._compiled.validate(data).valid;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
validateJSON(jsonStr) {
|
|
446
|
-
return this._compiled.validateJSON(jsonStr);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
isValidJSON(jsonStr) {
|
|
450
|
-
return this._compiled.isValidJSON(jsonStr);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
640
|
// Raw NAPI fast path for Buffer/Uint8Array
|
|
454
641
|
isValid(input) {
|
|
642
|
+
this._ensureNative();
|
|
455
643
|
return native.rawFastValidate(this._fastSlot, input);
|
|
456
644
|
}
|
|
457
645
|
|
|
458
646
|
// Zero-copy pre-padded path
|
|
459
647
|
isValidPrepadded(paddedBuffer, jsonLength) {
|
|
648
|
+
this._ensureNative();
|
|
460
649
|
return native.rawFastValidate(this._fastSlot, paddedBuffer, jsonLength);
|
|
461
650
|
}
|
|
462
651
|
|
|
463
652
|
// Parallel NDJSON batch (multi-core)
|
|
464
653
|
isValidParallel(buffer) {
|
|
654
|
+
this._ensureNative();
|
|
465
655
|
return native.rawParallelValidate(this._fastSlot, buffer);
|
|
466
656
|
}
|
|
467
657
|
|
|
468
|
-
// Parallel count (fastest
|
|
658
|
+
// Parallel count (fastest -- single uint32 return)
|
|
469
659
|
countValid(buffer) {
|
|
660
|
+
this._ensureNative();
|
|
470
661
|
return native.rawParallelCount(this._fastSlot, buffer);
|
|
471
662
|
}
|
|
472
663
|
|
|
473
664
|
// NDJSON single-thread batch
|
|
474
665
|
isValidNDJSON(buffer) {
|
|
666
|
+
this._ensureNative();
|
|
475
667
|
return native.rawNDJSONValidate(this._fastSlot, buffer);
|
|
476
668
|
}
|
|
477
669
|
}
|
|
@@ -492,44 +684,53 @@ function version() {
|
|
|
492
684
|
// fs.writeFileSync('validators.js', bundle);
|
|
493
685
|
// // On startup:
|
|
494
686
|
// const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
|
|
495
|
-
Validator.bundle = function(schemas, opts) {
|
|
496
|
-
const parts = schemas.map(schema => {
|
|
687
|
+
Validator.bundle = function (schemas, opts) {
|
|
688
|
+
const parts = schemas.map((schema) => {
|
|
497
689
|
const v = new Validator(schema, opts);
|
|
498
690
|
const standalone = v.toStandalone();
|
|
499
|
-
if (!standalone) return
|
|
500
|
-
return
|
|
691
|
+
if (!standalone) return "null";
|
|
692
|
+
return (
|
|
693
|
+
"(function(){" +
|
|
694
|
+
standalone
|
|
695
|
+
.replace("'use strict';", "")
|
|
696
|
+
.replace("module.exports = ", "return ") +
|
|
697
|
+
"})()"
|
|
698
|
+
);
|
|
501
699
|
});
|
|
502
|
-
return "'use strict';\nmodule.exports = [\n" + parts.join(
|
|
700
|
+
return "'use strict';\nmodule.exports = [\n" + parts.join(",\n") + "\n];\n";
|
|
503
701
|
};
|
|
504
702
|
|
|
505
703
|
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
506
|
-
Validator.bundleStandalone = function(schemas, opts) {
|
|
704
|
+
Validator.bundleStandalone = function (schemas, opts) {
|
|
507
705
|
const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
|
|
508
|
-
const fns = schemas.map(schema => {
|
|
706
|
+
const fns = schemas.map((schema) => {
|
|
509
707
|
const v = new Validator(schema, opts);
|
|
708
|
+
v._ensureCompiled();
|
|
510
709
|
const jsFn = v._jsFn;
|
|
511
|
-
if (!jsFn || !jsFn._hybridSource) return
|
|
710
|
+
if (!jsFn || !jsFn._hybridSource) return "null";
|
|
512
711
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
513
|
-
typeof schema ===
|
|
712
|
+
typeof schema === "string" ? JSON.parse(schema) : schema,
|
|
514
713
|
);
|
|
515
|
-
const errBody =
|
|
516
|
-
|
|
517
|
-
|
|
714
|
+
const errBody =
|
|
715
|
+
jsErrFn && jsErrFn._errSource
|
|
716
|
+
? jsErrFn._errSource
|
|
717
|
+
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
518
718
|
return `(function(R){var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
519
719
|
});
|
|
520
|
-
return `'use strict';\nvar R=${R};\nmodule.exports=[${fns.join(
|
|
720
|
+
return `'use strict';\nvar R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
|
|
521
721
|
};
|
|
522
722
|
|
|
523
723
|
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
524
724
|
// Much smaller file → faster V8 parse → faster startup.
|
|
525
|
-
Validator.bundleCompact = function(schemas, opts) {
|
|
725
|
+
Validator.bundleCompact = function (schemas, opts) {
|
|
526
726
|
// Analyze schemas and group by structure
|
|
527
|
-
const entries = schemas.map(schema => {
|
|
727
|
+
const entries = schemas.map((schema) => {
|
|
528
728
|
const v = new Validator(schema, opts);
|
|
729
|
+
v._ensureCompiled();
|
|
529
730
|
const jsFn = v._jsFn;
|
|
530
731
|
if (!jsFn || !jsFn._hybridSource) return null;
|
|
531
732
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
532
|
-
typeof schema ===
|
|
733
|
+
typeof schema === "string" ? JSON.parse(schema) : schema,
|
|
533
734
|
);
|
|
534
735
|
return {
|
|
535
736
|
hybrid: jsFn._hybridSource,
|
|
@@ -543,14 +744,22 @@ Validator.bundleCompact = function(schemas, opts) {
|
|
|
543
744
|
const errMap = new Map();
|
|
544
745
|
const errBodies = [];
|
|
545
746
|
|
|
546
|
-
const indices = entries.map(e => {
|
|
747
|
+
const indices = entries.map((e) => {
|
|
547
748
|
if (!e) return [-1, -1];
|
|
548
749
|
let hi = bodyMap.get(e.hybrid);
|
|
549
|
-
if (hi === undefined) {
|
|
750
|
+
if (hi === undefined) {
|
|
751
|
+
hi = bodies.length;
|
|
752
|
+
bodies.push(e.hybrid);
|
|
753
|
+
bodyMap.set(e.hybrid, hi);
|
|
754
|
+
}
|
|
550
755
|
let ei = -1;
|
|
551
756
|
if (e.err) {
|
|
552
757
|
ei = errMap.get(e.err);
|
|
553
|
-
if (ei === undefined) {
|
|
758
|
+
if (ei === undefined) {
|
|
759
|
+
ei = errBodies.length;
|
|
760
|
+
errBodies.push(e.err);
|
|
761
|
+
errMap.set(e.err, ei);
|
|
762
|
+
}
|
|
554
763
|
}
|
|
555
764
|
return [hi, ei];
|
|
556
765
|
});
|
|
@@ -561,31 +770,41 @@ Validator.bundleCompact = function(schemas, opts) {
|
|
|
561
770
|
|
|
562
771
|
// Shared hybrid factories
|
|
563
772
|
out += "var H=[\n";
|
|
564
|
-
out += bodies
|
|
773
|
+
out += bodies
|
|
774
|
+
.map((b) => `function(R,E){return function(d){${b}}}`)
|
|
775
|
+
.join(",\n");
|
|
565
776
|
out += "\n];\n";
|
|
566
777
|
|
|
567
778
|
// Shared error functions
|
|
568
779
|
out += "var EF=[\n";
|
|
569
|
-
out += errBodies.map(b => `function(d){var _all=true;${b}}`).join(
|
|
780
|
+
out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(",\n");
|
|
570
781
|
out += "\n];\n";
|
|
571
782
|
|
|
572
783
|
// Build validators from shared templates
|
|
573
784
|
out += "module.exports=[";
|
|
574
|
-
out += indices
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
785
|
+
out += indices
|
|
786
|
+
.map(([hi, ei]) => {
|
|
787
|
+
if (hi < 0) return "null";
|
|
788
|
+
if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
|
|
789
|
+
return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
|
|
790
|
+
})
|
|
791
|
+
.join(",");
|
|
579
792
|
out += "];\n";
|
|
580
793
|
|
|
581
794
|
return out;
|
|
582
795
|
};
|
|
583
796
|
|
|
584
|
-
Validator.loadBundle = function(mods, schemas, opts) {
|
|
797
|
+
Validator.loadBundle = function (mods, schemas, opts) {
|
|
585
798
|
return schemas.map((schema, i) => {
|
|
586
799
|
if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
|
|
587
800
|
return new Validator(schema, opts);
|
|
588
801
|
});
|
|
589
802
|
};
|
|
590
803
|
|
|
591
|
-
module.exports = {
|
|
804
|
+
module.exports = {
|
|
805
|
+
Validator,
|
|
806
|
+
validate,
|
|
807
|
+
version,
|
|
808
|
+
createPaddedBuffer,
|
|
809
|
+
SIMDJSON_PADDING,
|
|
810
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. Beats ajv on every valid-path benchmark: 1.1x–2.7x faster validate(obj), 151x faster compilation, 5.9x faster parallel batch. Speculative validation with V8-optimized JS codegen, simdjson, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|