ata-validator 0.7.3 → 0.8.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/src/ata.cpp CHANGED
@@ -8,7 +8,9 @@
8
8
  #include <algorithm>
9
9
  #include <cmath>
10
10
  #include <cstring>
11
+ #ifndef ATA_NO_RE2
11
12
  #include <re2/re2.h>
13
+ #endif
12
14
  #include <set>
13
15
  #include <unordered_map>
14
16
 
@@ -53,11 +55,15 @@ static bool fast_check_email(std::string_view s) {
53
55
  }
54
56
 
55
57
  static bool fast_check_date(std::string_view s) {
56
- // YYYY-MM-DD
57
- return s.size() == 10 && is_digit(s[0]) && is_digit(s[1]) &&
58
- is_digit(s[2]) && is_digit(s[3]) && s[4] == '-' &&
59
- is_digit(s[5]) && is_digit(s[6]) && s[7] == '-' &&
60
- is_digit(s[8]) && is_digit(s[9]);
58
+ // YYYY-MM-DD with range validation
59
+ if (s.size() != 10 || !is_digit(s[0]) || !is_digit(s[1]) ||
60
+ !is_digit(s[2]) || !is_digit(s[3]) || s[4] != '-' ||
61
+ !is_digit(s[5]) || !is_digit(s[6]) || s[7] != '-' ||
62
+ !is_digit(s[8]) || !is_digit(s[9]))
63
+ return false;
64
+ int month = (s[5] - '0') * 10 + (s[6] - '0');
65
+ int day = (s[8] - '0') * 10 + (s[9] - '0');
66
+ return month >= 1 && month <= 12 && day >= 1 && day <= 31;
61
67
  }
62
68
 
63
69
  static bool fast_check_time(std::string_view s) {
@@ -274,7 +280,9 @@ struct schema_node {
274
280
  std::optional<uint64_t> min_length;
275
281
  std::optional<uint64_t> max_length;
276
282
  std::optional<std::string> pattern;
283
+ #ifndef ATA_NO_RE2
277
284
  std::shared_ptr<re2::RE2> compiled_pattern; // cached compiled regex (RE2)
285
+ #endif
278
286
 
279
287
  // array
280
288
  std::optional<uint64_t> min_items;
@@ -301,7 +309,9 @@ struct schema_node {
301
309
  struct pattern_prop {
302
310
  std::string pattern;
303
311
  schema_node_ptr schema;
312
+ #ifndef ATA_NO_RE2
304
313
  std::shared_ptr<re2::RE2> compiled;
314
+ #endif
305
315
  };
306
316
  std::vector<pattern_prop> pattern_properties;
307
317
 
@@ -326,6 +336,8 @@ struct schema_node {
326
336
 
327
337
  // $ref
328
338
  std::string ref;
339
+ std::string dynamic_ref; // $dynamicRef value (e.g. "#items")
340
+ std::string id; // $id — resource boundary marker
329
341
 
330
342
  // $defs — stored on node for pointer navigation
331
343
  std::unordered_map<std::string, schema_node_ptr> defs;
@@ -351,7 +363,9 @@ struct plan {
351
363
  std::vector<ins> code;
352
364
  std::vector<double> doubles;
353
365
  std::vector<std::string> strings;
366
+ #ifndef ATA_NO_RE2
354
367
  std::vector<std::shared_ptr<re2::RE2>> regexes;
368
+ #endif
355
369
  std::vector<std::vector<std::string>> enum_sets;
356
370
  std::vector<uint8_t> type_masks;
357
371
  std::vector<uint8_t> format_ids;
@@ -374,7 +388,9 @@ struct od_plan {
374
388
 
375
389
  // String — single value.get(sv) then all checks
376
390
  std::optional<uint64_t> min_length, max_length;
391
+ #ifndef ATA_NO_RE2
377
392
  re2::RE2* pattern = nullptr; // borrowed pointer from schema_node
393
+ #endif
378
394
  uint8_t format_id = 255; // 255 = no format check
379
395
 
380
396
  // Object — single iterate with merged required+property lookup
@@ -408,10 +424,18 @@ struct compiled_schema {
408
424
  schema_node_ptr root;
409
425
  std::unordered_map<std::string, schema_node_ptr> defs;
410
426
  std::string raw_schema;
427
+ std::string compile_error; // non-empty if compilation failed
411
428
  dom::parser parser; // used only at compile time
412
429
  cg::plan gen_plan; // codegen validation plan
413
430
  bool use_ondemand = false; // true if codegen plan supports On Demand
414
431
  od_plan_ptr od; // On-Demand execution plan
432
+
433
+ // anchor resolution
434
+ std::unordered_map<std::string, schema_node_ptr> anchors;
435
+ std::unordered_map<std::string,
436
+ std::unordered_map<std::string, schema_node_ptr>> resource_dynamic_anchors;
437
+ bool has_dynamic_refs = false;
438
+ std::string current_resource_id; // compile-time only
415
439
  };
416
440
 
417
441
  // Thread-local persistent parsers — reused across all validate calls on the
@@ -462,6 +486,64 @@ static schema_node_ptr compile_node(dom::element el,
462
486
  }
463
487
  }
464
488
 
489
+ // $id — must come before $anchor/$dynamicAnchor so current_resource_id is set
490
+ std::string prev_resource = ctx.current_resource_id;
491
+ {
492
+ dom::element id_el;
493
+ if (obj["$id"].get(id_el) == SUCCESS) {
494
+ std::string_view sv;
495
+ if (id_el.get(sv) == SUCCESS) {
496
+ node->id = std::string(sv);
497
+ ctx.current_resource_id = node->id;
498
+ ctx.defs[node->id] = node;
499
+ }
500
+ }
501
+ }
502
+
503
+ // $anchor — register in flat anchor map
504
+ {
505
+ dom::element anchor_el;
506
+ if (obj["$anchor"].get(anchor_el) == SUCCESS) {
507
+ std::string_view sv;
508
+ if (anchor_el.get(sv) == SUCCESS) {
509
+ ctx.anchors[std::string(sv)] = node;
510
+ }
511
+ }
512
+ }
513
+
514
+ // $dynamicAnchor — register in both flat anchors and per-resource map
515
+ {
516
+ dom::element da_el;
517
+ if (obj["$dynamicAnchor"].get(da_el) == SUCCESS) {
518
+ std::string_view sv;
519
+ if (da_el.get(sv) == SUCCESS) {
520
+ std::string name(sv);
521
+ ctx.anchors[name] = node;
522
+ ctx.resource_dynamic_anchors[ctx.current_resource_id][name] = node;
523
+ }
524
+ }
525
+ }
526
+
527
+ // $dynamicRef
528
+ {
529
+ dom::element dr_el;
530
+ if (obj["$dynamicRef"].get(dr_el) == SUCCESS) {
531
+ std::string_view sv;
532
+ if (dr_el.get(sv) == SUCCESS) {
533
+ std::string dr_val(sv);
534
+ // If the $dynamicRef starts with "#" (fragment-only) and we're inside
535
+ // a non-root resource, qualify it with the current resource ID so
536
+ // validation can resolve it correctly.
537
+ if (!dr_val.empty() && dr_val[0] == '#' &&
538
+ !ctx.current_resource_id.empty()) {
539
+ dr_val = ctx.current_resource_id + dr_val;
540
+ }
541
+ node->dynamic_ref = dr_val;
542
+ ctx.has_dynamic_refs = true;
543
+ }
544
+ }
545
+ }
546
+
465
547
  // type
466
548
  dom::element type_el;
467
549
  if (obj["type"].get(type_el) == SUCCESS) {
@@ -516,10 +598,15 @@ static schema_node_ptr compile_node(dom::element el,
516
598
  std::string_view sv;
517
599
  if (str_el.get(sv) == SUCCESS) {
518
600
  node->pattern = std::string(sv);
601
+ #ifdef ATA_NO_RE2
602
+ ctx.compile_error = "pattern keyword requires RE2 support (built with ATA_NO_RE2)";
603
+ return node;
604
+ #else
519
605
  auto re = std::make_shared<re2::RE2>(node->pattern.value());
520
606
  if (re->ok()) {
521
607
  node->compiled_pattern = std::move(re);
522
608
  }
609
+ #endif
523
610
  }
524
611
  }
525
612
 
@@ -636,6 +723,10 @@ static schema_node_ptr compile_node(dom::element el,
636
723
  dom::element pp_el;
637
724
  if (obj["patternProperties"].get(pp_el) == SUCCESS &&
638
725
  pp_el.is<dom::object>()) {
726
+ #ifdef ATA_NO_RE2
727
+ ctx.compile_error = "patternProperties keyword requires RE2 support (built with ATA_NO_RE2)";
728
+ return node;
729
+ #else
639
730
  dom::object pp_obj; pp_el.get(pp_obj);
640
731
  for (auto [key, val] : pp_obj) {
641
732
  schema_node::pattern_prop pp;
@@ -647,6 +738,7 @@ static schema_node_ptr compile_node(dom::element el,
647
738
  }
648
739
  node->pattern_properties.push_back(std::move(pp));
649
740
  }
741
+ #endif
650
742
  }
651
743
 
652
744
  // format
@@ -659,15 +751,6 @@ static schema_node_ptr compile_node(dom::element el,
659
751
  }
660
752
  }
661
753
 
662
- // $id (register in defs for potential resolution)
663
- dom::element id_el;
664
- if (obj["$id"].get(id_el) == SUCCESS) {
665
- std::string_view sv;
666
- if (id_el.get(sv) == SUCCESS) {
667
- ctx.defs[std::string(sv)] = node;
668
- }
669
- }
670
-
671
754
  // enum — pre-minify each value at compile time
672
755
  dom::element enum_el;
673
756
  if (obj["enum"].get(enum_el) == SUCCESS) {
@@ -743,17 +826,144 @@ static schema_node_ptr compile_node(dom::element el,
743
826
  }
744
827
  }
745
828
 
829
+ ctx.current_resource_id = prev_resource;
746
830
  return node;
747
831
  }
748
832
 
749
833
  // --- Validation ---
750
834
 
835
+ using dynamic_scope_t = std::vector<const std::unordered_map<std::string, schema_node_ptr>*>;
836
+
837
+ // Decode a single JSON Pointer segment (percent-decode, then ~1->/, ~0->~)
838
+ static std::string decode_pointer_segment(const std::string& seg) {
839
+ std::string pct;
840
+ for (size_t i = 0; i < seg.size(); ++i) {
841
+ if (seg[i] == '%' && i + 2 < seg.size()) {
842
+ auto hex = [](char c) -> int {
843
+ if (c >= '0' && c <= '9') return c - '0';
844
+ if (c >= 'a' && c <= 'f') return 10 + c - 'a';
845
+ if (c >= 'A' && c <= 'F') return 10 + c - 'A';
846
+ return -1;
847
+ };
848
+ int hv = hex(seg[i+1]), lv = hex(seg[i+2]);
849
+ if (hv >= 0 && lv >= 0) {
850
+ pct += static_cast<char>(hv * 16 + lv);
851
+ i += 2;
852
+ } else {
853
+ pct += seg[i];
854
+ }
855
+ } else {
856
+ pct += seg[i];
857
+ }
858
+ }
859
+ std::string out;
860
+ for (size_t i = 0; i < pct.size(); ++i) {
861
+ if (pct[i] == '~' && i + 1 < pct.size()) {
862
+ if (pct[i + 1] == '1') { out += '/'; ++i; }
863
+ else if (pct[i + 1] == '0') { out += '~'; ++i; }
864
+ else out += pct[i];
865
+ } else {
866
+ out += pct[i];
867
+ }
868
+ }
869
+ return out;
870
+ }
871
+
872
+ // Walk a JSON Pointer (without leading #) within a given schema node.
873
+ // Returns the resolved node, or nullptr if not found.
874
+ static schema_node_ptr walk_json_pointer(const schema_node_ptr& root_node,
875
+ const std::string& pointer) {
876
+ if (pointer.empty()) return root_node;
877
+
878
+ std::vector<std::string> segments;
879
+ size_t spos = 0;
880
+ // pointer starts with "/" — skip leading slash
881
+ if (!pointer.empty() && pointer[0] == '/') spos = 1;
882
+ while (spos <= pointer.size()) {
883
+ size_t snext = pointer.find('/', spos);
884
+ segments.push_back(decode_pointer_segment(
885
+ pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
886
+ spos = (snext == std::string::npos) ? pointer.size() + 1 : snext + 1;
887
+ }
888
+
889
+ schema_node_ptr current = root_node;
890
+ for (size_t si = 0; si < segments.size() && current; ++si) {
891
+ const auto& key = segments[si];
892
+ if (key == "properties" && si + 1 < segments.size()) {
893
+ const auto& prop_name = segments[++si];
894
+ auto pit = current->properties.find(prop_name);
895
+ if (pit != current->properties.end()) { current = pit->second; }
896
+ else { return nullptr; }
897
+ } else if (key == "items" && current->items_schema) {
898
+ current = current->items_schema;
899
+ } else if (key == "$defs" || key == "definitions") {
900
+ if (si + 1 < segments.size()) {
901
+ const auto& def_name = segments[++si];
902
+ auto dit = current->defs.find(def_name);
903
+ if (dit != current->defs.end()) { current = dit->second; }
904
+ else { return nullptr; }
905
+ } else { return nullptr; }
906
+ } else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
907
+ if (si + 1 < segments.size()) {
908
+ size_t idx = std::stoul(segments[++si]);
909
+ auto& vec = (key == "allOf") ? current->all_of
910
+ : (key == "anyOf") ? current->any_of
911
+ : current->one_of;
912
+ if (idx < vec.size()) { current = vec[idx]; }
913
+ else { return nullptr; }
914
+ } else { return nullptr; }
915
+ } else if (key == "not" && current->not_schema) {
916
+ current = current->not_schema;
917
+ } else if (key == "if" && current->if_schema) {
918
+ current = current->if_schema;
919
+ } else if (key == "then" && current->then_schema) {
920
+ current = current->then_schema;
921
+ } else if (key == "else" && current->else_schema) {
922
+ current = current->else_schema;
923
+ } else if (key == "additionalProperties" &&
924
+ current->additional_properties_schema) {
925
+ current = current->additional_properties_schema;
926
+ } else if (key == "prefixItems") {
927
+ if (si + 1 < segments.size()) {
928
+ size_t idx = std::stoul(segments[++si]);
929
+ if (idx < current->prefix_items.size()) { current = current->prefix_items[idx]; }
930
+ else { return nullptr; }
931
+ } else { return nullptr; }
932
+ } else if (key == "contains" && current->contains_schema) {
933
+ current = current->contains_schema;
934
+ } else if (key == "propertyNames" && current->property_names_schema) {
935
+ current = current->property_names_schema;
936
+ } else {
937
+ return nullptr;
938
+ }
939
+ }
940
+ return current;
941
+ }
942
+
943
+ // Find an anchor (non-pointer fragment) within a specific resource node by
944
+ // searching its sub-tree. Used for resolving "base#anchor" references.
945
+ static schema_node_ptr find_anchor_in_resource(const compiled_schema& ctx,
946
+ const std::string& resource_id,
947
+ const std::string& anchor_name) {
948
+ // Look up in per-resource dynamic anchors first
949
+ auto rit = ctx.resource_dynamic_anchors.find(resource_id);
950
+ if (rit != ctx.resource_dynamic_anchors.end()) {
951
+ auto ait = rit->second.find(anchor_name);
952
+ if (ait != rit->second.end()) return ait->second;
953
+ }
954
+ // Fallback to flat anchors (which includes $anchor entries)
955
+ auto ait = ctx.anchors.find(anchor_name);
956
+ if (ait != ctx.anchors.end()) return ait->second;
957
+ return nullptr;
958
+ }
959
+
751
960
  static void validate_node(const schema_node_ptr& node,
752
961
  dom::element value,
753
962
  const std::string& path,
754
963
  const compiled_schema& ctx,
755
964
  std::vector<validation_error>& errors,
756
- bool all_errors = true);
965
+ bool all_errors = true,
966
+ dynamic_scope_t* dynamic_scope = nullptr);
757
967
 
758
968
  // Fast boolean-only tree walker — no error collection, no string allocation.
759
969
  // Uses [[likely]]/[[unlikely]] hints. Returns true if valid.
@@ -808,14 +1018,27 @@ static uint64_t utf8_length(std::string_view s) {
808
1018
  return count;
809
1019
  }
810
1020
 
1021
+ // Recursion depth guard — prevents stack overflow on self-referencing schemas
1022
+ struct DepthGuard {
1023
+ static thread_local int depth;
1024
+ bool overflow;
1025
+ DepthGuard() : overflow(++depth > 100) {}
1026
+ ~DepthGuard() { --depth; }
1027
+ };
1028
+ thread_local int DepthGuard::depth = 0;
1029
+
811
1030
  static void validate_node(const schema_node_ptr& node,
812
1031
  dom::element value,
813
1032
  const std::string& path,
814
1033
  const compiled_schema& ctx,
815
1034
  std::vector<validation_error>& errors,
816
- bool all_errors) {
1035
+ bool all_errors,
1036
+ dynamic_scope_t* dynamic_scope) {
817
1037
  if (!node) return;
818
1038
 
1039
+ DepthGuard guard;
1040
+ if (guard.overflow) return;
1041
+
819
1042
  // Boolean schema
820
1043
  if (node->boolean_schema.has_value()) {
821
1044
  if (!node->boolean_schema.value()) {
@@ -825,136 +1048,116 @@ static void validate_node(const schema_node_ptr& node,
825
1048
  return;
826
1049
  }
827
1050
 
1051
+ // Dynamic scope tracking: push this resource's dynamic anchors
1052
+ bool pushed_scope = false;
1053
+ if (dynamic_scope && !node->id.empty()) {
1054
+ auto it = ctx.resource_dynamic_anchors.find(node->id);
1055
+ if (it != ctx.resource_dynamic_anchors.end()) {
1056
+ dynamic_scope->push_back(&it->second);
1057
+ pushed_scope = true;
1058
+ }
1059
+ }
1060
+
828
1061
  // $ref — Draft 2020-12: $ref is not a short-circuit, sibling keywords still apply
829
1062
  bool ref_resolved = false;
830
1063
  if (!node->ref.empty()) {
831
- // First check defs map
832
- auto it = ctx.defs.find(node->ref);
833
- if (it != ctx.defs.end()) {
834
- validate_node(it->second, value, path, ctx, errors, all_errors);
1064
+ // Self-reference: "#"
1065
+ if (node->ref == "#" && ctx.root) {
1066
+ validate_node(ctx.root, value, path, ctx, errors, all_errors, dynamic_scope);
835
1067
  ref_resolved = true;
836
1068
  }
837
- // Try JSON Pointer resolution from root (e.g., "#/properties/foo")
838
- if (node->ref.size() > 1 && node->ref[0] == '#' &&
839
- node->ref[1] == '/') {
840
- // Decode JSON Pointer segments
841
- auto decode_pointer_segment = [](const std::string& seg) -> std::string {
842
- // Percent-decode first
843
- std::string pct;
844
- for (size_t i = 0; i < seg.size(); ++i) {
845
- if (seg[i] == '%' && i + 2 < seg.size()) {
846
- char h = seg[i+1], l = seg[i+2];
847
- auto hex = [](char c) -> int {
848
- if (c >= '0' && c <= '9') return c - '0';
849
- if (c >= 'a' && c <= 'f') return 10 + c - 'a';
850
- if (c >= 'A' && c <= 'F') return 10 + c - 'A';
851
- return -1;
852
- };
853
- int hv = hex(h), lv = hex(l);
854
- if (hv >= 0 && lv >= 0) {
855
- pct += static_cast<char>(hv * 16 + lv);
856
- i += 2;
857
- } else {
858
- pct += seg[i];
859
- }
860
- } else {
861
- pct += seg[i];
862
- }
863
- }
864
- // Then JSON Pointer unescape: ~1 -> /, ~0 -> ~
865
- std::string out;
866
- for (size_t i = 0; i < pct.size(); ++i) {
867
- if (pct[i] == '~' && i + 1 < pct.size()) {
868
- if (pct[i + 1] == '1') { out += '/'; ++i; }
869
- else if (pct[i + 1] == '0') { out += '~'; ++i; }
870
- else out += pct[i];
871
- } else {
872
- out += pct[i];
1069
+ // Check for "base#fragment" pattern (e.g. "first#/$defs/stuff", "tree.json")
1070
+ if (!ref_resolved) {
1071
+ std::string base_uri;
1072
+ std::string fragment;
1073
+ size_t hash_pos = node->ref.find('#');
1074
+ if (hash_pos != std::string::npos) {
1075
+ base_uri = node->ref.substr(0, hash_pos);
1076
+ fragment = node->ref.substr(hash_pos + 1);
1077
+ } else {
1078
+ base_uri = node->ref;
1079
+ }
1080
+
1081
+ // Helper: push base resource's dynamic anchors to scope, validate, pop
1082
+ auto validate_with_resource_scope = [&](const schema_node_ptr& target,
1083
+ const std::string& resource_id) {
1084
+ bool scope_pushed = false;
1085
+ if (dynamic_scope && !resource_id.empty()) {
1086
+ auto rit = ctx.resource_dynamic_anchors.find(resource_id);
1087
+ if (rit != ctx.resource_dynamic_anchors.end()) {
1088
+ dynamic_scope->push_back(&rit->second);
1089
+ scope_pushed = true;
873
1090
  }
874
1091
  }
875
- return out;
1092
+ validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1093
+ if (scope_pushed) dynamic_scope->pop_back();
876
1094
  };
877
1095
 
878
- // Split pointer into segments
879
- std::string pointer = node->ref.substr(2);
880
- std::vector<std::string> segments;
881
- size_t spos = 0;
882
- while (spos < pointer.size()) {
883
- size_t snext = pointer.find('/', spos);
884
- segments.push_back(decode_pointer_segment(
885
- pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
886
- spos = (snext == std::string::npos) ? pointer.size() : snext + 1;
887
- }
888
-
889
- // Walk the schema tree
890
- schema_node_ptr current = ctx.root;
891
- bool resolved = true;
892
- for (size_t si = 0; si < segments.size() && current; ++si) {
893
- const auto& key = segments[si];
894
-
895
- if (key == "properties" && si + 1 < segments.size()) {
896
- const auto& prop_name = segments[++si];
897
- auto pit = current->properties.find(prop_name);
898
- if (pit != current->properties.end()) {
899
- current = pit->second;
900
- } else { resolved = false; break; }
901
- } else if (key == "items" && current->items_schema) {
902
- current = current->items_schema;
903
- } else if (key == "$defs" || key == "definitions") {
904
- if (si + 1 < segments.size()) {
905
- const auto& def_name = segments[++si];
906
- // Navigate into node's defs map
907
- auto dit = current->defs.find(def_name);
908
- if (dit != current->defs.end()) {
909
- current = dit->second;
1096
+ if (!base_uri.empty()) {
1097
+ // Resolve base URI to a resource via defs
1098
+ auto it = ctx.defs.find(base_uri);
1099
+ if (it != ctx.defs.end()) {
1100
+ schema_node_ptr target = it->second;
1101
+ if (!fragment.empty()) {
1102
+ if (fragment[0] == '/') {
1103
+ // JSON Pointer within the resource
1104
+ auto resolved = walk_json_pointer(target, fragment);
1105
+ if (resolved) {
1106
+ validate_with_resource_scope(resolved, base_uri);
1107
+ ref_resolved = true;
1108
+ }
910
1109
  } else {
911
- // Fallback: try ctx.defs with full path
912
- std::string full_ref = "#/" + key + "/" + def_name;
913
- auto cit = ctx.defs.find(full_ref);
914
- if (cit != ctx.defs.end()) {
915
- current = cit->second;
916
- } else { resolved = false; break; }
1110
+ // Anchor lookup within the resource
1111
+ auto resolved = find_anchor_in_resource(ctx, base_uri, fragment);
1112
+ if (resolved) {
1113
+ validate_with_resource_scope(resolved, base_uri);
1114
+ ref_resolved = true;
1115
+ }
917
1116
  }
918
- } else { resolved = false; break; }
919
- } else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
920
- if (si + 1 < segments.size()) {
921
- size_t idx = std::stoul(segments[++si]);
922
- auto& vec = (key == "allOf") ? current->all_of
923
- : (key == "anyOf") ? current->any_of
924
- : current->one_of;
925
- if (idx < vec.size()) { current = vec[idx]; }
926
- else { resolved = false; break; }
927
- } else { resolved = false; break; }
928
- } else if (key == "not" && current->not_schema) {
929
- current = current->not_schema;
930
- } else if (key == "if" && current->if_schema) {
931
- current = current->if_schema;
932
- } else if (key == "then" && current->then_schema) {
933
- current = current->then_schema;
934
- } else if (key == "else" && current->else_schema) {
935
- current = current->else_schema;
936
- } else if (key == "additionalProperties" &&
937
- current->additional_properties_schema) {
938
- current = current->additional_properties_schema;
939
- } else if (key == "prefixItems") {
940
- if (si + 1 < segments.size()) {
941
- size_t idx = std::stoul(segments[++si]);
942
- if (idx < current->prefix_items.size()) { current = current->prefix_items[idx]; }
943
- else { resolved = false; break; }
944
- } else { resolved = false; break; }
1117
+ } else {
1118
+ // No fragment, just the base resource (it pushes its own scope)
1119
+ validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1120
+ ref_resolved = true;
1121
+ }
1122
+ }
1123
+ } else if (!fragment.empty()) {
1124
+ // "#fragment" no base URI
1125
+ if (fragment[0] == '/') {
1126
+ // JSON Pointer from root
1127
+ auto resolved = walk_json_pointer(ctx.root, fragment);
1128
+ if (resolved) {
1129
+ validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
1130
+ ref_resolved = true;
1131
+ }
945
1132
  } else {
946
- resolved = false; break;
1133
+ // Anchor lookup
1134
+ auto ait = ctx.anchors.find(fragment);
1135
+ if (ait != ctx.anchors.end()) {
1136
+ validate_node(ait->second, value, path, ctx, errors, all_errors, dynamic_scope);
1137
+ ref_resolved = true;
1138
+ }
947
1139
  }
948
1140
  }
949
- if (resolved && current) {
950
- validate_node(current, value, path, ctx, errors, all_errors);
1141
+ }
1142
+ // Fallback: try defs map directly (handles bare $id references like "list")
1143
+ if (!ref_resolved) {
1144
+ auto it = ctx.defs.find(node->ref);
1145
+ if (it != ctx.defs.end()) {
1146
+ validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
951
1147
  ref_resolved = true;
952
1148
  }
953
1149
  }
954
- // Self-reference: "#"
955
- if (!ref_resolved && node->ref == "#" && ctx.root) {
956
- validate_node(ctx.root, value, path, ctx, errors, all_errors);
957
- ref_resolved = true;
1150
+ // Fallback: relative URI resolution — match ref against defs keys by suffix
1151
+ if (!ref_resolved && !node->ref.empty() && node->ref[0] != '#') {
1152
+ std::string suffix = "/" + node->ref;
1153
+ for (const auto& [key, def_node] : ctx.defs) {
1154
+ if (key.size() >= suffix.size() &&
1155
+ key.compare(key.size() - suffix.size(), suffix.size(), suffix) == 0) {
1156
+ validate_node(def_node, value, path, ctx, errors, all_errors, dynamic_scope);
1157
+ ref_resolved = true;
1158
+ break;
1159
+ }
1160
+ }
958
1161
  }
959
1162
  if (!ref_resolved) {
960
1163
  errors.push_back({error_code::ref_not_found, path,
@@ -962,6 +1165,132 @@ static void validate_node(const schema_node_ptr& node,
962
1165
  }
963
1166
  }
964
1167
 
1168
+ // $dynamicRef — Draft 2020-12 dynamic scope resolution
1169
+ if (!node->dynamic_ref.empty()) {
1170
+ bool dref_resolved = false;
1171
+
1172
+ // Parse the $dynamicRef value into base URI and fragment
1173
+ std::string dr_base;
1174
+ std::string dr_fragment;
1175
+ {
1176
+ size_t hash_pos = node->dynamic_ref.find('#');
1177
+ if (hash_pos != std::string::npos) {
1178
+ dr_base = node->dynamic_ref.substr(0, hash_pos);
1179
+ dr_fragment = node->dynamic_ref.substr(hash_pos + 1);
1180
+ } else {
1181
+ dr_base = node->dynamic_ref;
1182
+ }
1183
+ }
1184
+
1185
+ // Helper: push base resource's dynamic anchors to scope temporarily
1186
+ auto push_resource_scope = [&](const std::string& resource_id) -> bool {
1187
+ if (dynamic_scope && !resource_id.empty()) {
1188
+ auto rit = ctx.resource_dynamic_anchors.find(resource_id);
1189
+ if (rit != ctx.resource_dynamic_anchors.end()) {
1190
+ dynamic_scope->push_back(&rit->second);
1191
+ return true;
1192
+ }
1193
+ }
1194
+ return false;
1195
+ };
1196
+
1197
+ // If fragment is a JSON pointer (starts with /), resolve like $ref
1198
+ if (!dr_fragment.empty() && dr_fragment[0] == '/') {
1199
+ schema_node_ptr base_node = dr_base.empty() ? ctx.root : nullptr;
1200
+ if (!dr_base.empty()) {
1201
+ auto it = ctx.defs.find(dr_base);
1202
+ if (it != ctx.defs.end()) base_node = it->second;
1203
+ }
1204
+ if (base_node) {
1205
+ auto resolved = walk_json_pointer(base_node, dr_fragment);
1206
+ if (resolved) {
1207
+ bool dr_scope_pushed = push_resource_scope(dr_base);
1208
+ validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
1209
+ if (dr_scope_pushed) dynamic_scope->pop_back();
1210
+ dref_resolved = true;
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ // If fragment is an anchor name (not a JSON pointer)
1216
+ if (!dref_resolved && !dr_fragment.empty() && dr_fragment[0] != '/') {
1217
+ std::string anchor_name = dr_fragment;
1218
+
1219
+ // Initial resolution: find the anchor
1220
+ schema_node_ptr target = nullptr;
1221
+
1222
+ if (!dr_base.empty()) {
1223
+ // Resolve base URI first, then find anchor in that resource
1224
+ auto it = ctx.defs.find(dr_base);
1225
+ if (it != ctx.defs.end()) {
1226
+ target = find_anchor_in_resource(ctx, dr_base, anchor_name);
1227
+ }
1228
+ } else {
1229
+ // No base URI — look up in flat anchors map
1230
+ auto ait = ctx.anchors.find(anchor_name);
1231
+ if (ait != ctx.anchors.end()) {
1232
+ target = ait->second;
1233
+ }
1234
+ }
1235
+
1236
+ if (target) {
1237
+ // Check if the initially resolved target is itself a $dynamicAnchor
1238
+ // (the "bookend" requirement). Only do dynamic scope walk if the
1239
+ // initial target's resource has a $dynamicAnchor with this name.
1240
+ bool is_dynamic_at_initial = false;
1241
+ if (!dr_base.empty()) {
1242
+ // We resolved via a specific base URI
1243
+ auto rit = ctx.resource_dynamic_anchors.find(dr_base);
1244
+ if (rit != ctx.resource_dynamic_anchors.end() &&
1245
+ rit->second.count(anchor_name)) {
1246
+ is_dynamic_at_initial = true;
1247
+ }
1248
+ } else {
1249
+ // No base URI — check if ANY resource has this as $dynamicAnchor
1250
+ // and the target matches (i.e., the initially resolved node IS a
1251
+ // $dynamicAnchor node)
1252
+ for (const auto& [rid, rmap] : ctx.resource_dynamic_anchors) {
1253
+ auto ait2 = rmap.find(anchor_name);
1254
+ if (ait2 != rmap.end() && ait2->second == target) {
1255
+ is_dynamic_at_initial = true;
1256
+ break;
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ // Dynamic scope walk: find first override in dynamic scope
1262
+ if (is_dynamic_at_initial && dynamic_scope) {
1263
+ for (size_t i = 0; i < dynamic_scope->size(); ++i) {
1264
+ auto dit = (*dynamic_scope)[i]->find(anchor_name);
1265
+ if (dit != (*dynamic_scope)[i]->end()) {
1266
+ target = dit->second;
1267
+ break;
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ bool dr_scope_pushed = push_resource_scope(dr_base);
1273
+ validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1274
+ if (dr_scope_pushed) dynamic_scope->pop_back();
1275
+ dref_resolved = true;
1276
+ }
1277
+ }
1278
+
1279
+ // Bare $dynamicRef without fragment (unusual, but handle it)
1280
+ if (!dref_resolved && dr_fragment.empty() && !dr_base.empty()) {
1281
+ auto it = ctx.defs.find(dr_base);
1282
+ if (it != ctx.defs.end()) {
1283
+ validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
1284
+ dref_resolved = true;
1285
+ }
1286
+ }
1287
+
1288
+ if (!dref_resolved) {
1289
+ errors.push_back({error_code::ref_not_found, path,
1290
+ "cannot resolve $dynamicRef: " + node->dynamic_ref});
1291
+ }
1292
+ }
1293
+
965
1294
  // type
966
1295
  if (node->type_mask) {
967
1296
  if (!type_matches_mask(value, node->type_mask)) {
@@ -1061,6 +1390,7 @@ static void validate_node(const schema_node_ptr& node,
1061
1390
  " > maxLength " +
1062
1391
  std::to_string(node->max_length.value())});
1063
1392
  }
1393
+ #ifndef ATA_NO_RE2
1064
1394
  if (node->compiled_pattern) {
1065
1395
  if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern)) {
1066
1396
  errors.push_back({error_code::pattern_mismatch, path,
@@ -1068,6 +1398,7 @@ static void validate_node(const schema_node_ptr& node,
1068
1398
  node->pattern.value()});
1069
1399
  }
1070
1400
  }
1401
+ #endif
1071
1402
 
1072
1403
  if (node->format.has_value()) {
1073
1404
  if (!check_format_by_id(sv, node->format_id)) {
@@ -1139,10 +1470,10 @@ static void validate_node(const schema_node_ptr& node,
1139
1470
  for (auto item : arr) {
1140
1471
  if (idx < node->prefix_items.size()) {
1141
1472
  validate_node(node->prefix_items[idx], item,
1142
- path + "/" + std::to_string(idx), ctx, errors, all_errors);
1473
+ path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
1143
1474
  } else if (node->items_schema) {
1144
1475
  validate_node(node->items_schema, item,
1145
- path + "/" + std::to_string(idx), ctx, errors, all_errors);
1476
+ path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
1146
1477
  }
1147
1478
  ++idx;
1148
1479
  }
@@ -1209,16 +1540,18 @@ static void validate_node(const schema_node_ptr& node,
1209
1540
  // Check properties
1210
1541
  auto it = node->properties.find(key_str);
1211
1542
  if (it != node->properties.end()) {
1212
- validate_node(it->second, val, path + "/" + key_str, ctx, errors, all_errors);
1543
+ validate_node(it->second, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1213
1544
  matched = true;
1214
1545
  }
1215
1546
 
1216
1547
  // Check patternProperties (use cached compiled regex)
1217
1548
  for (const auto& pp : node->pattern_properties) {
1549
+ #ifndef ATA_NO_RE2
1218
1550
  if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
1219
- validate_node(pp.schema, val, path + "/" + key_str, ctx, errors, all_errors);
1551
+ validate_node(pp.schema, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1220
1552
  matched = true;
1221
1553
  }
1554
+ #endif
1222
1555
  }
1223
1556
 
1224
1557
  // additionalProperties (only if not matched by properties or patternProperties)
@@ -1230,7 +1563,7 @@ static void validate_node(const schema_node_ptr& node,
1230
1563
  "additional property not allowed: " + key_str});
1231
1564
  } else if (node->additional_properties_schema) {
1232
1565
  validate_node(node->additional_properties_schema, val,
1233
- path + "/" + key_str, ctx, errors);
1566
+ path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1234
1567
  }
1235
1568
  }
1236
1569
  }
@@ -1259,12 +1592,14 @@ static void validate_node(const schema_node_ptr& node,
1259
1592
  errors.push_back({error_code::max_length_violation, path,
1260
1593
  "propertyNames: key too long: " + std::string(key_sv)});
1261
1594
  }
1595
+ #ifndef ATA_NO_RE2
1262
1596
  if (pn->compiled_pattern) {
1263
1597
  if (!re2::RE2::PartialMatch(re2::StringPiece(key_sv.data(), key_sv.size()), *pn->compiled_pattern)) {
1264
1598
  errors.push_back({error_code::pattern_mismatch, path,
1265
1599
  "propertyNames: key does not match pattern: " + std::string(key_sv)});
1266
1600
  }
1267
1601
  }
1602
+ #endif
1268
1603
  if (pn->format.has_value() && !check_format_by_id(key_sv, pn->format_id)) {
1269
1604
  errors.push_back({error_code::format_mismatch, path,
1270
1605
  "propertyNames: key does not match format: " + std::string(key_sv)});
@@ -1276,7 +1611,7 @@ static void validate_node(const schema_node_ptr& node,
1276
1611
  std::string key_json = "\"" + std::string(key) + "\"";
1277
1612
  auto key_result = tl_dom_key_parser().parse(key_json);
1278
1613
  if (!key_result.error()) {
1279
- validate_node(pn, key_result.value(), path, ctx, errors, all_errors);
1614
+ validate_node(pn, key_result.value(), path, ctx, errors, all_errors, dynamic_scope);
1280
1615
  }
1281
1616
  }
1282
1617
  }
@@ -1301,7 +1636,7 @@ static void validate_node(const schema_node_ptr& node,
1301
1636
  for (const auto& [prop, schema] : node->dependent_schemas) {
1302
1637
  dom::element dummy;
1303
1638
  if (obj[prop].get(dummy) == SUCCESS) {
1304
- validate_node(schema, value, path, ctx, errors, all_errors);
1639
+ validate_node(schema, value, path, ctx, errors, all_errors, dynamic_scope);
1305
1640
  }
1306
1641
  }
1307
1642
  }
@@ -1310,7 +1645,7 @@ static void validate_node(const schema_node_ptr& node,
1310
1645
  if (!node->all_of.empty()) {
1311
1646
  for (const auto& sub : node->all_of) {
1312
1647
  std::vector<validation_error> sub_errors;
1313
- validate_node(sub, value, path, ctx, sub_errors, all_errors);
1648
+ validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1314
1649
  if (!sub_errors.empty()) {
1315
1650
  errors.push_back({error_code::all_of_failed, path,
1316
1651
  "allOf subschema failed"});
@@ -1324,7 +1659,7 @@ static void validate_node(const schema_node_ptr& node,
1324
1659
  bool any_valid = false;
1325
1660
  for (const auto& sub : node->any_of) {
1326
1661
  std::vector<validation_error> sub_errors;
1327
- validate_node(sub, value, path, ctx, sub_errors, all_errors);
1662
+ validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1328
1663
  if (sub_errors.empty()) {
1329
1664
  any_valid = true;
1330
1665
  break;
@@ -1341,7 +1676,7 @@ static void validate_node(const schema_node_ptr& node,
1341
1676
  int match_count = 0;
1342
1677
  for (const auto& sub : node->one_of) {
1343
1678
  std::vector<validation_error> sub_errors;
1344
- validate_node(sub, value, path, ctx, sub_errors, all_errors);
1679
+ validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1345
1680
  if (sub_errors.empty()) ++match_count;
1346
1681
  }
1347
1682
  if (match_count != 1) {
@@ -1354,7 +1689,7 @@ static void validate_node(const schema_node_ptr& node,
1354
1689
  // not
1355
1690
  if (node->not_schema) {
1356
1691
  std::vector<validation_error> sub_errors;
1357
- validate_node(node->not_schema, value, path, ctx, sub_errors, all_errors);
1692
+ validate_node(node->not_schema, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1358
1693
  if (sub_errors.empty()) {
1359
1694
  errors.push_back({error_code::not_failed, path,
1360
1695
  "value should not match 'not' schema"});
@@ -1364,19 +1699,21 @@ static void validate_node(const schema_node_ptr& node,
1364
1699
  // if/then/else
1365
1700
  if (node->if_schema) {
1366
1701
  std::vector<validation_error> if_errors;
1367
- validate_node(node->if_schema, value, path, ctx, if_errors, all_errors);
1702
+ validate_node(node->if_schema, value, path, ctx, if_errors, all_errors, dynamic_scope);
1368
1703
  if (if_errors.empty()) {
1369
1704
  // if passed → validate then
1370
1705
  if (node->then_schema) {
1371
- validate_node(node->then_schema, value, path, ctx, errors, all_errors);
1706
+ validate_node(node->then_schema, value, path, ctx, errors, all_errors, dynamic_scope);
1372
1707
  }
1373
1708
  } else {
1374
1709
  // if failed → validate else
1375
1710
  if (node->else_schema) {
1376
- validate_node(node->else_schema, value, path, ctx, errors, all_errors);
1711
+ validate_node(node->else_schema, value, path, ctx, errors, all_errors, dynamic_scope);
1377
1712
  }
1378
1713
  }
1379
1714
  }
1715
+
1716
+ if (pushed_scope) dynamic_scope->pop_back();
1380
1717
  }
1381
1718
 
1382
1719
  // Fast boolean-only tree walker — stripped of all error collection.
@@ -1387,14 +1724,27 @@ static bool validate_fast(const schema_node_ptr& node,
1387
1724
  const compiled_schema& ctx) {
1388
1725
  if (!node) [[unlikely]] return true;
1389
1726
 
1727
+ DepthGuard guard;
1728
+ if (guard.overflow) [[unlikely]] return true;
1729
+
1390
1730
  if (node->boolean_schema.has_value()) [[unlikely]]
1391
1731
  return node->boolean_schema.value();
1392
1732
 
1733
+ // $dynamicRef — bail to tree walker
1734
+ if (!node->dynamic_ref.empty()) [[unlikely]] return false;
1735
+
1393
1736
  // $ref
1394
1737
  if (!node->ref.empty()) [[unlikely]] {
1395
1738
  auto it = ctx.defs.find(node->ref);
1396
1739
  if (it != ctx.defs.end()) {
1397
1740
  if (!validate_fast(it->second, value, ctx)) return false;
1741
+ } else if (node->ref.size() > 1 && node->ref[0] == '#' && node->ref[1] != '/') {
1742
+ auto ait = ctx.anchors.find(node->ref.substr(1));
1743
+ if (ait != ctx.anchors.end()) {
1744
+ if (!validate_fast(ait->second, value, ctx)) return false;
1745
+ } else {
1746
+ return false;
1747
+ }
1398
1748
  } else if (node->ref == "#" && ctx.root) {
1399
1749
  if (!validate_fast(ctx.root, value, ctx)) return false;
1400
1750
  } else {
@@ -1444,10 +1794,12 @@ static bool validate_fast(const schema_node_ptr& node,
1444
1794
  uint64_t len = utf8_length(sv);
1445
1795
  if (node->min_length.has_value() && len < node->min_length.value()) return false;
1446
1796
  if (node->max_length.has_value() && len > node->max_length.value()) return false;
1797
+ #ifndef ATA_NO_RE2
1447
1798
  if (node->compiled_pattern) {
1448
1799
  if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern))
1449
1800
  return false;
1450
1801
  }
1802
+ #endif
1451
1803
  if (node->format.has_value() && !check_format_by_id(sv, node->format_id)) return false;
1452
1804
  }
1453
1805
 
@@ -1532,11 +1884,13 @@ static bool validate_fast(const schema_node_ptr& node,
1532
1884
  }
1533
1885
 
1534
1886
  for (const auto& pp : node->pattern_properties) {
1887
+ #ifndef ATA_NO_RE2
1535
1888
  if (pp.compiled && re2::RE2::PartialMatch(
1536
1889
  re2::StringPiece(key_sv.data(), key_sv.size()), *pp.compiled)) {
1537
1890
  if (!validate_fast(pp.schema, val, ctx)) return false;
1538
1891
  matched = true;
1539
1892
  }
1893
+ #endif
1540
1894
  }
1541
1895
 
1542
1896
  if (!matched) {
@@ -1615,8 +1969,9 @@ static void cg_compile(const schema_node* n, cg::plan& p,
1615
1969
  return;
1616
1970
  }
1617
1971
  // Composition fallback
1618
- if (!n->ref.empty() || !n->all_of.empty() || !n->any_of.empty() ||
1619
- !n->one_of.empty() || n->not_schema || n->if_schema) {
1972
+ if (!n->ref.empty() || !n->dynamic_ref.empty() || !n->all_of.empty() ||
1973
+ !n->any_of.empty() || !n->one_of.empty() || n->not_schema ||
1974
+ n->if_schema) {
1620
1975
  uintptr_t ptr = reinterpret_cast<uintptr_t>(n);
1621
1976
  out.push_back({cg::op::COMPOSITION, (uint32_t)(ptr & 0xFFFFFFFF),
1622
1977
  (uint32_t)((ptr >> 32) & 0xFFFFFFFF)});
@@ -1670,7 +2025,9 @@ static void cg_compile(const schema_node* n, cg::plan& p,
1670
2025
  // String
1671
2026
  if (n->min_length.has_value()) out.push_back({cg::op::CHECK_MIN_LENGTH,(uint32_t)*n->min_length});
1672
2027
  if (n->max_length.has_value()) out.push_back({cg::op::CHECK_MAX_LENGTH,(uint32_t)*n->max_length});
2028
+ #ifndef ATA_NO_RE2
1673
2029
  if (n->compiled_pattern) { uint32_t i=(uint32_t)p.regexes.size(); p.regexes.push_back(n->compiled_pattern); out.push_back({cg::op::CHECK_PATTERN,i}); }
2030
+ #endif
1674
2031
  if (n->format.has_value()) {
1675
2032
  uint32_t i=(uint32_t)p.format_ids.size();
1676
2033
  p.format_ids.push_back(n->format_id);
@@ -1744,7 +2101,11 @@ static bool cg_exec(const cg::plan& p, const std::vector<cg::ins>& code,
1744
2101
  case cg::op::CHECK_MULTIPLE_OF: if(t_numeric){double d=p.doubles[c.a],r=std::fmod(t_dval,d);if(std::abs(r)>1e-8&&std::abs(r-d)>1e-8)return false;} break;
1745
2102
  case cg::op::CHECK_MIN_LENGTH: if(t==et::STRING){std::string_view sv;value.get(sv);if(utf8_length(sv)<c.a)return false;} break;
1746
2103
  case cg::op::CHECK_MAX_LENGTH: if(t==et::STRING){std::string_view sv;value.get(sv);if(utf8_length(sv)>c.a)return false;} break;
2104
+ #ifndef ATA_NO_RE2
1747
2105
  case cg::op::CHECK_PATTERN: if(t==et::STRING){std::string_view sv;value.get(sv);if(!re2::RE2::PartialMatch(re2::StringPiece(sv.data(),sv.size()),*p.regexes[c.a]))return false;} break;
2106
+ #else
2107
+ case cg::op::CHECK_PATTERN: break;
2108
+ #endif
1748
2109
  case cg::op::CHECK_FORMAT: if(t==et::STRING){std::string_view sv;value.get(sv);if(!check_format_by_id(sv,p.format_ids[c.a]))return false;} break;
1749
2110
  case cg::op::CHECK_MIN_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);uint64_t s=0;for([[maybe_unused]]auto _:a)++s;if(s<c.a)return false;} break;
1750
2111
  case cg::op::CHECK_MAX_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);uint64_t s=0;for([[maybe_unused]]auto _:a)++s;if(s>c.a)return false;} break;
@@ -1854,7 +2215,11 @@ static bool od_exec(const cg::plan& p, const std::vector<cg::ins>& code,
1854
2215
  }
1855
2216
  case cg::op::CHECK_MIN_LENGTH: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(utf8_length(sv)<c.a) return false;} break;
1856
2217
  case cg::op::CHECK_MAX_LENGTH: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(utf8_length(sv)>c.a) return false;} break;
2218
+ #ifndef ATA_NO_RE2
1857
2219
  case cg::op::CHECK_PATTERN: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(!re2::RE2::PartialMatch(re2::StringPiece(sv.data(),sv.size()),*p.regexes[c.a]))return false;} break;
2220
+ #else
2221
+ case cg::op::CHECK_PATTERN: break;
2222
+ #endif
1858
2223
  case cg::op::CHECK_FORMAT: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(!check_format_by_id(sv,p.format_ids[c.a]))return false;} break;
1859
2224
  case cg::op::CHECK_MIN_ITEMS: if(t==json_type::array){
1860
2225
  simdjson::ondemand::array a; if(value.get(a)!=SUCCESS) return false;
@@ -2012,7 +2377,9 @@ static od_plan_ptr compile_od_plan(const schema_node_ptr& node) {
2012
2377
  if (node->multiple_of) { plan->num_flags |= od_plan::HAS_MUL; plan->num_mul = *node->multiple_of; }
2013
2378
  plan->min_length = node->min_length;
2014
2379
  plan->max_length = node->max_length;
2380
+ #ifndef ATA_NO_RE2
2015
2381
  plan->pattern = node->compiled_pattern.get();
2382
+ #endif
2016
2383
  plan->format_id = node->format_id;
2017
2384
 
2018
2385
  // Object plan — build hash lookup for O(1) per-field dispatch
@@ -2151,10 +2518,12 @@ static bool od_exec_plan(const od_plan& plan, simdjson::ondemand::value value) {
2151
2518
  if (plan.min_length && len < *plan.min_length) return false;
2152
2519
  if (plan.max_length && len > *plan.max_length) return false;
2153
2520
  }
2521
+ #ifndef ATA_NO_RE2
2154
2522
  if (plan.pattern) {
2155
2523
  if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *plan.pattern))
2156
2524
  return false;
2157
2525
  }
2526
+ #endif
2158
2527
  if (plan.format_id != 255) {
2159
2528
  if (!check_format_by_id(sv, plan.format_id)) return false;
2160
2529
  }
@@ -2235,6 +2604,10 @@ schema_ref compile(std::string_view schema_json) {
2235
2604
 
2236
2605
  ctx->root = compile_node(doc, *ctx);
2237
2606
 
2607
+ if (!ctx->compile_error.empty()) {
2608
+ return schema_ref{nullptr};
2609
+ }
2610
+
2238
2611
  // Generate codegen plan
2239
2612
  cg_compile(ctx->root.get(), ctx->gen_plan, ctx->gen_plan.code);
2240
2613
  ctx->gen_plan.code.push_back({cg::op::END});
@@ -2290,8 +2663,24 @@ validation_result validate(const schema_ref& schema, std::string_view json,
2290
2663
 
2291
2664
  // Slow path: tree walker with error details (reuse already-parsed DOM)
2292
2665
  std::vector<validation_error> errors;
2293
- validate_node(schema.impl->root, result.value(), "", *schema.impl, errors,
2294
- opts.all_errors);
2666
+ if (schema.impl->has_dynamic_refs) {
2667
+ dynamic_scope_t scope;
2668
+ auto rit = schema.impl->resource_dynamic_anchors.find("");
2669
+ if (rit != schema.impl->resource_dynamic_anchors.end()) {
2670
+ scope.push_back(&rit->second);
2671
+ }
2672
+ if (!schema.impl->root->id.empty()) {
2673
+ auto iit = schema.impl->resource_dynamic_anchors.find(schema.impl->root->id);
2674
+ if (iit != schema.impl->resource_dynamic_anchors.end()) {
2675
+ scope.push_back(&iit->second);
2676
+ }
2677
+ }
2678
+ validate_node(schema.impl->root, result.value(), "", *schema.impl, errors,
2679
+ opts.all_errors, &scope);
2680
+ } else {
2681
+ validate_node(schema.impl->root, result.value(), "", *schema.impl, errors,
2682
+ opts.all_errors);
2683
+ }
2295
2684
 
2296
2685
  return {errors.empty(), std::move(errors)};
2297
2686
  }