ata-validator 0.2.0 → 0.4.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 +118 -185
- package/binding/ata_napi.cpp +610 -7
- package/binding.gyp +1 -1
- package/include/ata.h +11 -3
- package/index.js +169 -3
- package/lib/js-compiler.js +845 -0
- package/package.json +3 -2
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +78 -11
package/index.js
CHANGED
|
@@ -1,4 +1,60 @@
|
|
|
1
1
|
const native = require("node-gyp-build")(__dirname);
|
|
2
|
+
const { compileToJS, compileToJSCodegen } = require("./lib/js-compiler");
|
|
3
|
+
|
|
4
|
+
// Extract default values from a schema tree. Returns a function that applies
|
|
5
|
+
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
6
|
+
function buildDefaultsApplier(schema) {
|
|
7
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
8
|
+
const actions = [];
|
|
9
|
+
collectDefaults(schema, actions);
|
|
10
|
+
if (actions.length === 0) return null;
|
|
11
|
+
return (data) => {
|
|
12
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function collectDefaults(schema, actions, path) {
|
|
17
|
+
if (typeof schema !== 'object' || schema === null) return;
|
|
18
|
+
const props = schema.properties;
|
|
19
|
+
if (!props) return;
|
|
20
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
21
|
+
if (prop && typeof prop === 'object' && prop.default !== undefined) {
|
|
22
|
+
const defaultVal = prop.default;
|
|
23
|
+
if (!path) {
|
|
24
|
+
actions.push((data) => {
|
|
25
|
+
if (typeof data === 'object' && data !== null && !(key in data)) {
|
|
26
|
+
data[key] = typeof defaultVal === 'object' && defaultVal !== null
|
|
27
|
+
? JSON.parse(JSON.stringify(defaultVal)) : defaultVal;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
const parentPath = path;
|
|
32
|
+
actions.push((data) => {
|
|
33
|
+
let target = data;
|
|
34
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
35
|
+
if (typeof target !== 'object' || target === null) return;
|
|
36
|
+
target = target[parentPath[j]];
|
|
37
|
+
}
|
|
38
|
+
if (typeof target === 'object' && target !== null && !(key in target)) {
|
|
39
|
+
target[key] = typeof defaultVal === 'object' && defaultVal !== null
|
|
40
|
+
? JSON.parse(JSON.stringify(defaultVal)) : defaultVal;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Recurse into nested object schemas
|
|
46
|
+
if (prop && typeof prop === 'object' && prop.properties) {
|
|
47
|
+
collectDefaults(prop, actions, (path || []).concat(key));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SIMDJSON_PADDING = 64;
|
|
53
|
+
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
54
|
+
|
|
55
|
+
// Above this size, simdjson On Demand (selective field access) beats JSON.parse
|
|
56
|
+
// (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
|
|
57
|
+
const SIMDJSON_THRESHOLD = 8192;
|
|
2
58
|
|
|
3
59
|
function parsePointerPath(path) {
|
|
4
60
|
if (!path) return [];
|
|
@@ -10,11 +66,90 @@ function parsePointerPath(path) {
|
|
|
10
66
|
}));
|
|
11
67
|
}
|
|
12
68
|
|
|
69
|
+
function createPaddedBuffer(jsonStr) {
|
|
70
|
+
const jsonBuf = Buffer.from(jsonStr);
|
|
71
|
+
const padded = Buffer.allocUnsafe(jsonBuf.length + SIMDJSON_PADDING);
|
|
72
|
+
jsonBuf.copy(padded);
|
|
73
|
+
padded.fill(0, jsonBuf.length);
|
|
74
|
+
return { buffer: padded, length: jsonBuf.length };
|
|
75
|
+
}
|
|
76
|
+
|
|
13
77
|
class Validator {
|
|
14
78
|
constructor(schema) {
|
|
15
79
|
const schemaStr =
|
|
16
80
|
typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
17
|
-
|
|
81
|
+
const compiled = new native.CompiledSchema(schemaStr);
|
|
82
|
+
this._compiled = compiled;
|
|
83
|
+
this._fastSlot = native.fastRegister(schemaStr);
|
|
84
|
+
|
|
85
|
+
// Pure JS fast path — no NAPI, runs in V8 JIT
|
|
86
|
+
// Set ATA_FORCE_NAPI=1 to disable JS codegen (for correctness testing)
|
|
87
|
+
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
88
|
+
const jsFn = process.env.ATA_FORCE_NAPI
|
|
89
|
+
? null
|
|
90
|
+
: (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
|
|
91
|
+
this._jsFn = jsFn;
|
|
92
|
+
|
|
93
|
+
// Default value applier — applies schema defaults to objects in-place
|
|
94
|
+
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
95
|
+
this._applyDefaults = applyDefaults;
|
|
96
|
+
|
|
97
|
+
// Closure-capture: avoid `this` property lookup on every call.
|
|
98
|
+
// V8 keeps closure vars in registers — no hidden class traversal.
|
|
99
|
+
const fastSlot = this._fastSlot;
|
|
100
|
+
|
|
101
|
+
// Detect if schema is "selective" — doesn't recurse into arrays/deep objects.
|
|
102
|
+
// Selective schemas benefit from simdjson On Demand (seeks only needed fields).
|
|
103
|
+
// Non-selective schemas (items, allOf with nested) touch everything — JSON.parse + jsFn wins.
|
|
104
|
+
const hasArrayTraversal = schemaObj && (schemaObj.items || schemaObj.prefixItems ||
|
|
105
|
+
schemaObj.contains || (schemaObj.properties && Object.values(schemaObj.properties).some(
|
|
106
|
+
p => p && (p.items || p.prefixItems || p.contains))));
|
|
107
|
+
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
108
|
+
|
|
109
|
+
if (jsFn) {
|
|
110
|
+
this.validate = applyDefaults
|
|
111
|
+
? (data) => { applyDefaults(data); return jsFn(data) ? VALID_RESULT : compiled.validate(data); }
|
|
112
|
+
: (data) => jsFn(data) ? VALID_RESULT : compiled.validate(data);
|
|
113
|
+
this.isValidObject = jsFn;
|
|
114
|
+
this.validateJSON = useSimdjsonForLarge
|
|
115
|
+
? (jsonStr) => {
|
|
116
|
+
// Selective schema: large docs use simdjson (skips irrelevant data)
|
|
117
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
118
|
+
const buf = Buffer.from(jsonStr);
|
|
119
|
+
if (native.rawFastValidate(fastSlot, buf)) return VALID_RESULT;
|
|
120
|
+
return compiled.validateJSON(jsonStr);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const obj = JSON.parse(jsonStr);
|
|
124
|
+
if (jsFn(obj)) return VALID_RESULT;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
127
|
+
}
|
|
128
|
+
return compiled.validateJSON(jsonStr);
|
|
129
|
+
}
|
|
130
|
+
: (jsonStr) => {
|
|
131
|
+
// Non-selective schema: JSON.parse + jsFn always wins
|
|
132
|
+
try {
|
|
133
|
+
const obj = JSON.parse(jsonStr);
|
|
134
|
+
if (jsFn(obj)) return VALID_RESULT;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
137
|
+
}
|
|
138
|
+
return compiled.validateJSON(jsonStr);
|
|
139
|
+
};
|
|
140
|
+
this.isValidJSON = useSimdjsonForLarge
|
|
141
|
+
? (jsonStr) => {
|
|
142
|
+
if (jsonStr.length >= SIMDJSON_THRESHOLD) {
|
|
143
|
+
return native.rawFastValidate(fastSlot, Buffer.from(jsonStr));
|
|
144
|
+
}
|
|
145
|
+
try { return jsFn(JSON.parse(jsonStr)); }
|
|
146
|
+
catch (e) { if (!(e instanceof SyntaxError)) throw e; return false; }
|
|
147
|
+
}
|
|
148
|
+
: (jsonStr) => {
|
|
149
|
+
try { return jsFn(JSON.parse(jsonStr)); }
|
|
150
|
+
catch (e) { if (!(e instanceof SyntaxError)) throw e; return false; }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
18
153
|
|
|
19
154
|
const self = this;
|
|
20
155
|
Object.defineProperty(this, "~standard", {
|
|
@@ -22,7 +157,7 @@ class Validator {
|
|
|
22
157
|
version: 1,
|
|
23
158
|
vendor: "ata-validator",
|
|
24
159
|
validate(value) {
|
|
25
|
-
const result = self.
|
|
160
|
+
const result = self.validate(value);
|
|
26
161
|
if (result.valid) {
|
|
27
162
|
return { value };
|
|
28
163
|
}
|
|
@@ -40,10 +175,16 @@ class Validator {
|
|
|
40
175
|
});
|
|
41
176
|
}
|
|
42
177
|
|
|
178
|
+
// Fallback methods — only used when JS codegen is unavailable
|
|
43
179
|
validate(data) {
|
|
180
|
+
if (this._applyDefaults) this._applyDefaults(data);
|
|
44
181
|
return this._compiled.validate(data);
|
|
45
182
|
}
|
|
46
183
|
|
|
184
|
+
isValidObject(data) {
|
|
185
|
+
return this._compiled.validate(data).valid;
|
|
186
|
+
}
|
|
187
|
+
|
|
47
188
|
validateJSON(jsonStr) {
|
|
48
189
|
return this._compiled.validateJSON(jsonStr);
|
|
49
190
|
}
|
|
@@ -51,6 +192,31 @@ class Validator {
|
|
|
51
192
|
isValidJSON(jsonStr) {
|
|
52
193
|
return this._compiled.isValidJSON(jsonStr);
|
|
53
194
|
}
|
|
195
|
+
|
|
196
|
+
// Raw NAPI fast path for Buffer/Uint8Array
|
|
197
|
+
isValid(input) {
|
|
198
|
+
return native.rawFastValidate(this._fastSlot, input);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Zero-copy pre-padded path
|
|
202
|
+
isValidPrepadded(paddedBuffer, jsonLength) {
|
|
203
|
+
return native.rawFastValidate(this._fastSlot, paddedBuffer, jsonLength);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Parallel NDJSON batch (multi-core)
|
|
207
|
+
isValidParallel(buffer) {
|
|
208
|
+
return native.rawParallelValidate(this._fastSlot, buffer);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Parallel count (fastest — single uint32 return)
|
|
212
|
+
countValid(buffer) {
|
|
213
|
+
return native.rawParallelCount(this._fastSlot, buffer);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// NDJSON single-thread batch
|
|
217
|
+
isValidNDJSON(buffer) {
|
|
218
|
+
return native.rawNDJSONValidate(this._fastSlot, buffer);
|
|
219
|
+
}
|
|
54
220
|
}
|
|
55
221
|
|
|
56
222
|
function validate(schema, data) {
|
|
@@ -63,4 +229,4 @@ function version() {
|
|
|
63
229
|
return native.version();
|
|
64
230
|
}
|
|
65
231
|
|
|
66
|
-
module.exports = { Validator, validate, version };
|
|
232
|
+
module.exports = { Validator, validate, version, createPaddedBuffer, SIMDJSON_PADDING };
|