ata-validator 0.4.0 → 0.4.2
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/include/ata.h +2 -2
- package/index.js +192 -2
- package/lib/js-compiler.js +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
package/include/ata.h
CHANGED
|
@@ -12,10 +12,10 @@ namespace ata {
|
|
|
12
12
|
|
|
13
13
|
inline constexpr uint32_t VERSION_MAJOR = 0;
|
|
14
14
|
inline constexpr uint32_t VERSION_MINOR = 4;
|
|
15
|
-
inline constexpr uint32_t VERSION_REVISION =
|
|
15
|
+
inline constexpr uint32_t VERSION_REVISION = 1;
|
|
16
16
|
|
|
17
17
|
inline constexpr std::string_view version() noexcept {
|
|
18
|
-
return "0.4.
|
|
18
|
+
return "0.4.1";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
enum class error_code : uint8_t {
|
package/index.js
CHANGED
|
@@ -1,6 +1,179 @@
|
|
|
1
1
|
const native = require("node-gyp-build")(__dirname);
|
|
2
2
|
const { compileToJS, compileToJSCodegen } = require("./lib/js-compiler");
|
|
3
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
|
+
// Build a function that coerces property values to match schema types in-place.
|
|
53
|
+
// Handles string→number, string→integer, string→boolean, number→string, boolean→string.
|
|
54
|
+
function buildCoercer(schema) {
|
|
55
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
56
|
+
const actions = [];
|
|
57
|
+
collectCoercions(schema, actions);
|
|
58
|
+
if (actions.length === 0) return null;
|
|
59
|
+
return (data) => {
|
|
60
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectCoercions(schema, actions, path) {
|
|
65
|
+
if (typeof schema !== 'object' || schema === null) return;
|
|
66
|
+
const props = schema.properties;
|
|
67
|
+
if (!props) return;
|
|
68
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
69
|
+
if (!prop || typeof prop !== 'object' || !prop.type) continue;
|
|
70
|
+
const targetType = Array.isArray(prop.type) ? null : prop.type;
|
|
71
|
+
if (!targetType) continue;
|
|
72
|
+
|
|
73
|
+
const coerce = buildSingleCoercion(targetType);
|
|
74
|
+
if (!coerce) continue;
|
|
75
|
+
|
|
76
|
+
if (!path) {
|
|
77
|
+
actions.push((data) => {
|
|
78
|
+
if (typeof data === 'object' && data !== null && key in data) {
|
|
79
|
+
const coerced = coerce(data[key]);
|
|
80
|
+
if (coerced !== undefined) data[key] = coerced;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
const parentPath = path;
|
|
85
|
+
actions.push((data) => {
|
|
86
|
+
let target = data;
|
|
87
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
88
|
+
if (typeof target !== 'object' || target === null) return;
|
|
89
|
+
target = target[parentPath[j]];
|
|
90
|
+
}
|
|
91
|
+
if (typeof target === 'object' && target !== null && key in target) {
|
|
92
|
+
const coerced = coerce(target[key]);
|
|
93
|
+
if (coerced !== undefined) target[key] = coerced;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Recurse into nested object properties
|
|
99
|
+
if (prop.properties) {
|
|
100
|
+
collectCoercions(prop, actions, (path || []).concat(key));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildSingleCoercion(targetType) {
|
|
106
|
+
switch (targetType) {
|
|
107
|
+
case 'number': return (v) => {
|
|
108
|
+
if (typeof v === 'string') { const n = Number(v); if (v !== '' && !isNaN(n)) return n; }
|
|
109
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
110
|
+
};
|
|
111
|
+
case 'integer': return (v) => {
|
|
112
|
+
if (typeof v === 'string') { const n = Number(v); if (v !== '' && Number.isInteger(n)) return n; }
|
|
113
|
+
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
114
|
+
};
|
|
115
|
+
case 'string': return (v) => {
|
|
116
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
117
|
+
};
|
|
118
|
+
case 'boolean': return (v) => {
|
|
119
|
+
if (v === 'true' || v === '1') return true;
|
|
120
|
+
if (v === 'false' || v === '0') return false;
|
|
121
|
+
};
|
|
122
|
+
default: return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build a function that removes properties not defined in schema.properties.
|
|
127
|
+
// Walks nested objects recursively.
|
|
128
|
+
function buildRemover(schema) {
|
|
129
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
130
|
+
const actions = [];
|
|
131
|
+
collectRemovals(schema, actions);
|
|
132
|
+
if (actions.length === 0) return null;
|
|
133
|
+
return (data) => {
|
|
134
|
+
for (let i = 0; i < actions.length; i++) actions[i](data);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectRemovals(schema, actions, path) {
|
|
139
|
+
if (typeof schema !== 'object' || schema === null || !schema.properties) return;
|
|
140
|
+
|
|
141
|
+
// If this level has additionalProperties: false, add a removal action
|
|
142
|
+
if (schema.additionalProperties === false) {
|
|
143
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
144
|
+
if (!path) {
|
|
145
|
+
actions.push((data) => {
|
|
146
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return;
|
|
147
|
+
const keys = Object.keys(data);
|
|
148
|
+
for (let i = 0; i < keys.length; i++) {
|
|
149
|
+
if (!allowed.has(keys[i])) delete data[keys[i]];
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
const parentPath = path;
|
|
154
|
+
actions.push((data) => {
|
|
155
|
+
let target = data;
|
|
156
|
+
for (let j = 0; j < parentPath.length; j++) {
|
|
157
|
+
if (typeof target !== 'object' || target === null) return;
|
|
158
|
+
target = target[parentPath[j]];
|
|
159
|
+
}
|
|
160
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) return;
|
|
161
|
+
const keys = Object.keys(target);
|
|
162
|
+
for (let i = 0; i < keys.length; i++) {
|
|
163
|
+
if (!allowed.has(keys[i])) delete target[keys[i]];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Always recurse into nested properties (they may have their own additionalProperties: false)
|
|
170
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
171
|
+
if (prop && typeof prop === 'object' && prop.properties) {
|
|
172
|
+
collectRemovals(prop, actions, (path || []).concat(key));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
4
177
|
const SIMDJSON_PADDING = 64;
|
|
5
178
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
6
179
|
|
|
@@ -27,7 +200,8 @@ function createPaddedBuffer(jsonStr) {
|
|
|
27
200
|
}
|
|
28
201
|
|
|
29
202
|
class Validator {
|
|
30
|
-
constructor(schema) {
|
|
203
|
+
constructor(schema, opts) {
|
|
204
|
+
const options = opts || {};
|
|
31
205
|
const schemaStr =
|
|
32
206
|
typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
33
207
|
const compiled = new native.CompiledSchema(schemaStr);
|
|
@@ -42,6 +216,19 @@ class Validator {
|
|
|
42
216
|
: (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
|
|
43
217
|
this._jsFn = jsFn;
|
|
44
218
|
|
|
219
|
+
// Data mutators — applied in-place before validation
|
|
220
|
+
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
221
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
222
|
+
const applyRemove = options.removeAdditional ? buildRemover(schemaObj) : null;
|
|
223
|
+
this._applyDefaults = applyDefaults;
|
|
224
|
+
|
|
225
|
+
// Combine all mutators into a single pre-validation step
|
|
226
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
227
|
+
const preprocess = mutators.length === 0 ? null
|
|
228
|
+
: mutators.length === 1 ? mutators[0]
|
|
229
|
+
: (data) => { for (let i = 0; i < mutators.length; i++) mutators[i](data); };
|
|
230
|
+
this._preprocess = preprocess;
|
|
231
|
+
|
|
45
232
|
// Closure-capture: avoid `this` property lookup on every call.
|
|
46
233
|
// V8 keeps closure vars in registers — no hidden class traversal.
|
|
47
234
|
const fastSlot = this._fastSlot;
|
|
@@ -55,7 +242,9 @@ class Validator {
|
|
|
55
242
|
const useSimdjsonForLarge = !hasArrayTraversal;
|
|
56
243
|
|
|
57
244
|
if (jsFn) {
|
|
58
|
-
this.validate =
|
|
245
|
+
this.validate = preprocess
|
|
246
|
+
? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : compiled.validate(data); }
|
|
247
|
+
: (data) => jsFn(data) ? VALID_RESULT : compiled.validate(data);
|
|
59
248
|
this.isValidObject = jsFn;
|
|
60
249
|
this.validateJSON = useSimdjsonForLarge
|
|
61
250
|
? (jsonStr) => {
|
|
@@ -123,6 +312,7 @@ class Validator {
|
|
|
123
312
|
|
|
124
313
|
// Fallback methods — only used when JS codegen is unavailable
|
|
125
314
|
validate(data) {
|
|
315
|
+
if (this._preprocess) this._preprocess(data);
|
|
126
316
|
return this._compiled.validate(data);
|
|
127
317
|
}
|
|
128
318
|
|
package/lib/js-compiler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// Compile a JSON Schema into a pure JS validator function.
|
|
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
7
|
function compileToJS(schema, defs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
|
Binary file
|