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 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.1%** pass rate on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12).
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` |
@@ -1,7 +1,7 @@
1
1
  #include <napi.h>
2
2
 
3
3
  #include <cmath>
4
- #include <regex>
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<std::regex> compiled_pattern;
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
- std::vector<std::pair<std::string, schema_node_ptr>> pattern_properties;
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
- static std::string napi_to_json(Napi::Env env, Napi::Value val) {
119
- auto json = env.Global().Get("JSON").As<Napi::Object>();
120
- auto stringify = json.Get("stringify").As<Napi::Function>();
121
- auto result = stringify.Call(json, {val});
122
- if (result.IsString()) {
123
- return result.As<Napi::String>().Utf8Value();
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
- return;
304
+ ref_resolved = true;
160
305
  }
161
- // JSON Pointer resolution from root
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 pos = 0;
168
- while (pos < pointer.size() && current) {
169
- size_t next = pointer.find('/', pos);
170
- std::string segment =
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
- pos = (next == std::string::npos) ? pointer.size() : next + 1;
194
- next = pointer.find('/', pos);
195
- std::string def = pointer.substr(
196
- pos, next == std::string::npos ? next : next - pos);
197
- auto dit = ctx.defs.find("#/" + key + "/" + def);
198
- if (dit != ctx.defs.end()) current = dit->second;
199
- else { resolved = false; break; }
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
- pos = (next == std::string::npos) ? pointer.size() : next + 1;
202
- next = pointer.find('/', pos);
203
- std::string idx_s = pointer.substr(
204
- pos, next == std::string::npos ? next : next - pos);
205
- size_t idx = std::stoul(idx_s);
206
- auto& vec = (key == "allOf") ? current->all_of
207
- : (key == "anyOf") ? current->any_of : current->one_of;
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
- pos = (next == std::string::npos) ? pointer.size() : next + 1;
223
- next = pointer.find('/', pos);
224
- std::string idx_s = pointer.substr(
225
- pos, next == std::string::npos ? next : next - pos);
226
- size_t idx = std::stoul(idx_s);
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
- return;
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
- return;
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 (!std::regex_search(sv, *node->compiled_pattern)) {
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 = true;
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& [pat, pat_schema] : node->pattern_properties) {
497
- try {
498
- std::regex re(pat);
499
- if (std::regex_search(key_str, re)) {
500
- validate_napi(pat_schema, val, env, path + "/" + key_str, ctx,
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;