jsonschema_rs 0.56.0 → 0.58.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: e5e2046dda10b3b0e6109b3f7a55667f9d6cb86a38b0f7ddbb80e7d9bc67b25d
4
- data.tar.gz: f3eed3727e42c284573978507158cdd966691a0d55add5a12102bbf206440db3
3
+ metadata.gz: d283a285d78237b32ea3ddbca89297aa0ff3424b194a59add3dc5d552cbc6b9a
4
+ data.tar.gz: 7cc45e4701dba75e717b3935b6c94b44cc3b8b18f0aa07d65ab10e1da03ce51e
5
5
  SHA512:
6
- metadata.gz: '0632192b1860a29b067638bd11e1c70d71e58d39ff5a100514ae72b183c9f46784a466ef59ee80ab5f151e700df4eb8b3a10e3b2a2b7b062b301094b653c07f9'
7
- data.tar.gz: b9155ae7dae3ce0b2559733bc53c938f0129a696dc43d7a7913cfc850967583d9144a0c6f63940b44ec2c45b2d3b7b14ec110263f0daed0027a581174630474c
6
+ metadata.gz: 34521e6ec676f5154e8eadca6b3509a86d6e148dddfd8feda01d2b9463874604c3b340184d0cdfde53e3db9bc85a87d00b509d446463cda2a2ac393c624f038a
7
+ data.tar.gz: 38f1947bb694578ae871ea38693577d2d6c03fbbbdb6184b708a354de12c9031359ea03244dea147da385ce4ae89f6be645ece567406aaf61b2dced8e71f5913
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.58.0] - 2026-09-25
6
+
7
+ ### Performance
8
+
9
+ - `oneOf` and `anyOf` whose branches each require a property fixed to a distinct string validate only the branch matching the instance's value.
10
+ - `Meta.valid?` and `Meta.validate!` check a schema against the bundled drafts without converting it first, 2 to 5x faster on the benchmarked schemas.
11
+
12
+ ### Fixed
13
+
14
+ - `Canonical.find_unsatisfiable` missing a `oneOf` over `$ref` branches beside a sibling none of them admits.
15
+ - A value with no JSON form nested under `type: null` failing validation silently (it should raise `TypeError`).
16
+
17
+ ## [0.57.0] - 2026-09-22
18
+
19
+ ### Added
20
+
21
+ - `Canonical.find_unsatisfiable`, naming every subschema of a document that admits no value, and the keywords that leave it empty.
22
+
23
+ ### Fixed
24
+
25
+ - The instance path of a schema build error, which was empty instead of naming the keyword location that failed to compile.
26
+
5
27
  ## [0.56.0] - 2026-09-10
6
28
 
7
29
  ### Added
@@ -567,7 +589,9 @@
567
589
 
568
590
  - Initial public release
569
591
 
570
- [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.56.0...HEAD
592
+ [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.58.0...HEAD
593
+ [0.58.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.57.0...ruby-v0.58.0
594
+ [0.57.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.56.0...ruby-v0.57.0
571
595
  [0.56.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.55.1...ruby-v0.56.0
572
596
  [0.55.1]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.55.0...ruby-v0.55.1
573
597
  [0.55.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.54.0...ruby-v0.55.0
data/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb"
3
- version = "0.56.0"
3
+ version = "0.58.0"
4
4
  edition = "2021"
5
5
  authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
6
6
  license = "MIT"
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
16
  strum = "0.28.0"
17
- jsonschema = { version = "0.56.0", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
17
+ jsonschema = { version = "0.58.0", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
18
18
  magnus = { version = "0.8", features = ["rb-sys"] }
19
19
  rb-sys = "0.9"
20
20
  serde = { workspace = true }
data/README.md CHANGED
@@ -371,6 +371,33 @@ Compare a request schema new-against-old and a response schema old-against-new:
371
371
 
372
372
  Both operands must share one setup - the same draft, format policy, regular-expression engine and definitions - or `IncompatibleOperands` is raised. `UnsupportedOperand` means an operand is a `Raw` pass-through, and `UnsupportedResult` that the canonical form does not support the result. All three live under `JSONSchema::Canonical`.
373
373
 
374
+ ### Finding the dead subschemas of a document
375
+
376
+ `JSONSchema::Canonical.find_unsatisfiable` walks a whole document and answers which of its subschemas admit no value, and why - the keywords at fault and where they sit:
377
+
378
+ ```ruby
379
+ reasons = JSONSchema::Canonical.find_unsatisfiable(
380
+ {
381
+ "properties" => {
382
+ "tag" => { "type" => "string", "minLength" => 5, "maxLength" => 2 },
383
+ "name" => { "type" => "string" }
384
+ }
385
+ }
386
+ )
387
+
388
+ case reasons["/properties/tag"]
389
+ in JSONSchema::Canonical::ConflictReason[causes:]
390
+ causes.map { |cause| [cause.pointer, cause.keywords] }
391
+ # => [["/properties/tag", ["type"]], ["/properties/tag", ["minLength", "maxLength"]]]
392
+ end
393
+
394
+ # A live subschema is not reported
395
+ reasons.key?("/properties/name")
396
+ # => false
397
+ ```
398
+
399
+ A reason is a `LiteralReason` (written as `false`), an `EmptyReason` (one part admits nothing by itself) or a `ConflictReason` (each part admits values, together they admit none). A pointer left out is not proven satisfiable: a document canonicalization cannot model reports nothing, as `satisfiability` answers `:unknown`.
400
+
374
401
  ## Schema Bundling and Dereferencing
375
402
 
376
403
  Produce a Compound Schema Document ([Appendix B](https://json-schema.org/draft/2020-12/json-schema-core#appendix-B)) by embedding all external `$ref` targets into a draft-appropriate container. The result validates identically to the original.
@@ -673,6 +700,18 @@ Valid draft symbols: `:draft4`, `:draft6`, `:draft7`, `:draft201909`, `:draft202
673
700
 
674
701
  For detailed benchmarks, see our [full performance comparison](https://github.com/Stranger6667/jsonschema/blob/master/crates/jsonschema-rb/BENCHMARKS.md).
675
702
 
703
+ ### Compile-Time Validators
704
+
705
+ If you ship your own extension and know the schema at build time, the Rust crate's
706
+ `#[jsonschema::validator(..., backend = Magnus)]` macro compiles it into a validator that reads
707
+ Ruby objects directly, so nothing is parsed or compiled when your extension is loaded, and
708
+ validation runs up to 3.5x faster than with a validator built at run time. See the
709
+ [macro documentation](https://docs.rs/jsonschema/latest/jsonschema/#ruby-extension-modules).
710
+
711
+ This is not available from the `jsonschema_rs` gem, which takes its schemas at run time.
712
+ A complete extension with its build and test commands lives in
713
+ [`examples/magnus-extension`](https://github.com/Stranger6667/jsonschema/tree/master/examples/magnus-extension).
714
+
676
715
  ## Acknowledgements
677
716
 
678
717
  This library draws API design inspiration from the Python [`jsonschema`](https://github.com/python-jsonschema/jsonschema) package. We're grateful to the Python `jsonschema` maintainers and contributors for their pioneering work in JSON Schema validation.
@@ -693,9 +693,9 @@ dependencies = [
693
693
 
694
694
  [[package]]
695
695
  name = "jsonschema"
696
- version = "0.56.0"
696
+ version = "0.58.0"
697
697
  source = "registry+https://github.com/rust-lang/crates.io-index"
698
- checksum = "b6a806f80c1f5560431009ce5ec29b59d38f950e0cd7db5f1b8925e6d8104e21"
698
+ checksum = "7b055418f7dbe79d0c3c6236e4c7dc6de804ec25bbacd5eb2df914de42fa6d61"
699
699
  dependencies = [
700
700
  "ahash",
701
701
  "bytecount",
@@ -709,6 +709,7 @@ dependencies = [
709
709
  "jsonschema-macros",
710
710
  "jsonschema-regex",
711
711
  "jsonschema-value",
712
+ "magnus",
712
713
  "num-bigint",
713
714
  "num-cmp",
714
715
  "num-traits",
@@ -726,9 +727,9 @@ dependencies = [
726
727
 
727
728
  [[package]]
728
729
  name = "jsonschema-macros"
729
- version = "0.56.0"
730
+ version = "0.58.0"
730
731
  source = "registry+https://github.com/rust-lang/crates.io-index"
731
- checksum = "97ac41a7411c029f7f9971881ccf12c8190411a0ab5caf53c07b07eabbea253e"
732
+ checksum = "0d5d0dd1510274c2ce3291ef72d8c6f8c56b17d59c3a7eeb10dabd4106dd0daa"
732
733
  dependencies = [
733
734
  "jsonschema-macros-core",
734
735
  "proc-macro2",
@@ -736,9 +737,9 @@ dependencies = [
736
737
 
737
738
  [[package]]
738
739
  name = "jsonschema-macros-core"
739
- version = "0.56.0"
740
+ version = "0.58.0"
740
741
  source = "registry+https://github.com/rust-lang/crates.io-index"
741
- checksum = "b29ab02b2d2d1b26f9c7709c7985ced2d163cb570dedd38bd8467ef31e5dbc3c"
742
+ checksum = "c643d0e62d2398b6b5faf940dd9bfc5a2ee5307a56231529823e726f497f260c"
742
743
  dependencies = [
743
744
  "fancy-regex",
744
745
  "indexmap",
@@ -755,7 +756,7 @@ dependencies = [
755
756
 
756
757
  [[package]]
757
758
  name = "jsonschema-rb-ext"
758
- version = "0.56.0"
759
+ version = "0.58.0"
759
760
  dependencies = [
760
761
  "jsonschema",
761
762
  "magnus",
@@ -769,18 +770,18 @@ dependencies = [
769
770
 
770
771
  [[package]]
771
772
  name = "jsonschema-regex"
772
- version = "0.56.0"
773
+ version = "0.58.0"
773
774
  source = "registry+https://github.com/rust-lang/crates.io-index"
774
- checksum = "cb862addfa7782108933abcf842fe25334523a9df5b89ea4d9620e3dd7b42181"
775
+ checksum = "7c1628929292454a791622b721825f293df733070534a0a89fea620cce1b509d"
775
776
  dependencies = [
776
777
  "regex-syntax",
777
778
  ]
778
779
 
779
780
  [[package]]
780
781
  name = "jsonschema-value"
781
- version = "0.56.0"
782
+ version = "0.58.0"
782
783
  source = "registry+https://github.com/rust-lang/crates.io-index"
783
- checksum = "a05cd404c5ff6e2731dbbf7e290a27417750acfb59e329560a1d0af384c93fb0"
784
+ checksum = "5d070064f657124cf7504e20d2ad30ebb58d228c503aace3c3fe98acc8f1c870"
784
785
  dependencies = [
785
786
  "ahash",
786
787
  "bytecount",
@@ -1135,9 +1136,9 @@ dependencies = [
1135
1136
 
1136
1137
  [[package]]
1137
1138
  name = "referencing"
1138
- version = "0.56.0"
1139
+ version = "0.58.0"
1139
1140
  source = "registry+https://github.com/rust-lang/crates.io-index"
1140
- checksum = "a3b4a92fac7e28c27de3ad26df2ecb9e652f227b258e845af034052e5b22c96c"
1141
+ checksum = "8e0b0f22114d6c321197de72f9800cb2bb0e83b7ddc03d70bb7a83eeae8586e7"
1141
1142
  dependencies = [
1142
1143
  "ahash",
1143
1144
  "fluent-uri",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.56.0"
3
+ version = "0.58.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.56.0", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
13
+ jsonschema = { version = "0.58.0", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
14
14
  magnus = { version = "0.8", features = ["rb-sys"] }
15
15
  rb-sys = "0.9"
16
- referencing = "0.56.0"
16
+ referencing = "0.58.0"
17
17
  serde = { version = "1", features = ["derive"] }
18
18
  serde_json = { version = "1", features = ["arbitrary_precision"] }
19
19
  strum = "0.28.0"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSchema
4
- VERSION = "0.56.0"
4
+ VERSION = "0.58.0"
5
5
  end
data/sig/jsonschema.rbs CHANGED
@@ -306,6 +306,55 @@ module JSONSchema
306
306
  def inspect: () -> String
307
307
  def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
308
308
  end
309
+
310
+ # One part of a schema object, as a reason names it.
311
+ class Cause
312
+ # JSON Pointer of the schema object holding `keywords`, or of the subschema itself.
313
+ def pointer: () -> String
314
+ # Keywords of one family present at `pointer`; empty for a whole subschema.
315
+ def keywords: () -> Array[String]
316
+ def ==: (untyped other) -> bool
317
+ def inspect: () -> String
318
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
319
+ end
320
+
321
+ # The subschema is written as `false`.
322
+ class LiteralReason
323
+ def inspect: () -> String
324
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
325
+ end
326
+
327
+ # One part every value must satisfy admits nothing by itself. A subschema part has a reason
328
+ # of its own under its pointer.
329
+ class EmptyReason
330
+ def cause: () -> Cause
331
+ def inspect: () -> String
332
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
333
+ end
334
+
335
+ # Each part admits values; no value satisfies all of them together.
336
+ class ConflictReason
337
+ def causes: () -> Array[Cause]
338
+ def inspect: () -> String
339
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
340
+ end
341
+
342
+ # Why a subschema admits no value.
343
+ type unsatisfiable_reason = LiteralReason | EmptyReason | ConflictReason
344
+
345
+ # The subschemas that admit no value, by JSON Pointer, each with why.
346
+ #
347
+ # A pointer left out is not proven satisfiable: a document the canonical form does not model
348
+ # reports nothing, as `satisfiability` answers `:unknown`.
349
+ def self.find_unsatisfiable: (
350
+ untyped schema,
351
+ ?draft: JSONSchema::draft?,
352
+ ?validate_formats: bool?,
353
+ ?pattern_options: (RegexOptions | FancyRegexOptions)?,
354
+ ?retriever: (^(String) -> untyped)?,
355
+ ?registry: Registry?,
356
+ ?base_uri: String?
357
+ ) -> Hash[String, unsatisfiable_reason]
309
358
  end
310
359
 
311
360
  # Reduce a schema to its canonical form.
data/src/canonical.rs CHANGED
@@ -1,8 +1,9 @@
1
1
  use jsonschema::{
2
2
  canonical::{
3
- CanonicalKind, CanonicalSchema, CanonicalView, CanonicalizationError, Containment,
4
- ContainsView as CoreContainsView, Distinctness,
3
+ CanonicalKind, CanonicalSchema, CanonicalView, CanonicalizationError, Cause as CoreCause,
4
+ Containment, ContainsView as CoreContainsView, Distinctness,
5
5
  ObjectViolationView as CoreObjectViolationView, RawReason, Satisfiability,
6
+ UnsatisfiableReason as CoreUnsatisfiableReason,
6
7
  },
7
8
  JsonType,
8
9
  };
@@ -1374,10 +1375,154 @@ impl EnumView {
1374
1375
  }
1375
1376
  }
1376
1377
 
1377
- fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1378
- let parsed = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1379
- let (schema_arg,) = parsed.required;
1380
- let keywords: RHash = parsed.keywords;
1378
+ /// One part of a schema object, as a reason names it.
1379
+ #[derive(magnus::TypedData)]
1380
+ #[magnus(class = "JSONSchema::Canonical::Cause", free_immediately)]
1381
+ pub struct Cause {
1382
+ pointer: String,
1383
+ keywords: Vec<String>,
1384
+ }
1385
+
1386
+ impl DataTypeFunctions for Cause {}
1387
+
1388
+ impl Cause {
1389
+ fn pointer(ruby: &Ruby, rb_self: &Self) -> Value {
1390
+ ruby.str_new(&rb_self.pointer).as_value()
1391
+ }
1392
+
1393
+ fn keywords(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
1394
+ let array = ruby.ary_new_capa(rb_self.keywords.len());
1395
+ for keyword in &rb_self.keywords {
1396
+ array.push(ruby.str_new(keyword))?;
1397
+ }
1398
+ Ok(array.as_value())
1399
+ }
1400
+
1401
+ fn eq(rb_self: &Self, other: Value) -> bool {
1402
+ let Ok(other_ref) = <&Cause>::try_convert(other) else {
1403
+ return false;
1404
+ };
1405
+ rb_self.pointer == other_ref.pointer && rb_self.keywords == other_ref.keywords
1406
+ }
1407
+
1408
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
1409
+ Ok(format!(
1410
+ "#<JSONSchema::Canonical::Cause pointer={} keywords={}>",
1411
+ Self::pointer(ruby, rb_self).inspect(),
1412
+ Self::keywords(ruby, rb_self)?.inspect()
1413
+ ))
1414
+ }
1415
+
1416
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
1417
+ let hash = ruby.hash_new();
1418
+ hash.aset(ruby.sym_new("pointer"), Self::pointer(ruby, rb_self))?;
1419
+ hash.aset(ruby.sym_new("keywords"), Self::keywords(ruby, rb_self)?)?;
1420
+ Ok(hash)
1421
+ }
1422
+ }
1423
+
1424
+ /// The subschema is written as `false`.
1425
+ #[derive(magnus::TypedData)]
1426
+ #[magnus(class = "JSONSchema::Canonical::LiteralReason", free_immediately)]
1427
+ pub struct LiteralReason;
1428
+
1429
+ impl DataTypeFunctions for LiteralReason {}
1430
+
1431
+ impl LiteralReason {
1432
+ fn inspect(_rb_self: &Self) -> &'static str {
1433
+ "#<JSONSchema::Canonical::LiteralReason>"
1434
+ }
1435
+
1436
+ fn deconstruct_keys(ruby: &Ruby, _rb_self: &Self, _keys: Value) -> RHash {
1437
+ ruby.hash_new()
1438
+ }
1439
+ }
1440
+
1441
+ /// One part every value must satisfy admits nothing by itself. A subschema part has a reason of
1442
+ /// its own under its pointer.
1443
+ #[derive(magnus::TypedData)]
1444
+ #[magnus(class = "JSONSchema::Canonical::EmptyReason", free_immediately)]
1445
+ pub struct EmptyReason {
1446
+ cause: CoreCause,
1447
+ }
1448
+
1449
+ impl DataTypeFunctions for EmptyReason {}
1450
+
1451
+ impl EmptyReason {
1452
+ fn cause(ruby: &Ruby, rb_self: &Self) -> Value {
1453
+ cause_to_ruby(ruby, rb_self.cause.clone())
1454
+ }
1455
+
1456
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> String {
1457
+ format!(
1458
+ "#<JSONSchema::Canonical::EmptyReason cause={}>",
1459
+ Self::cause(ruby, rb_self).inspect()
1460
+ )
1461
+ }
1462
+
1463
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
1464
+ let hash = ruby.hash_new();
1465
+ hash.aset(ruby.sym_new("cause"), Self::cause(ruby, rb_self))?;
1466
+ Ok(hash)
1467
+ }
1468
+ }
1469
+
1470
+ /// Each part admits values; no value satisfies all of them together.
1471
+ #[derive(magnus::TypedData)]
1472
+ #[magnus(class = "JSONSchema::Canonical::ConflictReason", free_immediately)]
1473
+ pub struct ConflictReason {
1474
+ causes: Vec<CoreCause>,
1475
+ }
1476
+
1477
+ impl DataTypeFunctions for ConflictReason {}
1478
+
1479
+ impl ConflictReason {
1480
+ fn causes(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
1481
+ let array = ruby.ary_new_capa(rb_self.causes.len());
1482
+ for cause in &rb_self.causes {
1483
+ array.push(cause_to_ruby(ruby, cause.clone()))?;
1484
+ }
1485
+ Ok(array.as_value())
1486
+ }
1487
+
1488
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
1489
+ Ok(format!(
1490
+ "#<JSONSchema::Canonical::ConflictReason causes={}>",
1491
+ Self::causes(ruby, rb_self)?.inspect()
1492
+ ))
1493
+ }
1494
+
1495
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
1496
+ let hash = ruby.hash_new();
1497
+ hash.aset(ruby.sym_new("causes"), Self::causes(ruby, rb_self)?)?;
1498
+ Ok(hash)
1499
+ }
1500
+ }
1501
+
1502
+ fn cause_to_ruby(ruby: &Ruby, cause: CoreCause) -> Value {
1503
+ ruby.obj_wrap(Cause {
1504
+ pointer: cause.pointer,
1505
+ keywords: cause.keywords,
1506
+ })
1507
+ .as_value()
1508
+ }
1509
+
1510
+ fn reason_to_ruby(ruby: &Ruby, reason: CoreUnsatisfiableReason) -> Value {
1511
+ match reason {
1512
+ CoreUnsatisfiableReason::Literal => ruby.obj_wrap(LiteralReason).as_value(),
1513
+ CoreUnsatisfiableReason::Empty(cause) => ruby.obj_wrap(EmptyReason { cause }).as_value(),
1514
+ CoreUnsatisfiableReason::Conflict(causes) => {
1515
+ ruby.obj_wrap(ConflictReason { causes }).as_value()
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ /// Read the keyword arguments every canonicalization entry point takes, and run `call` with them.
1521
+ fn with_canonical_options<R>(
1522
+ ruby: &Ruby,
1523
+ keywords: RHash,
1524
+ call: impl FnOnce(jsonschema::canonical::CanonicalizeOptions<'_>) -> Result<R, Error>,
1525
+ ) -> Result<R, Error> {
1381
1526
  let base_kwargs: CanonicalKwArgs = get_kwargs(
1382
1527
  keywords,
1383
1528
  &[],
@@ -1393,7 +1538,6 @@ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1393
1538
  let (draft_val, validate_formats, pattern_options, retriever_val, registry_val, base_uri) =
1394
1539
  base_kwargs.optional;
1395
1540
 
1396
- let schema_value = to_schema_value(ruby, schema_arg)?;
1397
1541
  let mut options = jsonschema::canonical::options();
1398
1542
  if let Some(draft) = draft_val {
1399
1543
  options = options.with_draft(parse_draft_symbol(ruby, draft)?);
@@ -1414,31 +1558,61 @@ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1414
1558
  has_retriever = true;
1415
1559
  }
1416
1560
  }
1417
- if let Some(val) = registry_val {
1418
- if !val.is_nil() {
1419
- let registry: &Registry = TryConvert::try_convert(val).map_err(|_| {
1420
- Error::new(
1421
- ruby.exception_type_error(),
1422
- "registry must be a JSONSchema::Registry instance",
1423
- )
1424
- })?;
1425
- if !has_retriever {
1426
- if let Some(value) = registry.retriever_value(ruby) {
1427
- if let Some(retriever) = make_retriever(ruby, value)? {
1428
- options = options.with_retriever(retriever);
1429
- }
1430
- }
1431
- }
1432
- options = options.with_registry(registry.inner.as_ref());
1433
- }
1434
- }
1435
1561
  if let Some(base_uri) = base_uri {
1436
1562
  options = options.with_base_uri(base_uri);
1437
1563
  }
1438
- options
1439
- .canonicalize(&schema_value)
1440
- .map(|inner| ruby.obj_wrap(RbCanonicalSchema { inner }).as_value())
1441
- .map_err(|error| canonicalization_error(ruby, error))
1564
+ let Some(val) = registry_val else {
1565
+ return call(options);
1566
+ };
1567
+ if val.is_nil() {
1568
+ return call(options);
1569
+ }
1570
+ let registry: &Registry = TryConvert::try_convert(val).map_err(|_| {
1571
+ Error::new(
1572
+ ruby.exception_type_error(),
1573
+ "registry must be a JSONSchema::Registry instance",
1574
+ )
1575
+ })?;
1576
+ if !has_retriever {
1577
+ if let Some(value) = registry.retriever_value(ruby) {
1578
+ if let Some(retriever) = make_retriever(ruby, value)? {
1579
+ options = options.with_retriever(retriever);
1580
+ }
1581
+ }
1582
+ }
1583
+ call(options.with_registry(registry.inner.as_ref()))
1584
+ }
1585
+
1586
+ /// The subschemas of `schema` that admit no value, by JSON Pointer, each with why.
1587
+ fn find_unsatisfiable(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1588
+ let parsed = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1589
+ let (schema_arg,) = parsed.required;
1590
+ let schema_value = to_schema_value(ruby, schema_arg)?;
1591
+ with_canonical_options(ruby, parsed.keywords, |options| {
1592
+ let prepared = options
1593
+ .prepare(&schema_value)
1594
+ .map_err(|error| canonicalization_error(ruby, error))?;
1595
+ let reasons = prepared
1596
+ .unsatisfiable()
1597
+ .map_err(|error| canonicalization_error(ruby, error))?;
1598
+ let hash = ruby.hash_new();
1599
+ for (pointer, reason) in reasons {
1600
+ hash.aset(ruby.str_new(&pointer), reason_to_ruby(ruby, reason))?;
1601
+ }
1602
+ Ok(hash.as_value())
1603
+ })
1604
+ }
1605
+
1606
+ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1607
+ let parsed = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1608
+ let (schema_arg,) = parsed.required;
1609
+ let schema_value = to_schema_value(ruby, schema_arg)?;
1610
+ with_canonical_options(ruby, parsed.keywords, |options| {
1611
+ options
1612
+ .canonicalize(&schema_value)
1613
+ .map(|inner| ruby.obj_wrap(RbCanonicalSchema { inner }).as_value())
1614
+ .map_err(|error| canonicalization_error(ruby, error))
1615
+ })
1442
1616
  }
1443
1617
 
1444
1618
  fn define_labels<Label: Copy + Into<&'static str>>(
@@ -1716,6 +1890,39 @@ pub(crate) fn init_canonical(ruby: &Ruby, module: &RModule) -> Result<(), Error>
1716
1890
  raw_view.define_method("inspect", method!(RawView::inspect, 0))?;
1717
1891
  raw_view.define_method("deconstruct_keys", method!(RawView::deconstruct_keys, 1))?;
1718
1892
 
1893
+ let cause = canonical_module.define_class("Cause", ruby.class_object())?;
1894
+ cause.define_method("pointer", method!(Cause::pointer, 0))?;
1895
+ cause.define_method("keywords", method!(Cause::keywords, 0))?;
1896
+ cause.define_method("==", method!(Cause::eq, 1))?;
1897
+ cause.define_method("inspect", method!(Cause::inspect, 0))?;
1898
+ cause.define_method("deconstruct_keys", method!(Cause::deconstruct_keys, 1))?;
1899
+
1900
+ let literal_reason = canonical_module.define_class("LiteralReason", ruby.class_object())?;
1901
+ literal_reason.define_method("inspect", method!(LiteralReason::inspect, 0))?;
1902
+ literal_reason.define_method(
1903
+ "deconstruct_keys",
1904
+ method!(LiteralReason::deconstruct_keys, 1),
1905
+ )?;
1906
+
1907
+ let empty_reason = canonical_module.define_class("EmptyReason", ruby.class_object())?;
1908
+ empty_reason.define_method("cause", method!(EmptyReason::cause, 0))?;
1909
+ empty_reason.define_method("inspect", method!(EmptyReason::inspect, 0))?;
1910
+ empty_reason.define_method(
1911
+ "deconstruct_keys",
1912
+ method!(EmptyReason::deconstruct_keys, 1),
1913
+ )?;
1914
+
1915
+ let conflict_reason = canonical_module.define_class("ConflictReason", ruby.class_object())?;
1916
+ conflict_reason.define_method("causes", method!(ConflictReason::causes, 0))?;
1917
+ conflict_reason.define_method("inspect", method!(ConflictReason::inspect, 0))?;
1918
+ conflict_reason.define_method(
1919
+ "deconstruct_keys",
1920
+ method!(ConflictReason::deconstruct_keys, 1),
1921
+ )?;
1922
+
1923
+ canonical_module
1924
+ .define_singleton_method("find_unsatisfiable", function!(find_unsatisfiable, -1))?;
1925
+
1719
1926
  let json_module = canonical_module.define_module("JSON")?;
1720
1927
  json_module
1721
1928
  .define_singleton_method("to_string", function!(crate::canonical_json_to_string, 1))?;
data/src/lib.rs CHANGED
@@ -18,7 +18,7 @@ mod validator_map;
18
18
  use jsonschema::{
19
19
  json::{
20
20
  magnus_child, magnus_invalidate_members_cache, magnus_is_object, magnus_probe_root,
21
- magnus_take_pending_error, Magnus, MagnusPendingErrorScope, PendingError, RbNode,
21
+ magnus_take_pending_error, Magnus, MagnusPendingErrorScope, RbNode,
22
22
  },
23
23
  paths::LocationSegment,
24
24
  ValidationOptions,
@@ -550,19 +550,10 @@ fn handle_callback_panic(ruby: &Ruby, err: Box<dyn std::any::Any + Send>) -> Err
550
550
  })
551
551
  }
552
552
 
553
- fn pending_error_to_ruby(ruby: &Ruby, error: PendingError) -> Error {
554
- match error {
555
- PendingError::Type(message) => Error::new(ruby.exception_type_error(), message),
556
- PendingError::Encoding(message) => Error::new(ruby.exception_encoding_error(), message),
557
- PendingError::Argument(message) => Error::new(ruby.exception_arg_error(), message),
558
- }
559
- }
560
-
561
553
  /// Run validation over `instance` in place, raising anything the representation could only record.
562
554
  ///
563
555
  /// Accessors cannot fail, so a value the representation cannot read is stashed and surfaced here.
564
556
  fn surface_pending_errors<T>(
565
- ruby: &Ruby,
566
557
  instance: Value,
567
558
  run: impl FnOnce(RbNode<'_>) -> Result<T, Error>,
568
559
  ) -> Result<T, Error> {
@@ -573,11 +564,11 @@ fn surface_pending_errors<T>(
573
564
  let node = RbNode::new(instance.as_raw());
574
565
  magnus_probe_root(node);
575
566
  if let Some(error) = magnus_take_pending_error() {
576
- return Err(pending_error_to_ruby(ruby, error));
567
+ return Err(Error::from(error));
577
568
  }
578
569
  let result = run(node)?;
579
570
  if let Some(error) = magnus_take_pending_error() {
580
- return Err(pending_error_to_ruby(ruby, error));
571
+ return Err(Error::from(error));
581
572
  }
582
573
  Ok(result)
583
574
  }
@@ -633,14 +624,14 @@ impl Validator {
633
624
  }
634
625
 
635
626
  fn is_valid(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<bool, Error> {
636
- surface_pending_errors(ruby, instance, |node| {
627
+ surface_pending_errors(instance, |node| {
637
628
  let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.is_valid(node)));
638
629
  result.map_err(|err| handle_callback_panic(ruby, err))
639
630
  })
640
631
  }
641
632
 
642
633
  fn validate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<(), Error> {
643
- surface_pending_errors(ruby, instance, |node| {
634
+ surface_pending_errors(instance, |node| {
644
635
  let _scope = KeywordCauseScope::enter();
645
636
  let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.validate(node)));
646
637
  match result {
@@ -657,7 +648,7 @@ impl Validator {
657
648
  }
658
649
 
659
650
  fn iter_errors(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Value, Error> {
660
- surface_pending_errors(ruby, instance, |node| {
651
+ surface_pending_errors(instance, |node| {
661
652
  let _scope = KeywordCauseScope::enter();
662
653
 
663
654
  if ruby.block_given() {
@@ -704,7 +695,7 @@ impl Validator {
704
695
  }
705
696
 
706
697
  fn evaluate(ruby: &Ruby, rb_self: &Self, instance: Value) -> Result<Evaluation, Error> {
707
- surface_pending_errors(ruby, instance, |node| {
698
+ surface_pending_errors(instance, |node| {
708
699
  let _scope = KeywordCauseScope::enter();
709
700
  let result = catch_unwind_silent(AssertUnwindSafe(|| rb_self.validator.evaluate(node)));
710
701
  match result {
@@ -923,7 +914,7 @@ fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
923
914
  &json_schema,
924
915
  )?;
925
916
 
926
- surface_pending_errors(ruby, instance, |node| {
917
+ surface_pending_errors(instance, |node| {
927
918
  let _callback_roots =
928
919
  has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
929
920
  catch_unwind_silent(AssertUnwindSafe(|| validator.is_valid(node)))
@@ -954,7 +945,7 @@ fn validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
954
945
  &json_schema,
955
946
  )?;
956
947
 
957
- surface_pending_errors(ruby, instance, |node| {
948
+ surface_pending_errors(instance, |node| {
958
949
  let _callback_roots =
959
950
  has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
960
951
  match catch_unwind_silent(AssertUnwindSafe(|| validator.validate(node))) {
@@ -993,7 +984,7 @@ fn each_error(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
993
984
  &json_schema,
994
985
  )?;
995
986
 
996
- surface_pending_errors(ruby, instance, |node| {
987
+ surface_pending_errors(instance, |node| {
997
988
  let _callback_roots =
998
989
  has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
999
990
 
@@ -1057,7 +1048,7 @@ fn evaluate(ruby: &Ruby, args: &[Value]) -> Result<Evaluation, Error> {
1057
1048
  &json_schema,
1058
1049
  )?;
1059
1050
 
1060
- surface_pending_errors(ruby, instance, |node| {
1051
+ surface_pending_errors(instance, |node| {
1061
1052
  let _callback_roots =
1062
1053
  has_ruby_callbacks.then(|| CallbackRootGuard::new(ruby, &callback_roots));
1063
1054
  match catch_unwind_silent(AssertUnwindSafe(|| validator.evaluate(node))) {
@@ -1161,24 +1152,40 @@ define_draft_validator!(
1161
1152
  jsonschema::Draft::Draft202012
1162
1153
  );
1163
1154
 
1164
- fn meta_is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1165
- use magnus::scan_args::get_kwargs;
1166
- let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1167
- let (schema,) = parsed_args.required;
1168
- let kw: magnus::scan_args::KwArgs<(), (Option<Option<&Registry>>,), ()> =
1169
- get_kwargs(parsed_args.keywords, &[], &[*options::KW_REGISTRY])?;
1170
- let registry = kw.optional.0.flatten();
1155
+ // A Ruby string holds JSON text; anything else is read in place.
1156
+ fn json_text_schema(schema: Value) -> Option<serde_json::Value> {
1157
+ let rstring = RString::from_value(schema)?;
1158
+ #[allow(unsafe_code)]
1159
+ let bytes = unsafe { rstring.as_slice() };
1160
+ serde_json::from_slice(bytes).ok()
1161
+ }
1171
1162
 
1172
- let json_schema = to_schema_value(ruby, schema)?;
1163
+ // The bundled meta-schemas are compiled in and read the Ruby object in place; `None` sends a
1164
+ // `$schema` outside them through its chain.
1165
+ fn compiled_meta_is_valid(schema: Value) -> Option<Result<bool, Error>> {
1166
+ #[allow(unsafe_code)]
1167
+ let node = RbNode::new(schema.as_raw());
1168
+ match jsonschema::meta::magnus::draft_of(node) {
1169
+ jsonschema::Draft::Unknown => None,
1170
+ draft => Some(jsonschema::meta::magnus::is_valid_fn(draft)(&schema)),
1171
+ }
1172
+ }
1173
1173
 
1174
- let result = if let Some(registry) = registry {
1175
- jsonschema::meta::options()
1176
- .with_registry(registry.inner.as_ref())
1177
- .validate(&json_schema)
1178
- } else {
1179
- jsonschema::meta::validate(&json_schema)
1180
- };
1174
+ fn compiled_meta_validate(
1175
+ schema: &Value,
1176
+ ) -> Option<Result<Result<(), jsonschema::ValidationError<'_>>, Error>> {
1177
+ #[allow(unsafe_code)]
1178
+ let node = RbNode::new(schema.as_raw());
1179
+ match jsonschema::meta::magnus::draft_of(node) {
1180
+ jsonschema::Draft::Unknown => None,
1181
+ draft => Some(jsonschema::meta::magnus::validate_fn(draft)(schema)),
1182
+ }
1183
+ }
1181
1184
 
1185
+ fn meta_result_to_bool(
1186
+ ruby: &Ruby,
1187
+ result: Result<(), jsonschema::ValidationError<'_>>,
1188
+ ) -> Result<bool, Error> {
1182
1189
  match result {
1183
1190
  Ok(()) => Ok(true),
1184
1191
  Err(error) => {
@@ -1190,7 +1197,7 @@ fn meta_is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1190
1197
  }
1191
1198
  }
1192
1199
 
1193
- fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1200
+ fn meta_is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
1194
1201
  use magnus::scan_args::get_kwargs;
1195
1202
  let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1196
1203
  let (schema,) = parsed_args.required;
@@ -1198,27 +1205,59 @@ fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1198
1205
  get_kwargs(parsed_args.keywords, &[], &[*options::KW_REGISTRY])?;
1199
1206
  let registry = kw.optional.0.flatten();
1200
1207
 
1201
- let json_schema = to_schema_value(ruby, schema)?;
1202
-
1203
- let result = if let Some(registry) = registry {
1204
- jsonschema::meta::options()
1208
+ if let Some(registry) = registry {
1209
+ let json_schema = to_schema_value(ruby, schema)?;
1210
+ let result = jsonschema::meta::options()
1205
1211
  .with_registry(registry.inner.as_ref())
1206
- .validate(&json_schema)
1207
- } else {
1208
- jsonschema::meta::validate(&json_schema)
1209
- };
1212
+ .validate(&json_schema);
1213
+ return meta_result_to_bool(ruby, result);
1214
+ }
1215
+ if let Some(json_schema) = json_text_schema(schema) {
1216
+ return meta_result_to_bool(ruby, jsonschema::meta::validate(&json_schema));
1217
+ }
1218
+ surface_pending_errors(schema, |node| {
1219
+ if let Some(result) = compiled_meta_is_valid(schema) {
1220
+ return result;
1221
+ }
1222
+ match jsonschema::meta::is_valid_for::<Magnus>(node) {
1223
+ Ok(valid) => Ok(valid),
1224
+ Err(error) => meta_result_to_bool(ruby, Err(error)),
1225
+ }
1226
+ })
1227
+ }
1210
1228
 
1211
- match result {
1212
- Ok(()) => Ok(()),
1213
- Err(error) => {
1214
- if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
1215
- return Err(referencing_error(ruby, err.to_string()));
1216
- }
1217
- #[allow(unsafe_code)]
1218
- let node = RbNode::new(schema.as_raw());
1219
- Err(raise_validation_error(ruby, error, Some(node), None))
1229
+ fn meta_validate(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
1230
+ use magnus::scan_args::get_kwargs;
1231
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1232
+ let (schema,) = parsed_args.required;
1233
+ let kw: magnus::scan_args::KwArgs<(), (Option<Option<&Registry>>,), ()> =
1234
+ get_kwargs(parsed_args.keywords, &[], &[*options::KW_REGISTRY])?;
1235
+ let registry = kw.optional.0.flatten();
1236
+
1237
+ let raise = |error: jsonschema::ValidationError<'_>| {
1238
+ if let jsonschema::error::ValidationErrorKind::Referencing(err) = error.kind() {
1239
+ return referencing_error(ruby, err.to_string());
1220
1240
  }
1241
+ #[allow(unsafe_code)]
1242
+ let node = RbNode::new(schema.as_raw());
1243
+ raise_validation_error(ruby, error, Some(node), None)
1244
+ };
1245
+ if let Some(registry) = registry {
1246
+ let json_schema = to_schema_value(ruby, schema)?;
1247
+ return jsonschema::meta::options()
1248
+ .with_registry(registry.inner.as_ref())
1249
+ .validate(&json_schema)
1250
+ .map_err(raise);
1221
1251
  }
1252
+ if let Some(json_schema) = json_text_schema(schema) {
1253
+ return jsonschema::meta::validate(&json_schema).map_err(raise);
1254
+ }
1255
+ surface_pending_errors(schema, |node| {
1256
+ if let Some(result) = compiled_meta_validate(&schema) {
1257
+ return result?.map_err(raise);
1258
+ }
1259
+ jsonschema::meta::validate_for::<Magnus>(node).map_err(raise)
1260
+ })
1222
1261
  }
1223
1262
 
1224
1263
  // ValidationError instance methods (defined from Rust, called on exception instances)
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.56.0
4
+ version: 0.58.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dygalo