jsonschema_rs 0.48.5 → 0.49.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.
data/src/lib.rs CHANGED
@@ -15,13 +15,21 @@ mod ser;
15
15
  mod static_id;
16
16
  mod validator_map;
17
17
 
18
- use jsonschema::{paths::LocationSegment, ValidationOptions};
18
+ use jsonschema::{
19
+ json::{
20
+ magnus_child, magnus_invalidate_members_cache, magnus_is_object, magnus_probe_root,
21
+ magnus_take_pending_error, Magnus, MagnusPendingErrorScope, PendingError, RbNode,
22
+ },
23
+ paths::LocationSegment,
24
+ ValidationOptions,
25
+ };
19
26
  use magnus::{
20
27
  error::ErrorType,
21
28
  function,
22
29
  gc::{register_address, register_mark_object, unregister_address},
23
30
  method,
24
31
  prelude::*,
32
+ rb_sys::AsRawValue,
25
33
  scan_args::scan_args,
26
34
  value::{Lazy, ReprValue},
27
35
  DataTypeFunctions, Error, Exception, ExceptionClass, RClass, RHash, RModule, RObject, RString,
@@ -45,7 +53,7 @@ use crate::{
45
53
  },
46
54
  registry::Registry,
47
55
  retriever::{retriever_error_message, RubyRetriever},
48
- ser::{to_canonical_string, to_schema_value, to_value},
56
+ ser::{to_canonical_string, to_schema_value},
49
57
  static_id::define_rb_intern,
50
58
  };
51
59
 
@@ -79,14 +87,14 @@ define_rb_intern!(static ID_SYM_EVALUATION_PATH_POINTER: "evaluation_path_pointe
79
87
  define_rb_intern!(static ID_SYM_ABSOLUTE_KEYWORD_LOCATION: "absolute_keyword_location");
80
88
 
81
89
  struct BuiltValidator {
82
- validator: jsonschema::Validator,
90
+ validator: jsonschema::Validator<Magnus>,
83
91
  callback_roots: CallbackRoots,
84
92
  compilation_roots: CompilationRootsRef,
85
93
  }
86
94
 
87
95
  fn build_validator(
88
96
  ruby: &Ruby,
89
- options: ValidationOptions,
97
+ options: ValidationOptions<'_, Arc<dyn jsonschema::Retrieve>, Magnus>,
90
98
  registry: Option<&jsonschema::Registry<'_>>,
91
99
  retriever: Option<RubyRetriever>,
92
100
  callback_roots: CallbackRoots,
@@ -279,7 +287,7 @@ fn build_verbose_message(
279
287
  mut message: String,
280
288
  schema_path: &jsonschema::paths::Location,
281
289
  instance_path: &jsonschema::paths::Location,
282
- root_instance: Option<&serde_json::Value>,
290
+ root_instance: Option<RbNode<'_>>,
283
291
  failing_instance: &serde_json::Value,
284
292
  mask: Option<&str>,
285
293
  ) -> String {
@@ -347,21 +355,14 @@ fn build_verbose_message(
347
355
  let segment = unescape_segment(segment);
348
356
  let segment = segment.as_ref();
349
357
  let is_index = match current {
350
- Some(serde_json::Value::Object(_)) => false,
358
+ Some(node) if magnus_is_object(node) => false,
351
359
  _ => is_index_segment(segment),
352
360
  };
353
361
  message.push('[');
354
362
  push_segment(&mut message, segment, is_index);
355
363
  message.push(']');
356
364
 
357
- current = match (current, is_index) {
358
- (Some(serde_json::Value::Array(values)), true) => segment
359
- .parse::<usize>()
360
- .ok()
361
- .and_then(|idx| values.get(idx)),
362
- (Some(serde_json::Value::Object(values)), false) => values.get(segment),
363
- _ => None,
364
- };
365
+ current = current.and_then(|node| magnus_child(node, segment, is_index));
365
366
  }
366
367
  message.push_str(":\n ");
367
368
 
@@ -388,7 +389,7 @@ fn error_message(error: &jsonschema::ValidationError<'_>, mask: Option<&str>) ->
388
389
  fn into_ruby_error(
389
390
  ruby: &Ruby,
390
391
  error: jsonschema::ValidationError<'_>,
391
- root_instance: Option<&serde_json::Value>,
392
+ root_instance: Option<RbNode<'_>>,
392
393
  message: &str,
393
394
  mask: Option<&str>,
394
395
  ) -> Result<Value, Error> {
@@ -470,7 +471,7 @@ fn into_ruby_error(
470
471
  fn to_ruby_error_value(
471
472
  ruby: &Ruby,
472
473
  error: jsonschema::ValidationError<'_>,
473
- root_instance: Option<&serde_json::Value>,
474
+ root_instance: Option<RbNode<'_>>,
474
475
  mask: Option<&str>,
475
476
  ) -> Result<Value, Error> {
476
477
  let message = error_message(&error, mask);
@@ -485,7 +486,7 @@ pub(crate) fn referencing_error(ruby: &Ruby, message: String) -> Error {
485
486
  fn raise_validation_error(
486
487
  ruby: &Ruby,
487
488
  error: jsonschema::ValidationError<'_>,
488
- root_instance: Option<&serde_json::Value>,
489
+ root_instance: Option<RbNode<'_>>,
489
490
  mask: Option<&str>,
490
491
  ) -> Error {
491
492
  let message = error_message(&error, mask);
@@ -547,60 +548,36 @@ fn handle_callback_panic(ruby: &Ruby, err: Box<dyn std::any::Any + Send>) -> Err
547
548
  })
548
549
  }
549
550
 
550
- #[allow(clippy::needless_pass_by_value)]
551
- fn handle_without_gvl_panic(ruby: &Ruby, err: Box<dyn std::any::Any + Send>) -> Error {
552
- let msg = if let Some(s) = err.downcast_ref::<&str>() {
553
- format!("Validation panicked: {s}")
554
- } else if let Some(s) = err.downcast_ref::<String>() {
555
- format!("Validation panicked: {s}")
556
- } else {
557
- "Validation panicked".to_string()
558
- };
559
- Error::new(ruby.exception_runtime_error(), msg)
551
+ fn pending_error_to_ruby(ruby: &Ruby, error: PendingError) -> Error {
552
+ match error {
553
+ PendingError::Type(message) => Error::new(ruby.exception_type_error(), message),
554
+ PendingError::Encoding(message) => Error::new(ruby.exception_encoding_error(), message),
555
+ PendingError::Argument(message) => Error::new(ruby.exception_arg_error(), message),
556
+ }
560
557
  }
561
558
 
562
- /// Run a closure without holding the Ruby GVL.
559
+ /// Run validation over `instance` in place, raising anything the representation could only record.
563
560
  ///
564
- /// The closure runs on the same thread, just without the GVL held,
565
- /// allowing other Ruby threads to proceed. The closure MUST NOT
566
- /// access any Ruby objects or call any Ruby API.
567
- ///
568
- /// # Safety
569
- /// Caller must ensure the closure does not interact with Ruby.
570
- #[allow(unsafe_code)]
571
- unsafe fn without_gvl<F, R>(f: F) -> Result<R, Box<dyn std::any::Any + Send>>
572
- where
573
- F: FnMut() -> R,
574
- {
575
- struct Payload<F, R> {
576
- f: F,
577
- result: std::mem::MaybeUninit<Result<R, Box<dyn std::any::Any + Send>>>,
561
+ /// Accessors cannot fail, so a value the representation cannot read is stashed and surfaced here.
562
+ fn surface_pending_errors<T>(
563
+ ruby: &Ruby,
564
+ instance: Value,
565
+ run: impl FnOnce(RbNode<'_>) -> Result<T, Error>,
566
+ ) -> Result<T, Error> {
567
+ let _scope = MagnusPendingErrorScope::enter();
568
+ // Ruby is free to mutate the instance between calls, so no snapshot from an earlier one holds.
569
+ magnus_invalidate_members_cache();
570
+ #[allow(unsafe_code)]
571
+ let node = RbNode::new(instance.as_raw());
572
+ magnus_probe_root(node);
573
+ if let Some(error) = magnus_take_pending_error() {
574
+ return Err(pending_error_to_ruby(ruby, error));
578
575
  }
579
-
580
- unsafe extern "C" fn call<F: FnMut() -> R, R>(
581
- data: *mut std::ffi::c_void,
582
- ) -> *mut std::ffi::c_void {
583
- let payload = unsafe { &mut *data.cast::<Payload<F, R>>() };
584
- let result = panic::catch_unwind(AssertUnwindSafe(|| (payload.f)()));
585
- payload.result.write(result);
586
- std::ptr::null_mut()
576
+ let result = run(node)?;
577
+ if let Some(error) = magnus_take_pending_error() {
578
+ return Err(pending_error_to_ruby(ruby, error));
587
579
  }
588
-
589
- let mut payload = Payload {
590
- f,
591
- result: std::mem::MaybeUninit::uninit(),
592
- };
593
-
594
- unsafe {
595
- rb_sys::rb_thread_call_without_gvl(
596
- Some(call::<F, R>),
597
- (&raw mut payload).cast::<std::ffi::c_void>(),
598
- None,
599
- std::ptr::null_mut(),
600
- )
601
- };
602
-
603
- unsafe { payload.result.assume_init() }
580
+ Ok(result)
604
581
  }
605
582
 
606
583
  /// Wrapper around `jsonschema::Validator`.
@@ -611,9 +588,8 @@ where
611
588
  #[derive(magnus::TypedData)]
612
589
  #[magnus(class = "JSONSchema::Validator", free_immediately, size, mark)]
613
590
  pub struct Validator {
614
- validator: jsonschema::Validator,
591
+ validator: jsonschema::Validator<Magnus>,
615
592
  mask: Option<String>,
616
- has_ruby_callbacks: bool,
617
593
  /// Marked during Ruby's GC mark phase to keep runtime callbacks alive.
618
594
  callback_roots: CallbackRoots,
619
595
  /// Protects callbacks via `register_address` during schema compilation —
@@ -630,16 +606,14 @@ impl DataTypeFunctions for Validator {
630
606
 
631
607
  impl Validator {
632
608
  pub(crate) fn from_jsonschema_with_roots(
633
- validator: jsonschema::Validator,
609
+ validator: jsonschema::Validator<Magnus>,
634
610
  mask: Option<String>,
635
- has_ruby_callbacks: bool,
636
611
  callback_roots: CallbackRoots,
637
612
  compilation_roots: CompilationRootsRef,
638
613
  ) -> Self {
639
614
  Self {
640
615
  validator,
641
616
  mask,
642
- has_ruby_callbacks,
643
617
  callback_roots,
644
618
  _compilation_roots: compilation_roots,
645
619
  }
@@ -656,70 +630,37 @@ impl Validator {
656
630
  }
657
631
  }
658
632
 
659
- #[allow(unsafe_code)]
660
633
  fn is_valid(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<bool, Error> {
661
- let json_instance = to_value(ruby, instance)?;
662
-
663
- if rb_self.has_ruby_callbacks {
664
- let result = catch_unwind_silent(AssertUnwindSafe(|| {
665
- rb_self.validator.is_valid(&json_instance)
666
- }));
667
- match result {
668
- Ok(valid) => Ok(valid),
669
- Err(err) => Err(handle_callback_panic(ruby, err)),
670
- }
671
- } else {
672
- // SAFETY: validation is pure Rust with no Ruby callbacks
673
- match unsafe { without_gvl(|| rb_self.validator.is_valid(&json_instance)) } {
674
- Ok(valid) => Ok(valid),
675
- Err(err) => Err(handle_without_gvl_panic(ruby, err)),
676
- }
677
- }
634
+ surface_pending_errors(ruby, instance, |node| {
635
+ let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.is_valid(node)));
636
+ result.map_err(|err| handle_callback_panic(ruby, err))
637
+ })
678
638
  }
679
639
 
680
- #[allow(unsafe_code)]
681
640
  fn validate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<(), Error> {
682
- let _scope = KeywordCauseScope::enter();
683
- let json_instance = to_value(ruby, instance)?;
684
-
685
- if rb_self.has_ruby_callbacks {
686
- let result = catch_unwind_silent(AssertUnwindSafe(|| {
687
- rb_self.validator.validate(&json_instance)
688
- }));
641
+ surface_pending_errors(ruby, instance, |node| {
642
+ let _scope = KeywordCauseScope::enter();
643
+ let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.validate(node)));
689
644
  match result {
690
645
  Ok(Ok(())) => Ok(()),
691
646
  Ok(Err(error)) => Err(raise_validation_error(
692
647
  ruby,
693
648
  error,
694
- Some(&json_instance),
649
+ Some(node),
695
650
  rb_self.mask.as_deref(),
696
651
  )),
697
652
  Err(err) => Err(handle_callback_panic(ruby, err)),
698
653
  }
699
- } else {
700
- // SAFETY: validation is pure Rust with no Ruby callbacks
701
- match unsafe { without_gvl(|| rb_self.validator.validate(&json_instance)) } {
702
- Ok(Ok(())) => Ok(()),
703
- Ok(Err(error)) => Err(raise_validation_error(
704
- ruby,
705
- error,
706
- Some(&json_instance),
707
- rb_self.mask.as_deref(),
708
- )),
709
- Err(err) => Err(handle_without_gvl_panic(ruby, err)),
710
- }
711
- }
654
+ })
712
655
  }
713
656
 
714
- #[allow(unsafe_code)]
715
657
  fn iter_errors(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Value, Error> {
716
- let _scope = KeywordCauseScope::enter();
717
- let json_instance = to_value(ruby, instance)?;
658
+ surface_pending_errors(ruby, instance, |node| {
659
+ let _scope = KeywordCauseScope::enter();
718
660
 
719
- if ruby.block_given() {
720
- // Lazy path: yield errors one at a time to the block
721
- if rb_self.has_ruby_callbacks {
722
- let mut iter = rb_self.validator.iter_errors(&json_instance);
661
+ if ruby.block_given() {
662
+ // Lazy path: yield errors one at a time to the block
663
+ let mut iter = rb_self.validator.iter_errors(node);
723
664
  loop {
724
665
  let result = catch_unwind_silent(AssertUnwindSafe(|| iter.next()));
725
666
  match result {
@@ -727,7 +668,7 @@ impl Validator {
727
668
  let ruby_error = to_ruby_error_value(
728
669
  ruby,
729
670
  error,
730
- Some(&json_instance),
671
+ Some(node),
731
672
  rb_self.mask.as_deref(),
732
673
  )?;
733
674
  ruby.yield_value::<Value, Value>(ruby_error)?;
@@ -736,34 +677,20 @@ impl Validator {
736
677
  Err(err) => return Err(handle_callback_panic(ruby, err)),
737
678
  }
738
679
  }
739
- } else {
740
- for error in rb_self.validator.iter_errors(&json_instance) {
741
- let ruby_error = to_ruby_error_value(
742
- ruby,
743
- error,
744
- Some(&json_instance),
745
- rb_self.mask.as_deref(),
746
- )?;
747
- ruby.yield_value::<Value, Value>(ruby_error)?;
748
- }
680
+ return Ok(ruby.qnil().as_value());
749
681
  }
750
- Ok(ruby.qnil().as_value())
751
- } else if rb_self.has_ruby_callbacks {
752
- // Eager path with callbacks
682
+
753
683
  let result = catch_unwind_silent(AssertUnwindSafe(|| {
754
- rb_self
755
- .validator
756
- .iter_errors(&json_instance)
757
- .collect::<Vec<_>>()
684
+ rb_self.validator.iter_errors(node).collect::<Vec<_>>()
758
685
  }));
759
686
  match result {
760
687
  Ok(errors) => {
761
688
  let arr = ruby.ary_new_capa(errors.len());
762
- for e in errors {
689
+ for error in errors {
763
690
  arr.push(to_ruby_error_value(
764
691
  ruby,
765
- e,
766
- Some(&json_instance),
692
+ error,
693
+ Some(node),
767
694
  rb_self.mask.as_deref(),
768
695
  )?)?;
769
696
  }
@@ -771,55 +698,18 @@ impl Validator {
771
698
  }
772
699
  Err(err) => Err(handle_callback_panic(ruby, err)),
773
700
  }
774
- } else {
775
- // Eager path without callbacks — release GVL
776
- // SAFETY: validation is pure Rust with no Ruby callbacks
777
- let errors = match unsafe {
778
- without_gvl(|| {
779
- rb_self
780
- .validator
781
- .iter_errors(&json_instance)
782
- .collect::<Vec<_>>()
783
- })
784
- } {
785
- Ok(errors) => errors,
786
- Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
787
- };
788
- let arr = ruby.ary_new_capa(errors.len());
789
- for e in errors {
790
- arr.push(to_ruby_error_value(
791
- ruby,
792
- e,
793
- Some(&json_instance),
794
- rb_self.mask.as_deref(),
795
- )?)?;
796
- }
797
- Ok(arr.as_value())
798
- }
701
+ })
799
702
  }
800
703
 
801
- #[allow(unsafe_code)]
802
704
  fn evaluate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Evaluation, Error> {
803
- let _scope = KeywordCauseScope::enter();
804
- let json_instance = to_value(ruby, instance)?;
805
-
806
- if rb_self.has_ruby_callbacks {
807
- let result = catch_unwind_silent(AssertUnwindSafe(|| {
808
- rb_self.validator.evaluate(&json_instance)
809
- }));
705
+ surface_pending_errors(ruby, instance, |node| {
706
+ let _scope = KeywordCauseScope::enter();
707
+ let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.evaluate(node)));
810
708
  match result {
811
709
  Ok(output) => Ok(Evaluation::new(output)),
812
710
  Err(err) => Err(handle_callback_panic(ruby, err)),
813
711
  }
814
- } else {
815
- // SAFETY: validation is pure Rust with no Ruby callbacks
816
- let output = match unsafe { without_gvl(|| rb_self.validator.evaluate(&json_instance)) }
817
- {
818
- Ok(output) => output,
819
- Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
820
- };
821
- Ok(Evaluation::new(output))
822
- }
712
+ })
823
713
  }
824
714
 
825
715
  fn inspect(&self) -> String {
@@ -892,7 +782,6 @@ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
892
782
 
893
783
  let json_schema = to_schema_value(ruby, schema)?;
894
784
  let parsed = build_parsed_options(ruby, kw, None)?;
895
- let has_ruby_callbacks = parsed.has_ruby_callbacks;
896
785
  let BuiltValidator {
897
786
  validator,
898
787
  callback_roots,
@@ -909,7 +798,6 @@ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
909
798
  Ok(Validator {
910
799
  validator,
911
800
  mask: parsed.mask,
912
- has_ruby_callbacks,
913
801
  callback_roots,
914
802
  _compilation_roots: compilation_roots,
915
803
  })
@@ -922,7 +810,6 @@ fn validator_map_for(ruby: &Ruby, args: &[Value]) -> Result<validator_map::Valid
922
810
 
923
811
  let json_schema = to_schema_value(ruby, schema)?;
924
812
  let parsed = build_parsed_options(ruby, kw, None)?;
925
- let has_ruby_callbacks = parsed.has_ruby_callbacks;
926
813
  let callback_roots = parsed.callback_roots;
927
814
  let compilation_roots = parsed.compilation_roots;
928
815
  let mask = parsed.mask;
@@ -938,7 +825,6 @@ fn validator_map_for(ruby: &Ruby, args: &[Value]) -> Result<validator_map::Valid
938
825
  match options.build_map(&json_schema) {
939
826
  Ok(inner) => Ok(validator_map::ValidatorMap {
940
827
  inner,
941
- has_ruby_callbacks,
942
828
  callback_roots,
943
829
  compilation_roots,
944
830
  mask,
@@ -1013,14 +899,12 @@ fn canonical_json_to_string(ruby: &Ruby, object: Value) -> Result<String, Error>
1013
899
  to_canonical_string(ruby, object)
1014
900
  }
1015
901
 
1016
- #[allow(unsafe_code)]
1017
902
  fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1018
903
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
1019
904
  let (schema, instance) = parsed_args.required;
1020
905
  let kw = extract_kwargs(ruby, parsed_args.keywords)?;
1021
906
 
1022
907
  let json_schema = to_schema_value(ruby, schema)?;
1023
- let json_instance = to_value(ruby, instance)?;
1024
908
  let parsed = build_parsed_options(ruby, kw, None)?;
1025
909
  let has_ruby_callbacks = parsed.has_ruby_callbacks;
1026
910
  let BuiltValidator {
@@ -1037,23 +921,14 @@ fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1037
921
  &json_schema,
1038
922
  )?;
1039
923
 
1040
- if has_ruby_callbacks {
1041
- let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1042
- let result = catch_unwind_silent(AssertUnwindSafe(|| validator.is_valid(&json_instance)));
1043
- match result {
1044
- Ok(valid) => Ok(valid),
1045
- Err(err) => Err(handle_callback_panic(ruby, err)),
1046
- }
1047
- } else {
1048
- // SAFETY: validation is pure Rust with no Ruby callbacks
1049
- match unsafe { without_gvl(|| validator.is_valid(&json_instance)) } {
1050
- Ok(valid) => Ok(valid),
1051
- Err(err) => Err(handle_without_gvl_panic(ruby, err)),
1052
- }
1053
- }
924
+ surface_pending_errors(ruby, instance, |node| {
925
+ let _callback_roots =
926
+ has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
927
+ catch_unwind_silent(AssertUnwindSafe(|| validator.is_valid(node)))
928
+ .map_err(|err| handle_callback_panic(ruby, err))
929
+ })
1054
930
  }
1055
931
 
1056
- #[allow(unsafe_code)]
1057
932
  fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1058
933
  let _scope = KeywordCauseScope::enter();
1059
934
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
@@ -1061,7 +936,6 @@ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1061
936
  let kw = extract_kwargs(ruby, parsed_args.keywords)?;
1062
937
 
1063
938
  let json_schema = to_schema_value(ruby, schema)?;
1064
- let json_instance = to_value(ruby, instance)?;
1065
939
  let parsed = build_parsed_options(ruby, kw, None)?;
1066
940
  let has_ruby_callbacks = parsed.has_ruby_callbacks;
1067
941
  let BuiltValidator {
@@ -1078,35 +952,22 @@ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1078
952
  &json_schema,
1079
953
  )?;
1080
954
 
1081
- if has_ruby_callbacks {
1082
- let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1083
- let result = catch_unwind_silent(AssertUnwindSafe(|| validator.validate(&json_instance)));
1084
- match result {
955
+ surface_pending_errors(ruby, instance, |node| {
956
+ let _callback_roots =
957
+ has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
958
+ match catch_unwind_silent(AssertUnwindSafe(|| validator.validate(node))) {
1085
959
  Ok(Ok(())) => Ok(()),
1086
960
  Ok(Err(error)) => Err(raise_validation_error(
1087
961
  ruby,
1088
962
  error,
1089
- Some(&json_instance),
963
+ Some(node),
1090
964
  parsed.mask.as_deref(),
1091
965
  )),
1092
966
  Err(err) => Err(handle_callback_panic(ruby, err)),
1093
967
  }
1094
- } else {
1095
- // SAFETY: validation is pure Rust with no Ruby callbacks
1096
- match unsafe { without_gvl(|| validator.validate(&json_instance)) } {
1097
- Ok(Ok(())) => Ok(()),
1098
- Ok(Err(error)) => Err(raise_validation_error(
1099
- ruby,
1100
- error,
1101
- Some(&json_instance),
1102
- parsed.mask.as_deref(),
1103
- )),
1104
- Err(err) => Err(handle_without_gvl_panic(ruby, err)),
1105
- }
1106
- }
968
+ })
1107
969
  }
1108
970
 
1109
- #[allow(unsafe_code)]
1110
971
  fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1111
972
  let _scope = KeywordCauseScope::enter();
1112
973
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
@@ -1114,7 +975,6 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1114
975
  let kw = extract_kwargs(ruby, parsed_args.keywords)?;
1115
976
 
1116
977
  let json_schema = to_schema_value(ruby, schema)?;
1117
- let json_instance = to_value(ruby, instance)?;
1118
978
  let parsed = build_parsed_options(ruby, kw, None)?;
1119
979
  let has_ruby_callbacks = parsed.has_ruby_callbacks;
1120
980
  let BuiltValidator {
@@ -1131,49 +991,37 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1131
991
  &json_schema,
1132
992
  )?;
1133
993
 
1134
- if ruby.block_given() {
1135
- // Lazy path: yield errors one at a time to the block
1136
- if has_ruby_callbacks {
1137
- let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1138
- let mut iter = validator.iter_errors(&json_instance);
994
+ surface_pending_errors(ruby, instance, |node| {
995
+ let _callback_roots =
996
+ has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
997
+
998
+ if ruby.block_given() {
999
+ // Lazy path: yield errors one at a time to the block
1000
+ let mut iter = validator.iter_errors(node);
1139
1001
  loop {
1140
- let result = catch_unwind_silent(AssertUnwindSafe(|| iter.next()));
1141
- match result {
1002
+ match catch_unwind_silent(AssertUnwindSafe(|| iter.next())) {
1142
1003
  Ok(Some(error)) => {
1143
- let ruby_error = to_ruby_error_value(
1144
- ruby,
1145
- error,
1146
- Some(&json_instance),
1147
- parsed.mask.as_deref(),
1148
- )?;
1004
+ let ruby_error =
1005
+ to_ruby_error_value(ruby, error, Some(node), parsed.mask.as_deref())?;
1149
1006
  ruby.yield_value::<Value, Value>(ruby_error)?;
1150
1007
  }
1151
1008
  Ok(None) => break,
1152
1009
  Err(err) => return Err(handle_callback_panic(ruby, err)),
1153
1010
  }
1154
1011
  }
1155
- } else {
1156
- for error in validator.iter_errors(&json_instance) {
1157
- let ruby_error =
1158
- to_ruby_error_value(ruby, error, Some(&json_instance), parsed.mask.as_deref())?;
1159
- ruby.yield_value::<Value, Value>(ruby_error)?;
1160
- }
1012
+ return Ok(ruby.qnil().as_value());
1161
1013
  }
1162
- Ok(ruby.qnil().as_value())
1163
- } else if has_ruby_callbacks {
1164
- // Eager path with callbacks
1165
- let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1166
- let result = catch_unwind_silent(AssertUnwindSafe(|| {
1167
- validator.iter_errors(&json_instance).collect::<Vec<_>>()
1168
- }));
1169
- match result {
1014
+
1015
+ match catch_unwind_silent(AssertUnwindSafe(|| {
1016
+ validator.iter_errors(node).collect::<Vec<_>>()
1017
+ })) {
1170
1018
  Ok(errors) => {
1171
1019
  let arr = ruby.ary_new_capa(errors.len());
1172
- for e in errors {
1020
+ for error in errors {
1173
1021
  arr.push(to_ruby_error_value(
1174
1022
  ruby,
1175
- e,
1176
- Some(&json_instance),
1023
+ error,
1024
+ Some(node),
1177
1025
  parsed.mask.as_deref(),
1178
1026
  )?)?;
1179
1027
  }
@@ -1181,29 +1029,9 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1181
1029
  }
1182
1030
  Err(err) => Err(handle_callback_panic(ruby, err)),
1183
1031
  }
1184
- } else {
1185
- // Eager path without callbacks — release GVL
1186
- // SAFETY: validation is pure Rust with no Ruby callbacks
1187
- let errors = match unsafe {
1188
- without_gvl(|| validator.iter_errors(&json_instance).collect::<Vec<_>>())
1189
- } {
1190
- Ok(errors) => errors,
1191
- Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
1192
- };
1193
- let arr = ruby.ary_new_capa(errors.len());
1194
- for e in errors {
1195
- arr.push(to_ruby_error_value(
1196
- ruby,
1197
- e,
1198
- Some(&json_instance),
1199
- parsed.mask.as_deref(),
1200
- )?)?;
1201
- }
1202
- Ok(arr.as_value())
1203
- }
1032
+ })
1204
1033
  }
1205
1034
 
1206
- #[allow(unsafe_code)]
1207
1035
  fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1208
1036
  let _scope = KeywordCauseScope::enter();
1209
1037
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
@@ -1211,7 +1039,6 @@ fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1211
1039
  let kw = extract_evaluate_kwargs(ruby, parsed_args.keywords)?;
1212
1040
 
1213
1041
  let json_schema = to_schema_value(ruby, schema)?;
1214
- let json_instance = to_value(ruby, instance)?;
1215
1042
  let parsed = build_parsed_options(ruby, kw, None)?;
1216
1043
  let has_ruby_callbacks = parsed.has_ruby_callbacks;
1217
1044
  let BuiltValidator {
@@ -1228,21 +1055,14 @@ fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1228
1055
  &json_schema,
1229
1056
  )?;
1230
1057
 
1231
- if has_ruby_callbacks {
1232
- let _callback_roots = CallbackRootGuard::new(ruby, &callback_roots);
1233
- let result = catch_unwind_silent(AssertUnwindSafe(|| validator.evaluate(&json_instance)));
1234
- match result {
1058
+ surface_pending_errors(ruby, instance, |node| {
1059
+ let _callback_roots =
1060
+ has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
1061
+ match catch_unwind_silent(AssertUnwindSafe(|| validator.evaluate(node))) {
1235
1062
  Ok(output) => Ok(Evaluation::new(output)),
1236
1063
  Err(err) => Err(handle_callback_panic(ruby, err)),
1237
1064
  }
1238
- } else {
1239
- // SAFETY: validation is pure Rust with no Ruby callbacks
1240
- let output = match unsafe { without_gvl(|| validator.evaluate(&json_instance)) } {
1241
- Ok(output) => output,
1242
- Err(err) => return Err(handle_without_gvl_panic(ruby, err)),
1243
- };
1244
- Ok(Evaluation::new(output))
1245
- }
1065
+ })
1246
1066
  }
1247
1067
 
1248
1068
  macro_rules! define_draft_validator {
@@ -1267,7 +1087,6 @@ macro_rules! define_draft_validator {
1267
1087
 
1268
1088
  let json_schema = to_schema_value(ruby, schema)?;
1269
1089
  let parsed = build_parsed_options(ruby, kw, Some($draft))?;
1270
- let has_ruby_callbacks = parsed.has_ruby_callbacks;
1271
1090
  let BuiltValidator {
1272
1091
  validator,
1273
1092
  callback_roots,
@@ -1285,7 +1104,6 @@ macro_rules! define_draft_validator {
1285
1104
  inner: Validator {
1286
1105
  validator,
1287
1106
  mask: parsed.mask,
1288
- has_ruby_callbacks,
1289
1107
  callback_roots,
1290
1108
  _compilation_roots: compilation_roots,
1291
1109
  },
@@ -1394,12 +1212,9 @@ fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1394
1212
  if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
1395
1213
  return Err(referencing_error(ruby, err.to_string()));
1396
1214
  }
1397
- Err(raise_validation_error(
1398
- ruby,
1399
- error,
1400
- Some(&json_schema),
1401
- None,
1402
- ))
1215
+ #[allow(unsafe_code)]
1216
+ let node = RbNode::new(schema.as_raw());
1217
+ Err(raise_validation_error(ruby, error, Some(node), None))
1403
1218
  }
1404
1219
  }
1405
1220
  }