ata-validator 0.1.0 → 0.2.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/README.md +3 -3
- package/binding/ata_napi.cpp +297 -111
- package/binding.gyp +12 -1
- package/compat.d.ts +23 -0
- package/index.d.ts +37 -0
- package/index.js +37 -3
- package/package.json +14 -8
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +706 -122
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ A blazing-fast C++ JSON Schema validator powered by [simdjson](https://github.co
|
|
|
27
27
|
|
|
28
28
|
### JSON Schema Test Suite
|
|
29
29
|
|
|
30
|
-
**97.
|
|
30
|
+
**97.6%** pass rate (803/823) on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12).
|
|
31
31
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
@@ -170,8 +170,8 @@ int main(void) {
|
|
|
170
170
|
| Type | `type` (string, number, integer, boolean, null, array, object, union) |
|
|
171
171
|
| Numeric | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` |
|
|
172
172
|
| String | `minLength`, `maxLength`, `pattern`, `format` |
|
|
173
|
-
| Array | `items`, `prefixItems`, `minItems`, `maxItems`, `uniqueItems` |
|
|
174
|
-
| Object | `properties`, `required`, `additionalProperties`, `patternProperties`, `minProperties`, `maxProperties` |
|
|
173
|
+
| Array | `items`, `prefixItems`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `minContains`, `maxContains` |
|
|
174
|
+
| Object | `properties`, `required`, `additionalProperties`, `patternProperties`, `minProperties`, `maxProperties`, `propertyNames`, `dependentRequired`, `dependentSchemas` |
|
|
175
175
|
| Enum/Const | `enum`, `const` |
|
|
176
176
|
| Composition | `allOf`, `anyOf`, `oneOf`, `not` |
|
|
177
177
|
| Conditional | `if`, `then`, `else` |
|
package/binding/ata_napi.cpp
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#include <napi.h>
|
|
2
2
|
|
|
3
3
|
#include <cmath>
|
|
4
|
-
#include <
|
|
4
|
+
#include <re2/re2.h>
|
|
5
5
|
#include <set>
|
|
6
6
|
#include <string>
|
|
7
7
|
#include <vector>
|
|
@@ -31,13 +31,16 @@ struct schema_node {
|
|
|
31
31
|
std::optional<uint64_t> min_length;
|
|
32
32
|
std::optional<uint64_t> max_length;
|
|
33
33
|
std::optional<std::string> pattern;
|
|
34
|
-
std::shared_ptr<
|
|
34
|
+
std::shared_ptr<re2::RE2> compiled_pattern;
|
|
35
35
|
|
|
36
36
|
std::optional<uint64_t> min_items;
|
|
37
37
|
std::optional<uint64_t> max_items;
|
|
38
38
|
bool unique_items = false;
|
|
39
39
|
schema_node_ptr items_schema;
|
|
40
40
|
std::vector<schema_node_ptr> prefix_items;
|
|
41
|
+
schema_node_ptr contains_schema;
|
|
42
|
+
std::optional<uint64_t> min_contains;
|
|
43
|
+
std::optional<uint64_t> max_contains;
|
|
41
44
|
|
|
42
45
|
std::unordered_map<std::string, schema_node_ptr> properties;
|
|
43
46
|
std::vector<std::string> required;
|
|
@@ -45,8 +48,16 @@ struct schema_node {
|
|
|
45
48
|
schema_node_ptr additional_properties_schema;
|
|
46
49
|
std::optional<uint64_t> min_properties;
|
|
47
50
|
std::optional<uint64_t> max_properties;
|
|
51
|
+
schema_node_ptr property_names_schema;
|
|
52
|
+
std::unordered_map<std::string, std::vector<std::string>> dependent_required;
|
|
53
|
+
std::unordered_map<std::string, schema_node_ptr> dependent_schemas;
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
struct pattern_prop {
|
|
56
|
+
std::string pattern;
|
|
57
|
+
schema_node_ptr schema;
|
|
58
|
+
std::shared_ptr<re2::RE2> compiled;
|
|
59
|
+
};
|
|
60
|
+
std::vector<pattern_prop> pattern_properties;
|
|
50
61
|
|
|
51
62
|
std::optional<std::string> enum_values_raw;
|
|
52
63
|
std::vector<std::string> enum_values_minified;
|
|
@@ -65,6 +76,8 @@ struct schema_node {
|
|
|
65
76
|
|
|
66
77
|
std::string ref;
|
|
67
78
|
|
|
79
|
+
std::unordered_map<std::string, schema_node_ptr> defs;
|
|
80
|
+
|
|
68
81
|
std::optional<bool> boolean_schema;
|
|
69
82
|
};
|
|
70
83
|
|
|
@@ -73,6 +86,89 @@ struct compiled_schema_internal {
|
|
|
73
86
|
std::unordered_map<std::string, schema_node_ptr> defs;
|
|
74
87
|
};
|
|
75
88
|
|
|
89
|
+
// --- Fast format validators (no regex) ---
|
|
90
|
+
|
|
91
|
+
static bool nb_is_digit(char c) { return c >= '0' && c <= '9'; }
|
|
92
|
+
static bool nb_is_alpha(char c) {
|
|
93
|
+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
94
|
+
}
|
|
95
|
+
static bool nb_is_alnum(char c) { return nb_is_alpha(c) || nb_is_digit(c); }
|
|
96
|
+
static bool nb_is_hex(char c) {
|
|
97
|
+
return nb_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static bool napi_check_format(const std::string& sv, const std::string& fmt) {
|
|
101
|
+
if (fmt == "email") {
|
|
102
|
+
auto at = sv.find('@');
|
|
103
|
+
if (at == std::string::npos || at == 0 || at == sv.size() - 1) return false;
|
|
104
|
+
auto dot = sv.find('.', at + 1);
|
|
105
|
+
return dot != std::string::npos && dot != at + 1 && dot != sv.size() - 1 &&
|
|
106
|
+
(sv.size() - dot - 1) >= 2;
|
|
107
|
+
}
|
|
108
|
+
if (fmt == "date") {
|
|
109
|
+
return sv.size() == 10 && nb_is_digit(sv[0]) && nb_is_digit(sv[1]) &&
|
|
110
|
+
nb_is_digit(sv[2]) && nb_is_digit(sv[3]) && sv[4] == '-' &&
|
|
111
|
+
nb_is_digit(sv[5]) && nb_is_digit(sv[6]) && sv[7] == '-' &&
|
|
112
|
+
nb_is_digit(sv[8]) && nb_is_digit(sv[9]);
|
|
113
|
+
}
|
|
114
|
+
if (fmt == "time") {
|
|
115
|
+
if (sv.size() < 8) return false;
|
|
116
|
+
return nb_is_digit(sv[0]) && nb_is_digit(sv[1]) && sv[2] == ':' &&
|
|
117
|
+
nb_is_digit(sv[3]) && nb_is_digit(sv[4]) && sv[5] == ':' &&
|
|
118
|
+
nb_is_digit(sv[6]) && nb_is_digit(sv[7]);
|
|
119
|
+
}
|
|
120
|
+
if (fmt == "date-time") {
|
|
121
|
+
if (sv.size() < 19) return false;
|
|
122
|
+
if (!napi_check_format(sv.substr(0, 10), "date")) return false;
|
|
123
|
+
if (sv[10] != 'T' && sv[10] != 't' && sv[10] != ' ') return false;
|
|
124
|
+
return napi_check_format(sv.substr(11), "time");
|
|
125
|
+
}
|
|
126
|
+
if (fmt == "ipv4") {
|
|
127
|
+
int parts = 0, val = 0, digits = 0;
|
|
128
|
+
for (size_t i = 0; i <= sv.size(); ++i) {
|
|
129
|
+
if (i == sv.size() || sv[i] == '.') {
|
|
130
|
+
if (digits == 0 || val > 255) return false;
|
|
131
|
+
++parts; val = 0; digits = 0;
|
|
132
|
+
} else if (nb_is_digit(sv[i])) {
|
|
133
|
+
val = val * 10 + (sv[i] - '0'); ++digits;
|
|
134
|
+
if (digits > 3) return false;
|
|
135
|
+
} else {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return parts == 4;
|
|
140
|
+
}
|
|
141
|
+
if (fmt == "ipv6") return sv.find(':') != std::string::npos;
|
|
142
|
+
if (fmt == "uri" || fmt == "uri-reference") {
|
|
143
|
+
if (sv.size() < 3 || !nb_is_alpha(sv[0])) return false;
|
|
144
|
+
size_t i = 1;
|
|
145
|
+
while (i < sv.size() && (nb_is_alnum(sv[i]) || sv[i] == '+' || sv[i] == '-' || sv[i] == '.')) ++i;
|
|
146
|
+
return i < sv.size() && sv[i] == ':' && i + 1 < sv.size();
|
|
147
|
+
}
|
|
148
|
+
if (fmt == "uuid") {
|
|
149
|
+
if (sv.size() != 36) return false;
|
|
150
|
+
for (size_t i = 0; i < 36; ++i) {
|
|
151
|
+
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
|
152
|
+
if (sv[i] != '-') return false;
|
|
153
|
+
} else {
|
|
154
|
+
if (!nb_is_hex(sv[i])) return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (fmt == "hostname") {
|
|
160
|
+
if (sv.empty() || sv.size() > 253) return false;
|
|
161
|
+
size_t label_len = 0;
|
|
162
|
+
for (size_t i = 0; i < sv.size(); ++i) {
|
|
163
|
+
if (sv[i] == '.') { if (label_len == 0) return false; label_len = 0; }
|
|
164
|
+
else if (nb_is_alnum(sv[i]) || sv[i] == '-') { ++label_len; if (label_len > 63) return false; }
|
|
165
|
+
else return false;
|
|
166
|
+
}
|
|
167
|
+
return label_len > 0;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
76
172
|
// --- V8 Direct Validator ---
|
|
77
173
|
|
|
78
174
|
static std::string napi_type_of(Napi::Value val) {
|
|
@@ -115,17 +211,66 @@ static uint64_t utf8_codepoint_length(const std::string& s) {
|
|
|
115
211
|
}
|
|
116
212
|
|
|
117
213
|
// Serialize a Napi::Value to a minified JSON string (for enum/const comparison)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
214
|
+
// Canonical JSON: sort object keys for semantic equality comparison
|
|
215
|
+
static std::string napi_canonical_json(Napi::Env env, Napi::Value val) {
|
|
216
|
+
if (val.IsNull() || val.IsUndefined()) return "null";
|
|
217
|
+
if (val.IsBoolean()) return val.As<Napi::Boolean>().Value() ? "true" : "false";
|
|
218
|
+
if (val.IsNumber()) {
|
|
219
|
+
double d = val.As<Napi::Number>().DoubleValue();
|
|
220
|
+
if (d == static_cast<int64_t>(d) && std::abs(d) <= 9007199254740991.0) {
|
|
221
|
+
return std::to_string(static_cast<int64_t>(d));
|
|
222
|
+
}
|
|
223
|
+
auto json = env.Global().Get("JSON").As<Napi::Object>();
|
|
224
|
+
auto stringify = json.Get("stringify").As<Napi::Function>();
|
|
225
|
+
auto r = stringify.Call(json, {val});
|
|
226
|
+
return r.IsString() ? r.As<Napi::String>().Utf8Value() : "null";
|
|
227
|
+
}
|
|
228
|
+
if (val.IsString()) {
|
|
229
|
+
// JSON-encode the string
|
|
230
|
+
auto json = env.Global().Get("JSON").As<Napi::Object>();
|
|
231
|
+
auto stringify = json.Get("stringify").As<Napi::Function>();
|
|
232
|
+
auto r = stringify.Call(json, {val});
|
|
233
|
+
return r.IsString() ? r.As<Napi::String>().Utf8Value() : "null";
|
|
234
|
+
}
|
|
235
|
+
if (val.IsArray()) {
|
|
236
|
+
auto arr = val.As<Napi::Array>();
|
|
237
|
+
std::string r = "[";
|
|
238
|
+
for (uint32_t i = 0; i < arr.Length(); ++i) {
|
|
239
|
+
if (i) r += ',';
|
|
240
|
+
r += napi_canonical_json(env, arr.Get(i));
|
|
241
|
+
}
|
|
242
|
+
r += ']';
|
|
243
|
+
return r;
|
|
244
|
+
}
|
|
245
|
+
if (val.IsObject()) {
|
|
246
|
+
auto obj = val.As<Napi::Object>();
|
|
247
|
+
auto keys = obj.GetPropertyNames();
|
|
248
|
+
std::vector<std::string> sorted_keys;
|
|
249
|
+
for (uint32_t i = 0; i < keys.Length(); ++i) {
|
|
250
|
+
sorted_keys.push_back(keys.Get(i).As<Napi::String>().Utf8Value());
|
|
251
|
+
}
|
|
252
|
+
std::sort(sorted_keys.begin(), sorted_keys.end());
|
|
253
|
+
std::string r = "{";
|
|
254
|
+
for (size_t i = 0; i < sorted_keys.size(); ++i) {
|
|
255
|
+
if (i) r += ',';
|
|
256
|
+
// JSON-encode the key
|
|
257
|
+
auto json = env.Global().Get("JSON").As<Napi::Object>();
|
|
258
|
+
auto stringify = json.Get("stringify").As<Napi::Function>();
|
|
259
|
+
auto k = stringify.Call(json, {Napi::String::New(env, sorted_keys[i])});
|
|
260
|
+
r += k.As<Napi::String>().Utf8Value();
|
|
261
|
+
r += ':';
|
|
262
|
+
r += napi_canonical_json(env, obj.Get(sorted_keys[i]));
|
|
263
|
+
}
|
|
264
|
+
r += '}';
|
|
265
|
+
return r;
|
|
124
266
|
}
|
|
125
|
-
if (val.IsUndefined()) return "null";
|
|
126
267
|
return "null";
|
|
127
268
|
}
|
|
128
269
|
|
|
270
|
+
static std::string napi_to_json(Napi::Env env, Napi::Value val) {
|
|
271
|
+
return napi_canonical_json(env, val);
|
|
272
|
+
}
|
|
273
|
+
|
|
129
274
|
static void validate_napi(const schema_node_ptr& node,
|
|
130
275
|
Napi::Value value,
|
|
131
276
|
Napi::Env env,
|
|
@@ -150,63 +295,80 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
150
295
|
return;
|
|
151
296
|
}
|
|
152
297
|
|
|
153
|
-
// $ref
|
|
298
|
+
// $ref — Draft 2020-12: $ref is not a short-circuit, sibling keywords still apply
|
|
299
|
+
bool ref_resolved = false;
|
|
154
300
|
if (!node->ref.empty()) {
|
|
155
|
-
// First check defs map
|
|
156
301
|
auto it = ctx.defs.find(node->ref);
|
|
157
302
|
if (it != ctx.defs.end()) {
|
|
158
303
|
validate_napi(it->second, value, env, path, ctx, errors);
|
|
159
|
-
|
|
304
|
+
ref_resolved = true;
|
|
160
305
|
}
|
|
161
|
-
|
|
162
|
-
if (node->ref.size() > 1 && node->ref[0] == '#' &&
|
|
306
|
+
if (!ref_resolved && node->ref.size() > 1 && node->ref[0] == '#' &&
|
|
163
307
|
node->ref[1] == '/') {
|
|
308
|
+
// Decode JSON Pointer segments
|
|
309
|
+
auto decode_seg = [](const std::string& seg) -> std::string {
|
|
310
|
+
std::string pct;
|
|
311
|
+
for (size_t i = 0; i < seg.size(); ++i) {
|
|
312
|
+
if (seg[i] == '%' && i + 2 < seg.size()) {
|
|
313
|
+
auto hex = [](char c) -> int {
|
|
314
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
315
|
+
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
|
316
|
+
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
|
|
317
|
+
return -1;
|
|
318
|
+
};
|
|
319
|
+
int hv = hex(seg[i+1]), lv = hex(seg[i+2]);
|
|
320
|
+
if (hv >= 0 && lv >= 0) { pct += static_cast<char>(hv * 16 + lv); i += 2; }
|
|
321
|
+
else pct += seg[i];
|
|
322
|
+
} else pct += seg[i];
|
|
323
|
+
}
|
|
324
|
+
std::string out;
|
|
325
|
+
for (size_t i = 0; i < pct.size(); ++i) {
|
|
326
|
+
if (pct[i] == '~' && i + 1 < pct.size()) {
|
|
327
|
+
if (pct[i+1] == '1') { out += '/'; ++i; }
|
|
328
|
+
else if (pct[i+1] == '0') { out += '~'; ++i; }
|
|
329
|
+
else out += pct[i];
|
|
330
|
+
} else out += pct[i];
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
};
|
|
164
334
|
std::string pointer = node->ref.substr(2);
|
|
335
|
+
std::vector<std::string> segments;
|
|
336
|
+
size_t spos = 0;
|
|
337
|
+
while (spos < pointer.size()) {
|
|
338
|
+
size_t snext = pointer.find('/', spos);
|
|
339
|
+
segments.push_back(decode_seg(
|
|
340
|
+
pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
|
|
341
|
+
spos = (snext == std::string::npos) ? pointer.size() : snext + 1;
|
|
342
|
+
}
|
|
165
343
|
schema_node_ptr current = ctx.root;
|
|
166
344
|
bool resolved = true;
|
|
167
|
-
size_t
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
pointer.substr(pos, next == std::string::npos ? next : next - pos);
|
|
172
|
-
std::string key;
|
|
173
|
-
for (size_t i = 0; i < segment.size(); ++i) {
|
|
174
|
-
if (segment[i] == '~' && i + 1 < segment.size()) {
|
|
175
|
-
if (segment[i + 1] == '1') { key += '/'; ++i; }
|
|
176
|
-
else if (segment[i + 1] == '0') { key += '~'; ++i; }
|
|
177
|
-
else key += segment[i];
|
|
178
|
-
} else {
|
|
179
|
-
key += segment[i];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (key == "properties" && !current->properties.empty()) {
|
|
183
|
-
pos = (next == std::string::npos) ? pointer.size() : next + 1;
|
|
184
|
-
next = pointer.find('/', pos);
|
|
185
|
-
std::string prop = pointer.substr(
|
|
186
|
-
pos, next == std::string::npos ? next : next - pos);
|
|
187
|
-
auto pit = current->properties.find(prop);
|
|
345
|
+
for (size_t si = 0; si < segments.size() && current; ++si) {
|
|
346
|
+
const auto& key = segments[si];
|
|
347
|
+
if (key == "properties" && si + 1 < segments.size()) {
|
|
348
|
+
auto pit = current->properties.find(segments[++si]);
|
|
188
349
|
if (pit != current->properties.end()) current = pit->second;
|
|
189
350
|
else { resolved = false; break; }
|
|
190
351
|
} else if (key == "items" && current->items_schema) {
|
|
191
352
|
current = current->items_schema;
|
|
192
353
|
} else if (key == "$defs" || key == "definitions") {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
354
|
+
if (si + 1 < segments.size()) {
|
|
355
|
+
const auto& def_name = segments[++si];
|
|
356
|
+
auto dit = current->defs.find(def_name);
|
|
357
|
+
if (dit != current->defs.end()) current = dit->second;
|
|
358
|
+
else {
|
|
359
|
+
auto cit = ctx.defs.find("#/" + key + "/" + def_name);
|
|
360
|
+
if (cit != ctx.defs.end()) current = cit->second;
|
|
361
|
+
else { resolved = false; break; }
|
|
362
|
+
}
|
|
363
|
+
} else { resolved = false; break; }
|
|
200
364
|
} else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (idx < vec.size()) current = vec[idx];
|
|
209
|
-
else { resolved = false; break; }
|
|
365
|
+
if (si + 1 < segments.size()) {
|
|
366
|
+
size_t idx = std::stoul(segments[++si]);
|
|
367
|
+
auto& vec = (key == "allOf") ? current->all_of
|
|
368
|
+
: (key == "anyOf") ? current->any_of : current->one_of;
|
|
369
|
+
if (idx < vec.size()) current = vec[idx];
|
|
370
|
+
else { resolved = false; break; }
|
|
371
|
+
} else { resolved = false; break; }
|
|
210
372
|
} else if (key == "not" && current->not_schema) {
|
|
211
373
|
current = current->not_schema;
|
|
212
374
|
} else if (key == "if" && current->if_schema) {
|
|
@@ -219,28 +381,26 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
219
381
|
current->additional_properties_schema) {
|
|
220
382
|
current = current->additional_properties_schema;
|
|
221
383
|
} else if (key == "prefixItems") {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (idx < current->prefix_items.size()) current = current->prefix_items[idx];
|
|
228
|
-
else { resolved = false; break; }
|
|
384
|
+
if (si + 1 < segments.size()) {
|
|
385
|
+
size_t idx = std::stoul(segments[++si]);
|
|
386
|
+
if (idx < current->prefix_items.size()) current = current->prefix_items[idx];
|
|
387
|
+
else { resolved = false; break; }
|
|
388
|
+
} else { resolved = false; break; }
|
|
229
389
|
} else { resolved = false; break; }
|
|
230
|
-
pos = (next == std::string::npos) ? pointer.size() : next + 1;
|
|
231
390
|
}
|
|
232
391
|
if (resolved && current) {
|
|
233
392
|
validate_napi(current, value, env, path, ctx, errors);
|
|
234
|
-
|
|
393
|
+
ref_resolved = true;
|
|
235
394
|
}
|
|
236
395
|
}
|
|
237
|
-
if (node->ref == "#" && ctx.root) {
|
|
396
|
+
if (!ref_resolved && node->ref == "#" && ctx.root) {
|
|
238
397
|
validate_napi(ctx.root, value, env, path, ctx, errors);
|
|
239
|
-
|
|
398
|
+
ref_resolved = true;
|
|
399
|
+
}
|
|
400
|
+
if (!ref_resolved) {
|
|
401
|
+
errors.push_back({ata::error_code::ref_not_found, path,
|
|
402
|
+
"cannot resolve $ref: " + node->ref});
|
|
240
403
|
}
|
|
241
|
-
errors.push_back({ata::error_code::ref_not_found, path,
|
|
242
|
-
"cannot resolve $ref: " + node->ref});
|
|
243
|
-
return;
|
|
244
404
|
}
|
|
245
405
|
|
|
246
406
|
auto actual_type = napi_type_of(value);
|
|
@@ -356,7 +516,7 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
356
516
|
std::to_string(node->max_length.value())});
|
|
357
517
|
}
|
|
358
518
|
if (node->compiled_pattern) {
|
|
359
|
-
if (!
|
|
519
|
+
if (!re2::RE2::PartialMatch(sv, *node->compiled_pattern)) {
|
|
360
520
|
errors.push_back({ata::error_code::pattern_mismatch, path,
|
|
361
521
|
"string does not match pattern: " +
|
|
362
522
|
node->pattern.value()});
|
|
@@ -364,40 +524,7 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
364
524
|
}
|
|
365
525
|
if (node->format.has_value()) {
|
|
366
526
|
const auto& fmt = node->format.value();
|
|
367
|
-
bool format_ok =
|
|
368
|
-
if (fmt == "email") {
|
|
369
|
-
static const std::regex email_re(
|
|
370
|
-
R"([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})");
|
|
371
|
-
format_ok = std::regex_match(sv, email_re);
|
|
372
|
-
} else if (fmt == "uri" || fmt == "uri-reference") {
|
|
373
|
-
static const std::regex uri_re(R"([a-zA-Z][a-zA-Z0-9+\-.]*:.+)");
|
|
374
|
-
format_ok = std::regex_match(sv, uri_re);
|
|
375
|
-
} else if (fmt == "date") {
|
|
376
|
-
static const std::regex date_re(R"(\d{4}-\d{2}-\d{2})");
|
|
377
|
-
format_ok = std::regex_match(sv, date_re);
|
|
378
|
-
} else if (fmt == "date-time") {
|
|
379
|
-
static const std::regex dt_re(
|
|
380
|
-
R"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+\-]\d{2}:\d{2})?)");
|
|
381
|
-
format_ok = std::regex_match(sv, dt_re);
|
|
382
|
-
} else if (fmt == "time") {
|
|
383
|
-
static const std::regex time_re(
|
|
384
|
-
R"(\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+\-]\d{2}:\d{2})?)");
|
|
385
|
-
format_ok = std::regex_match(sv, time_re);
|
|
386
|
-
} else if (fmt == "ipv4") {
|
|
387
|
-
static const std::regex ipv4_re(
|
|
388
|
-
R"((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))");
|
|
389
|
-
format_ok = std::regex_match(sv, ipv4_re);
|
|
390
|
-
} else if (fmt == "ipv6") {
|
|
391
|
-
format_ok = sv.find(':') != std::string::npos;
|
|
392
|
-
} else if (fmt == "uuid") {
|
|
393
|
-
static const std::regex uuid_re(
|
|
394
|
-
R"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})");
|
|
395
|
-
format_ok = std::regex_match(sv, uuid_re);
|
|
396
|
-
} else if (fmt == "hostname") {
|
|
397
|
-
static const std::regex host_re(
|
|
398
|
-
R"([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*)");
|
|
399
|
-
format_ok = std::regex_match(sv, host_re);
|
|
400
|
-
}
|
|
527
|
+
bool format_ok = napi_check_format(sv, fmt);
|
|
401
528
|
if (!format_ok) {
|
|
402
529
|
errors.push_back({ata::error_code::format_mismatch, path,
|
|
403
530
|
"string does not match format: " + fmt});
|
|
@@ -449,6 +576,28 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
449
576
|
path + "/" + std::to_string(i), ctx, errors);
|
|
450
577
|
}
|
|
451
578
|
}
|
|
579
|
+
|
|
580
|
+
// contains / minContains / maxContains
|
|
581
|
+
if (node->contains_schema) {
|
|
582
|
+
uint64_t match_count = 0;
|
|
583
|
+
for (uint32_t i = 0; i < arr_size; ++i) {
|
|
584
|
+
std::vector<ata::validation_error> tmp;
|
|
585
|
+
validate_napi(node->contains_schema, arr.Get(i), env, path, ctx, tmp);
|
|
586
|
+
if (tmp.empty()) ++match_count;
|
|
587
|
+
}
|
|
588
|
+
uint64_t min_c = node->min_contains.value_or(1);
|
|
589
|
+
uint64_t max_c = node->max_contains.value_or(arr_size);
|
|
590
|
+
if (match_count < min_c) {
|
|
591
|
+
errors.push_back({ata::error_code::min_items_violation, path,
|
|
592
|
+
"contains: " + std::to_string(match_count) +
|
|
593
|
+
" matches, minimum " + std::to_string(min_c)});
|
|
594
|
+
}
|
|
595
|
+
if (match_count > max_c) {
|
|
596
|
+
errors.push_back({ata::error_code::max_items_violation, path,
|
|
597
|
+
"contains: " + std::to_string(match_count) +
|
|
598
|
+
" matches, maximum " + std::to_string(max_c)});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
452
601
|
}
|
|
453
602
|
|
|
454
603
|
// Object validations
|
|
@@ -493,15 +642,11 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
493
642
|
matched = true;
|
|
494
643
|
}
|
|
495
644
|
|
|
496
|
-
for (const auto&
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
errors);
|
|
502
|
-
matched = true;
|
|
503
|
-
}
|
|
504
|
-
} catch (...) {
|
|
645
|
+
for (const auto& pp : node->pattern_properties) {
|
|
646
|
+
if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
|
|
647
|
+
validate_napi(pp.schema, val, env, path + "/" + key_str, ctx,
|
|
648
|
+
errors);
|
|
649
|
+
matched = true;
|
|
505
650
|
}
|
|
506
651
|
}
|
|
507
652
|
|
|
@@ -517,6 +662,35 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
517
662
|
}
|
|
518
663
|
}
|
|
519
664
|
}
|
|
665
|
+
|
|
666
|
+
// propertyNames
|
|
667
|
+
if (node->property_names_schema) {
|
|
668
|
+
for (uint32_t i = 0; i < prop_count; ++i) {
|
|
669
|
+
Napi::Value key_val = keys.Get(i);
|
|
670
|
+
validate_napi(node->property_names_schema, key_val, env, path, ctx,
|
|
671
|
+
errors);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// dependentRequired
|
|
676
|
+
for (const auto& [prop, deps] : node->dependent_required) {
|
|
677
|
+
if (obj.HasOwnProperty(prop)) {
|
|
678
|
+
for (const auto& dep : deps) {
|
|
679
|
+
if (!obj.HasOwnProperty(dep)) {
|
|
680
|
+
errors.push_back({ata::error_code::required_property_missing, path,
|
|
681
|
+
"property '" + prop + "' requires '" + dep +
|
|
682
|
+
"' to be present"});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// dependentSchemas
|
|
689
|
+
for (const auto& [prop, schema] : node->dependent_schemas) {
|
|
690
|
+
if (obj.HasOwnProperty(prop)) {
|
|
691
|
+
validate_napi(schema, value, env, path, ctx, errors);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
520
694
|
}
|
|
521
695
|
|
|
522
696
|
// allOf
|
|
@@ -618,7 +792,8 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
618
792
|
env, "CompiledSchema",
|
|
619
793
|
{InstanceMethod("validate", &CompiledSchema::Validate),
|
|
620
794
|
InstanceMethod("validateJSON", &CompiledSchema::ValidateJSON),
|
|
621
|
-
InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect)
|
|
795
|
+
InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect),
|
|
796
|
+
InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON)});
|
|
622
797
|
auto* constructor = new Napi::FunctionReference();
|
|
623
798
|
*constructor = Napi::Persistent(func);
|
|
624
799
|
env.SetInstanceData(constructor);
|
|
@@ -671,6 +846,17 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
671
846
|
return make_result(env, result);
|
|
672
847
|
}
|
|
673
848
|
|
|
849
|
+
// Fast boolean-only validation — no error object creation
|
|
850
|
+
Napi::Value IsValidJSON(const Napi::CallbackInfo& info) {
|
|
851
|
+
Napi::Env env = info.Env();
|
|
852
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
853
|
+
return Napi::Boolean::New(env, false);
|
|
854
|
+
}
|
|
855
|
+
std::string json = info[0].As<Napi::String>().Utf8Value();
|
|
856
|
+
auto result = ata::validate(schema_, json);
|
|
857
|
+
return Napi::Boolean::New(env, result.valid);
|
|
858
|
+
}
|
|
859
|
+
|
|
674
860
|
// Explicit direct validation (always V8 traversal, never stringify)
|
|
675
861
|
Napi::Value ValidateDirect(const Napi::CallbackInfo& info) {
|
|
676
862
|
Napi::Env env = info.Env();
|
package/binding.gyp
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"include_dirs": [
|
|
11
11
|
"<!@(node -p \"require('node-addon-api').include\")",
|
|
12
12
|
"include",
|
|
13
|
-
"deps/simdjson"
|
|
13
|
+
"deps/simdjson",
|
|
14
|
+
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){console.log(a==='arm64'?'/opt/homebrew/opt/re2/include':'/usr/local/opt/re2/include');console.log(a==='arm64'?'/opt/homebrew/opt/abseil/include':'/usr/local/opt/abseil/include')}else{console.log('/usr/include')}\")"
|
|
15
|
+
],
|
|
16
|
+
"libraries": [
|
|
17
|
+
"<!@(node -e \"var p=process.platform,a=process.arch;if(p==='darwin'){var pre=a==='arm64'?'/opt/homebrew/opt/re2':'/usr/local/opt/re2';console.log('-L'+pre+'/lib -lre2')}else{console.log('-lre2')}\")"
|
|
14
18
|
],
|
|
15
19
|
"dependencies": [
|
|
16
20
|
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
@@ -26,6 +30,13 @@
|
|
|
26
30
|
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
|
|
27
31
|
"MACOSX_DEPLOYMENT_TARGET": "12.0"
|
|
28
32
|
}
|
|
33
|
+
}],
|
|
34
|
+
["OS=='win'", {
|
|
35
|
+
"msvs_settings": {
|
|
36
|
+
"VCCLCompilerTool": {
|
|
37
|
+
"AdditionalOptions": ["/std:c++20", "/EHsc"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
29
40
|
}]
|
|
30
41
|
]
|
|
31
42
|
}
|
package/compat.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface AjvError {
|
|
2
|
+
instancePath: string;
|
|
3
|
+
schemaPath: string;
|
|
4
|
+
keyword: string;
|
|
5
|
+
params: Record<string, unknown>;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ValidateFunction {
|
|
10
|
+
(data: unknown): boolean;
|
|
11
|
+
errors: AjvError[] | null;
|
|
12
|
+
schema: object;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare class Ata {
|
|
16
|
+
constructor(opts?: Record<string, unknown>);
|
|
17
|
+
compile(schema: object): ValidateFunction;
|
|
18
|
+
validate(schema: object, data: unknown): boolean;
|
|
19
|
+
addSchema(schema: object, key?: string): this;
|
|
20
|
+
getSchema(key: string): ValidateFunction | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export = Ata;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface ValidationError {
|
|
2
|
+
code: number;
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
errors: ValidationError[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StandardSchemaV1Props {
|
|
13
|
+
version: 1;
|
|
14
|
+
vendor: "ata-validator";
|
|
15
|
+
validate(
|
|
16
|
+
value: unknown
|
|
17
|
+
):
|
|
18
|
+
| { value: unknown }
|
|
19
|
+
| { issues: Array<{ message: string; path?: ReadonlyArray<{ key: PropertyKey }> }> };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Validator {
|
|
23
|
+
constructor(schema: object | string);
|
|
24
|
+
validate(data: unknown): ValidationResult;
|
|
25
|
+
validateJSON(jsonString: string): ValidationResult;
|
|
26
|
+
isValidJSON(jsonString: string): boolean;
|
|
27
|
+
|
|
28
|
+
/** Standard Schema V1 interface — compatible with Fastify, tRPC, TanStack, etc. */
|
|
29
|
+
readonly "~standard": StandardSchemaV1Props;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validate(
|
|
33
|
+
schema: object | string,
|
|
34
|
+
data: unknown
|
|
35
|
+
): ValidationResult;
|
|
36
|
+
|
|
37
|
+
export function version(): string;
|