jsonschema_rs 0.46.10 → 0.47.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f602cda016a40d57191148d7552e7df55349dc5ce714cef366d91167ce8330e0
4
- data.tar.gz: 656d8ad04dc3303b4057b644be51e33edc387ca595028efe9e4c1ffcc946cd37
3
+ metadata.gz: 6ac8c8fefbc1cb61a17c20ff939d639db9da1dfdd5fdc232847259b28495dc4e
4
+ data.tar.gz: 9ce91dc23dab850146636a4cb5aa5f3dea8ad62b1a045b93052c9ad5459baadd
5
5
  SHA512:
6
- metadata.gz: 0c76e865059ff45b73b3d522b765d8da5fea2f6728e7e486a1b10051ecfc9382a411d7f08cd7eca161f2d3d51ab3cec492423f30b0231e2df5637acbfe8fcb06
7
- data.tar.gz: 35ba6635cd492e6ff420ede329eef28d2e4027f466d7061f91579921ead262c0a72c70266ec6673b0188d48d15505ea96f96b625a0cca145bd548d023b1bc615
6
+ metadata.gz: e073285344d71b2179d2a63b4c6cfec6ade151baa84360a3015538fe148afd5223b1ca0cde689bf6ae3fce4b0060c1a0ab8b6ee49f0cfb49d573b1a8c2250ac1
7
+ data.tar.gz: 10fe811cf88137cfb5d195b76ae1fed97ed56b821b2e0ca502836211dacbfa4ecf92a4d96cc1def8a3c72a32230d7181112423372fb2a2e62b72bf5138e50674
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.47.0] - 2026-07-08
6
+
7
+ ### Added
8
+
9
+ - Optional `iter_errors(instance)` method on custom keyword validators for reporting multiple errors from a single keyword. [#1071](https://github.com/Stranger6667/jsonschema/discussions/1071)
10
+
11
+ ### Performance
12
+
13
+ - Faster validator construction, via compile-time meta-schema validators.
14
+
15
+ ### Fixed
16
+
17
+ - `type` under `items` asserted with the Validation vocabulary disabled.
18
+ - Disabled vocabularies ignored for `$ref` targets without their own `$schema` (e.g. `$defs` entries).
19
+
5
20
  ## [0.46.10] - 2026-07-05
6
21
 
7
22
  ### Fixed
@@ -149,7 +164,8 @@
149
164
 
150
165
  - Initial public release
151
166
 
152
- [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.10...HEAD
167
+ [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.47.0...HEAD
168
+ [0.47.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.10...ruby-v0.47.0
153
169
  [0.46.10]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.9...ruby-v0.46.10
154
170
  [0.46.9]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.8...ruby-v0.46.9
155
171
  [0.46.8]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.7...ruby-v0.46.8
data/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb"
3
- version = "0.46.10"
3
+ version = "0.47.0"
4
4
  edition = "2021"
5
5
  authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
6
6
  license = "MIT"
@@ -13,7 +13,7 @@ publish = false
13
13
  crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
- jsonschema = { version = "0.46.10", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
16
+ jsonschema = { version = "0.47.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros"] }
17
17
  magnus = { version = "0.8", features = ["rb-sys"] }
18
18
  rb-sys = "0.9"
19
19
  serde = { workspace = true }
data/README.md CHANGED
@@ -148,7 +148,26 @@ Each custom keyword class must implement:
148
148
  - `initialize(parent_schema, value, schema_path)` - called during schema compilation
149
149
  - `validate(instance)` - raise on failure, return normally on success
150
150
 
151
- When `validate` raises, the original exception is preserved as the `cause` of the `ValidationError`, so callers can inspect it:
151
+ A class may also implement `iter_errors(instance)`, returning an array of exceptions, to report several problems from one keyword. `each_error` then surfaces each of them; when the method is absent it falls back to the single error from `validate`:
152
+
153
+ ```ruby
154
+ class AllPositive
155
+ def initialize(parent_schema, value, schema_path); end
156
+
157
+ def validate(instance)
158
+ first_error = iter_errors(instance).first
159
+ raise first_error if first_error
160
+ end
161
+
162
+ def iter_errors(instance)
163
+ return [] unless instance.is_a?(Array)
164
+ instance.each_index.select { |i| instance[i].negative? }
165
+ .map { |i| ArgumentError.new("item #{i} is negative") }
166
+ end
167
+ end
168
+ ```
169
+
170
+ When `validate` raises, the original exception is preserved as the `cause` of the `ValidationError`, so callers can inspect it (errors from `iter_errors` keep their exception as `cause` in the same way):
152
171
 
153
172
  ```ruby
154
173
  begin
@@ -378,21 +378,15 @@ dependencies = [
378
378
 
379
379
  [[package]]
380
380
  name = "hashbrown"
381
- version = "0.16.1"
381
+ version = "0.17.0"
382
382
  source = "registry+https://github.com/rust-lang/crates.io-index"
383
- checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
383
+ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
384
384
  dependencies = [
385
385
  "allocator-api2",
386
386
  "equivalent",
387
387
  "foldhash",
388
388
  ]
389
389
 
390
- [[package]]
391
- name = "hashbrown"
392
- version = "0.17.0"
393
- source = "registry+https://github.com/rust-lang/crates.io-index"
394
- checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
395
-
396
390
  [[package]]
397
391
  name = "http"
398
392
  version = "1.4.0"
@@ -601,7 +595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
601
595
  checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
602
596
  dependencies = [
603
597
  "equivalent",
604
- "hashbrown 0.17.0",
598
+ "hashbrown",
605
599
  ]
606
600
 
607
601
  [[package]]
@@ -693,9 +687,9 @@ dependencies = [
693
687
 
694
688
  [[package]]
695
689
  name = "jsonschema"
696
- version = "0.46.10"
690
+ version = "0.47.0"
697
691
  source = "registry+https://github.com/rust-lang/crates.io-index"
698
- checksum = "f0a699d3e77675e6aa4bfffe3b907c8b5f7ed3241f9965bffb25475ad4b08d05"
692
+ checksum = "281c43ff06dcb331e9356d30e38853d559ce3d0a3f693e0b0e102667dec14fb1"
699
693
  dependencies = [
700
694
  "ahash",
701
695
  "bytecount",
@@ -706,6 +700,7 @@ dependencies = [
706
700
  "getrandom 0.3.4",
707
701
  "idna",
708
702
  "itoa",
703
+ "jsonschema-macros",
709
704
  "jsonschema-regex",
710
705
  "num-bigint",
711
706
  "num-cmp",
@@ -721,9 +716,38 @@ dependencies = [
721
716
  "uuid-simd",
722
717
  ]
723
718
 
719
+ [[package]]
720
+ name = "jsonschema-macros"
721
+ version = "0.47.0"
722
+ source = "registry+https://github.com/rust-lang/crates.io-index"
723
+ checksum = "33814349f9eb1e2f5814207c40d92413098d4d57d0fb5b5972b1c47d63a79137"
724
+ dependencies = [
725
+ "jsonschema-macros-core",
726
+ "proc-macro2",
727
+ ]
728
+
729
+ [[package]]
730
+ name = "jsonschema-macros-core"
731
+ version = "0.47.0"
732
+ source = "registry+https://github.com/rust-lang/crates.io-index"
733
+ checksum = "7bc971df1f0042bff8020f1db975da867b56e11c519821e6e573f489dd909a94"
734
+ dependencies = [
735
+ "fancy-regex",
736
+ "indexmap",
737
+ "jsonschema-regex",
738
+ "percent-encoding",
739
+ "proc-macro-crate",
740
+ "proc-macro2",
741
+ "quote",
742
+ "referencing",
743
+ "regex",
744
+ "serde_json",
745
+ "syn",
746
+ ]
747
+
724
748
  [[package]]
725
749
  name = "jsonschema-rb-ext"
726
- version = "0.46.10"
750
+ version = "0.47.0"
727
751
  dependencies = [
728
752
  "jsonschema",
729
753
  "magnus",
@@ -735,9 +759,9 @@ dependencies = [
735
759
 
736
760
  [[package]]
737
761
  name = "jsonschema-regex"
738
- version = "0.46.10"
762
+ version = "0.47.0"
739
763
  source = "registry+https://github.com/rust-lang/crates.io-index"
740
- checksum = "6dbd1086b01b9349fd4ef9a07433965af64c8ce8159abe633a189e4ff817bd13"
764
+ checksum = "ee0b351864e7ffbc5db9273daf7fa1b4d5177b0946713d667ca571b83c0b4045"
741
765
  dependencies = [
742
766
  "regex-syntax",
743
767
  ]
@@ -988,6 +1012,15 @@ dependencies = [
988
1012
  "zerovec",
989
1013
  ]
990
1014
 
1015
+ [[package]]
1016
+ name = "proc-macro-crate"
1017
+ version = "3.5.0"
1018
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1019
+ checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
1020
+ dependencies = [
1021
+ "toml_edit",
1022
+ ]
1023
+
991
1024
  [[package]]
992
1025
  name = "proc-macro2"
993
1026
  version = "1.0.106"
@@ -1073,14 +1106,14 @@ dependencies = [
1073
1106
 
1074
1107
  [[package]]
1075
1108
  name = "referencing"
1076
- version = "0.46.10"
1109
+ version = "0.47.0"
1077
1110
  source = "registry+https://github.com/rust-lang/crates.io-index"
1078
- checksum = "0fbf332a2f81899f6836f22c03da73dae8a664c32e3016b84692c23cddadc95d"
1111
+ checksum = "348e860aeb0b7bd035778fd11dd9cd5290d32e4aed3b8f2274a00287a9fd362b"
1079
1112
  dependencies = [
1080
1113
  "ahash",
1081
1114
  "fluent-uri",
1082
1115
  "getrandom 0.3.4",
1083
- "hashbrown 0.16.1",
1116
+ "hashbrown",
1084
1117
  "itoa",
1085
1118
  "micromap",
1086
1119
  "parking_lot",
@@ -1495,6 +1528,36 @@ dependencies = [
1495
1528
  "tokio",
1496
1529
  ]
1497
1530
 
1531
+ [[package]]
1532
+ name = "toml_datetime"
1533
+ version = "1.1.1+spec-1.1.0"
1534
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1535
+ checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
1536
+ dependencies = [
1537
+ "serde_core",
1538
+ ]
1539
+
1540
+ [[package]]
1541
+ name = "toml_edit"
1542
+ version = "0.25.12+spec-1.1.0"
1543
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1544
+ checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
1545
+ dependencies = [
1546
+ "indexmap",
1547
+ "toml_datetime",
1548
+ "toml_parser",
1549
+ "winnow",
1550
+ ]
1551
+
1552
+ [[package]]
1553
+ name = "toml_parser"
1554
+ version = "1.1.2+spec-1.1.0"
1555
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1556
+ checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
1557
+ dependencies = [
1558
+ "winnow",
1559
+ ]
1560
+
1498
1561
  [[package]]
1499
1562
  name = "tower"
1500
1563
  version = "0.5.3"
@@ -1894,6 +1957,15 @@ version = "0.52.6"
1894
1957
  source = "registry+https://github.com/rust-lang/crates.io-index"
1895
1958
  checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1896
1959
 
1960
+ [[package]]
1961
+ name = "winnow"
1962
+ version = "1.0.3"
1963
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1964
+ checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
1965
+ dependencies = [
1966
+ "memchr",
1967
+ ]
1968
+
1897
1969
  [[package]]
1898
1970
  name = "wit-bindgen"
1899
1971
  version = "0.57.1"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.46.10"
3
+ version = "0.47.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -10,10 +10,10 @@ name = "jsonschema_rb"
10
10
  path = "../../src/lib.rs"
11
11
 
12
12
  [dependencies]
13
- jsonschema = { version = "0.46.10", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
13
+ jsonschema = { version = "0.47.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros"] }
14
14
  magnus = { version = "0.8", features = ["rb-sys"] }
15
15
  rb-sys = "0.9"
16
- referencing = "0.46.10"
16
+ referencing = "0.47.0"
17
17
  serde = { version = "1", features = ["derive"] }
18
18
  serde_json = { version = "1", features = ["arbitrary_precision"] }
19
19
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSchema
4
- VERSION = "0.46.10"
4
+ VERSION = "0.47.0"
5
5
  end
data/src/lib.rs CHANGED
@@ -29,6 +29,7 @@ use magnus::{
29
29
  use referencing::unescape_segment;
30
30
  use std::{
31
31
  cell::RefCell,
32
+ collections::VecDeque,
32
33
  panic::{self, AssertUnwindSafe},
33
34
  sync::Arc,
34
35
  };
@@ -198,13 +199,36 @@ fn build_parsed_options(
198
199
 
199
200
  thread_local! {
200
201
  static LAST_CALLBACK_ERROR: RefCell<Option<Error>> = const { RefCell::new(None) };
201
- // Stores the original Ruby exception from a custom keyword's validate() call so it can be
202
- // set as the `cause` on the resulting ValidationError.
203
- pub(crate) static CUSTOM_KEYWORD_CAUSE: RefCell<Option<Error>> = const { RefCell::new(None) };
202
+ // Stores the original Ruby exceptions from a custom keyword's validate()/iter_errors() calls so
203
+ // each can be set as the `cause` on the resulting ValidationError, in FIFO order.
204
+ pub(crate) static CUSTOM_KEYWORD_CAUSE: RefCell<VecDeque<Error>> = const { RefCell::new(VecDeque::new()) };
204
205
  /// When `true`, the custom panic hook suppresses output (inside `catch_unwind` blocks).
205
206
  static SUPPRESS_PANIC_OUTPUT: RefCell<bool> = const { RefCell::new(false) };
206
207
  }
207
208
 
209
+ // RAII scope giving each surfacing call its own keyword-cause queue and restoring the caller's on
210
+ // exit. This discards causes a call leaves unconsumed (`evaluate`, an abandoned enumeration, a
211
+ // panic — the drop still runs) and lets a keyword re-enter validation without clobbering the outer
212
+ // call's pending causes.
213
+ pub(crate) struct KeywordCauseScope {
214
+ saved: VecDeque<Error>,
215
+ }
216
+
217
+ impl KeywordCauseScope {
218
+ pub(crate) fn enter() -> Self {
219
+ Self {
220
+ saved: CUSTOM_KEYWORD_CAUSE.with(|cell| std::mem::take(&mut *cell.borrow_mut())),
221
+ }
222
+ }
223
+ }
224
+
225
+ impl Drop for KeywordCauseScope {
226
+ fn drop(&mut self) {
227
+ let saved = std::mem::take(&mut self.saved);
228
+ CUSTOM_KEYWORD_CAUSE.with(|cell| *cell.borrow_mut() = saved);
229
+ }
230
+ }
231
+
208
232
  static VALIDATION_ERROR_CLASS: Lazy<ExceptionClass> = Lazy::new(|ruby| {
209
233
  let module: RModule = ruby
210
234
  .class_object()
@@ -431,7 +455,7 @@ fn into_ruby_error(
431
455
  exc.ivar_set(*ID_AT_ABSOLUTE_KEYWORD_LOCATION, absolute_location)?;
432
456
 
433
457
  if is_custom {
434
- if let Some(cause) = CUSTOM_KEYWORD_CAUSE.with(|cell| cell.borrow_mut().take()) {
458
+ if let Some(cause) = CUSTOM_KEYWORD_CAUSE.with(|cell| cell.borrow_mut().pop_front()) {
435
459
  if let ErrorType::Exception(cause_exc) = cause.error_type() {
436
460
  exc.ivar_set(*ID_CAUSE, cause_exc.as_value())?;
437
461
  }
@@ -654,6 +678,7 @@ impl Validator {
654
678
 
655
679
  #[allow(unsafe_code)]
656
680
  fn validate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<(), Error> {
681
+ let _scope = KeywordCauseScope::enter();
657
682
  let json_instance = to_value(ruby, instance)?;
658
683
 
659
684
  if rb_self.has_ruby_callbacks {
@@ -687,6 +712,7 @@ impl Validator {
687
712
 
688
713
  #[allow(unsafe_code)]
689
714
  fn iter_errors(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Value, Error> {
715
+ let _scope = KeywordCauseScope::enter();
690
716
  let json_instance = to_value(ruby, instance)?;
691
717
 
692
718
  if ruby.block_given() {
@@ -773,6 +799,7 @@ impl Validator {
773
799
 
774
800
  #[allow(unsafe_code)]
775
801
  fn evaluate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Evaluation, Error> {
802
+ let _scope = KeywordCauseScope::enter();
776
803
  let json_instance = to_value(ruby, instance)?;
777
804
 
778
805
  if rb_self.has_ruby_callbacks {
@@ -1027,6 +1054,7 @@ fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1027
1054
 
1028
1055
  #[allow(unsafe_code)]
1029
1056
  fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1057
+ let _scope = KeywordCauseScope::enter();
1030
1058
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
1031
1059
  let (schema, instance) = parsed_args.required;
1032
1060
  let kw = extract_kwargs(ruby, parsed_args.keywords)?;
@@ -1079,6 +1107,7 @@ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1079
1107
 
1080
1108
  #[allow(unsafe_code)]
1081
1109
  fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1110
+ let _scope = KeywordCauseScope::enter();
1082
1111
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
1083
1112
  let (schema, instance) = parsed_args.required;
1084
1113
  let kw = extract_kwargs(ruby, parsed_args.keywords)?;
@@ -1175,6 +1204,7 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1175
1204
 
1176
1205
  #[allow(unsafe_code)]
1177
1206
  fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1207
+ let _scope = KeywordCauseScope::enter();
1178
1208
  let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
1179
1209
  let (schema, instance) = parsed_args.required;
1180
1210
  let kw = extract_evaluate_kwargs(ruby, parsed_args.keywords)?;
data/src/options.rs CHANGED
@@ -12,7 +12,7 @@ use magnus::{
12
12
  prelude::*,
13
13
  scan_args::{get_kwargs, scan_args, KwArgs},
14
14
  value::Opaque,
15
- Error, RHash, RModule, Ruby, TryConvert, Value,
15
+ Error, Exception, RArray, RHash, RModule, Ruby, TryConvert, Value,
16
16
  };
17
17
 
18
18
  use crate::{
@@ -388,7 +388,7 @@ impl jsonschema::Keyword for RubyKeyword {
388
388
  Err(e) => {
389
389
  let msg = e.to_string();
390
390
  CUSTOM_KEYWORD_CAUSE.with(|cell| {
391
- *cell.borrow_mut() = Some(e);
391
+ cell.borrow_mut().push_back(e);
392
392
  });
393
393
  Err(jsonschema::ValidationError::custom(msg))
394
394
  }
@@ -404,6 +404,67 @@ impl jsonschema::Keyword for RubyKeyword {
404
404
  let result: Result<Value, _> = inst.funcall("validate", (rb_instance,));
405
405
  result.is_ok()
406
406
  }
407
+
408
+ fn iter_errors<'i>(
409
+ &self,
410
+ instance: &'i serde_json::Value,
411
+ ) -> Box<dyn Iterator<Item = jsonschema::ValidationError<'i>> + 'i> {
412
+ let ruby = Ruby::get().expect("Ruby VM should be initialized");
413
+ let keyword = ruby.get_inner(self.instance);
414
+ // Without an `iter_errors` method, fall back to the single-error `validate` path.
415
+ if !keyword.respond_to("iter_errors", false).unwrap_or(false) {
416
+ return Box::new(self.validate(instance).err().into_iter());
417
+ }
418
+ let rb_instance = match value_to_ruby(&ruby, instance) {
419
+ Ok(rb_instance) => rb_instance,
420
+ Err(error) => return Box::new(std::iter::once(custom_error_from_ruby(error))),
421
+ };
422
+ let array = keyword
423
+ .funcall::<_, _, Value>("iter_errors", (rb_instance,))
424
+ .and_then(|result| result.funcall::<_, _, RArray>("to_a", ()));
425
+ let array = match array {
426
+ Ok(array) => array,
427
+ Err(error) => return Box::new(std::iter::once(custom_error_from_ruby(error))),
428
+ };
429
+ let mut errors: Vec<jsonschema::ValidationError<'i>> = Vec::with_capacity(array.len());
430
+ for index in 0..array.len() {
431
+ // `entry` (not `as_slice`) because `ruby_value_to_error` calls back into Ruby.
432
+ let item = isize::try_from(index)
433
+ .map_err(|_| {
434
+ Error::new(
435
+ ruby.exception_arg_error(),
436
+ "Array index exceeds supported range",
437
+ )
438
+ })
439
+ .and_then(|index| array.entry::<Value>(index));
440
+ let error = match item {
441
+ Ok(item) => ruby_value_to_error(&ruby, item),
442
+ Err(error) => error,
443
+ };
444
+ errors.push(custom_error_from_ruby(error));
445
+ }
446
+ Box::new(errors.into_iter())
447
+ }
448
+ }
449
+
450
+ // Wrap a yielded Ruby value as an `Error`, preserving exception instances so they surface as
451
+ // `#cause`; a non-exception is reported through a generic `RuntimeError`.
452
+ fn ruby_value_to_error(ruby: &Ruby, value: Value) -> Error {
453
+ if let Ok(exception) = Exception::try_convert(value) {
454
+ return Error::from(exception);
455
+ }
456
+ let message = value
457
+ .funcall::<_, _, String>("to_s", ())
458
+ .unwrap_or_else(|_| "custom keyword error".to_string());
459
+ Error::new(ruby.exception_runtime_error(), message)
460
+ }
461
+
462
+ // Build a custom `ValidationError` from a Ruby error, queuing the exception as the error's cause so
463
+ // it surfaces as `#cause`, exactly like the single `validate` path.
464
+ fn custom_error_from_ruby<'i>(error: Error) -> jsonschema::ValidationError<'i> {
465
+ let message = error.to_string();
466
+ CUSTOM_KEYWORD_CAUSE.with(|cell| cell.borrow_mut().push_back(error));
467
+ jsonschema::ValidationError::custom(message)
407
468
  }
408
469
 
409
470
  #[allow(clippy::too_many_arguments)]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonschema_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.46.10
4
+ version: 0.47.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dygalo