jsonschema_rs 0.55.1 → 0.57.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: 2514fe367de6ec6e45655fb8a9a06b0e10edb78f2a795f4cc7db63c223435ae5
4
- data.tar.gz: 79027de8d2b2ffcda419cd3248b22f956f259a49d52dd9961f813ca6b10d31ff
3
+ metadata.gz: 11217286a118f4602ff911fcb2b4bd68810935fd7bf08fcffdd6ad906804683f
4
+ data.tar.gz: cdbf9193f44e167d360fd34032089368d86dc6971f87fe7118da6549b46efd36
5
5
  SHA512:
6
- metadata.gz: a2d648f5826ce9490f9ec562015ece34eac2f296a5566d746d380de601ce1799d4acdf91aa99014e8aec1a5d1d79d65f5ec9cced8e4bbae9412e672e55fb5e9a
7
- data.tar.gz: 6387cb7a07b4f4bf260109770e3e81aff8ff7c31ec560231cdb0abf9abb592876046e0fec456754c413d5ff85dcb5085e25eac6bf858d7ad0cf853933c4d8bf6
6
+ metadata.gz: '0875f5b26b56167b3e71a6278620d72db85ea80d5c8b14fa44197c5cfc36b12ec85703c809ed07bd5bb847dcebfc018a18cecb1d5727c05809ff50af3903a85d'
7
+ data.tar.gz: 537158f7caeba6c57e103b6630869cd78b929c2e3ffee0e12c7b267797b89449fb97446d7d163040e65a9e39c9d1916e61e298441ccb01ea90061d447a923033
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.57.0] - 2026-09-22
6
+
7
+ ### Added
8
+
9
+ - `Canonical.find_unsatisfiable`, naming every subschema of a document that admits no value, and the keywords that leave it empty.
10
+
11
+ ### Fixed
12
+
13
+ - The instance path of a schema build error, which was empty instead of naming the keyword location that failed to compile.
14
+
15
+ ## [0.56.0] - 2026-09-10
16
+
17
+ ### Added
18
+
19
+ - `JSONSchema::Canonical::RawReason`, what stopped a run from modeling a document, and `RawView#reason` and `RawView#pointer`, carrying it beside the subschema the run stopped on.
20
+
21
+ ### Changed
22
+
23
+ - Canonicalization of a `oneOf` whose branches share a region too wide to expand, which spent the document's whole intersection allowance on that choice and kept the document `Raw`.
24
+
25
+ ### Performance
26
+
27
+ - Preparing or building a schema that names no base URI parses the default one once for the process instead of once per schema.
28
+ - Canonicalization reads the facets no checker covers once per node instead of once per containment query.
29
+
5
30
  ## [0.55.1] - 2026-09-08
6
31
 
7
32
  ### Changed
@@ -552,7 +577,9 @@
552
577
 
553
578
  - Initial public release
554
579
 
555
- [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.55.1...HEAD
580
+ [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.57.0...HEAD
581
+ [0.57.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.56.0...ruby-v0.57.0
582
+ [0.56.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.55.1...ruby-v0.56.0
556
583
  [0.55.1]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.55.0...ruby-v0.55.1
557
584
  [0.55.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.54.0...ruby-v0.55.0
558
585
  [0.54.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.53.0...ruby-v0.54.0
data/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb"
3
- version = "0.55.1"
3
+ version = "0.57.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.55.1", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
17
+ jsonschema = { version = "0.57.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.
@@ -693,9 +693,9 @@ dependencies = [
693
693
 
694
694
  [[package]]
695
695
  name = "jsonschema"
696
- version = "0.55.1"
696
+ version = "0.57.0"
697
697
  source = "registry+https://github.com/rust-lang/crates.io-index"
698
- checksum = "b68339c3d874e48151d74ffe256d93a58cffa240983cb0967d3cbaea083a44fe"
698
+ checksum = "71160ed5f6dbe36a2d6be79f4ca03ee09a99d2866f340faed49458913449aefa"
699
699
  dependencies = [
700
700
  "ahash",
701
701
  "bytecount",
@@ -726,9 +726,9 @@ dependencies = [
726
726
 
727
727
  [[package]]
728
728
  name = "jsonschema-macros"
729
- version = "0.55.1"
729
+ version = "0.57.0"
730
730
  source = "registry+https://github.com/rust-lang/crates.io-index"
731
- checksum = "0ee1d26bd968d4f88b291615dada57b6acc38662bae4e2244c6c9fc31e70bacc"
731
+ checksum = "9b5421caa3ea97392a353a9bc57b33ac844a6a128f4c3d11ac6c12cc462072ac"
732
732
  dependencies = [
733
733
  "jsonschema-macros-core",
734
734
  "proc-macro2",
@@ -736,9 +736,9 @@ dependencies = [
736
736
 
737
737
  [[package]]
738
738
  name = "jsonschema-macros-core"
739
- version = "0.55.1"
739
+ version = "0.57.0"
740
740
  source = "registry+https://github.com/rust-lang/crates.io-index"
741
- checksum = "676d45db449ae16bce2582dcff3e7b2dfb177a3c2f885af072257815bc8825c4"
741
+ checksum = "7c1e51f90029687e9fe880db5ff56b1a99e66eef2464a862f7a8408aed851168"
742
742
  dependencies = [
743
743
  "fancy-regex",
744
744
  "indexmap",
@@ -755,7 +755,7 @@ dependencies = [
755
755
 
756
756
  [[package]]
757
757
  name = "jsonschema-rb-ext"
758
- version = "0.55.1"
758
+ version = "0.57.0"
759
759
  dependencies = [
760
760
  "jsonschema",
761
761
  "magnus",
@@ -769,18 +769,18 @@ dependencies = [
769
769
 
770
770
  [[package]]
771
771
  name = "jsonschema-regex"
772
- version = "0.55.1"
772
+ version = "0.57.0"
773
773
  source = "registry+https://github.com/rust-lang/crates.io-index"
774
- checksum = "6307b5b51216ec9b941b52244c74043fa0b1d6b657b56199f57cb1416d3641c5"
774
+ checksum = "48d2120d8466ffcdc1b3be4b88ff0e9191bf5b6b09b1b1af1eef9aff8f465ea3"
775
775
  dependencies = [
776
776
  "regex-syntax",
777
777
  ]
778
778
 
779
779
  [[package]]
780
780
  name = "jsonschema-value"
781
- version = "0.55.1"
781
+ version = "0.57.0"
782
782
  source = "registry+https://github.com/rust-lang/crates.io-index"
783
- checksum = "0230ac05e09c6111e96c147b75c390579f5cbd45654b980c68ac60fe17b3f129"
783
+ checksum = "3fbfa40a42415369d940b3848f3ce08099f1f1ac04d73e8580f4d652617c4f44"
784
784
  dependencies = [
785
785
  "ahash",
786
786
  "bytecount",
@@ -1135,9 +1135,9 @@ dependencies = [
1135
1135
 
1136
1136
  [[package]]
1137
1137
  name = "referencing"
1138
- version = "0.55.1"
1138
+ version = "0.57.0"
1139
1139
  source = "registry+https://github.com/rust-lang/crates.io-index"
1140
- checksum = "a196a5b4a8a12f46b6353174df865a05d41a6055aff212ec30877492788618b6"
1140
+ checksum = "b06f6798be4fed305e74df8b1fb90cf59c6b8cc17aa3febeb1830c0c6b627b3e"
1141
1141
  dependencies = [
1142
1142
  "ahash",
1143
1143
  "fluent-uri",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.55.1"
3
+ version = "0.57.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.55.1", default-features = false, features = ["magnus", "arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros", "idna"] }
13
+ jsonschema = { version = "0.57.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.55.1"
16
+ referencing = "0.57.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.55.1"
4
+ VERSION = "0.57.0"
5
5
  end
data/sig/jsonschema.rbs CHANGED
@@ -23,6 +23,10 @@ module JSONSchema
23
23
  # What an array requires of its elements: all distinct, some repeated, or neither.
24
24
  type distinctness = :unconstrained | :all_distinct | :some_repeated
25
25
 
26
+ # What stopped a run from modeling a document.
27
+ type raw_reason = :unknown_dialect | :unmodeled | :outgrew_intersections
28
+ | :outgrew_cases | :inexact_intersection
29
+
26
30
  # Whether one schema admits every value another does. Only `:yes` proves containment: test
27
31
  # for `:yes`, and treat `:unknown` like `:no`.
28
32
  type containment = :yes | :no | :unknown
@@ -57,6 +61,15 @@ module JSONSchema
57
61
  ALL: Array[distinctness]
58
62
  end
59
63
 
64
+ module RawReason
65
+ UNKNOWN_DIALECT: :unknown_dialect
66
+ UNMODELED: :unmodeled
67
+ OUTGREW_INTERSECTIONS: :outgrew_intersections
68
+ OUTGREW_CASES: :outgrew_cases
69
+ INEXACT_INTERSECTION: :inexact_intersection
70
+ ALL: Array[raw_reason]
71
+ end
72
+
60
73
  module Kind
61
74
  MULTI_TYPE: :multi_type
62
75
  TYPED_GROUP: :typed_group
@@ -288,9 +301,60 @@ module JSONSchema
288
301
  # A schema the canonical form does not support structurally, kept verbatim.
289
302
  class RawView
290
303
  def schema: () -> untyped
304
+ def reason: () -> raw_reason
305
+ def pointer: () -> String?
291
306
  def inspect: () -> String
292
307
  def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
293
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]
294
358
  end
295
359
 
296
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,
5
- ObjectViolationView as CoreObjectViolationView, Satisfiability,
3
+ CanonicalKind, CanonicalSchema, CanonicalView, CanonicalizationError, Cause as CoreCause,
4
+ Containment, ContainsView as CoreContainsView, Distinctness,
5
+ ObjectViolationView as CoreObjectViolationView, RawReason, Satisfiability,
6
+ UnsatisfiableReason as CoreUnsatisfiableReason,
6
7
  },
7
8
  JsonType,
8
9
  };
@@ -226,7 +227,13 @@ impl RbCanonicalSchema {
226
227
  CanonicalView::AnyOf(branches) => ruby.obj_wrap(AnyOfView { branches }).as_value(),
227
228
  CanonicalView::OneOf(branches) => ruby.obj_wrap(OneOfView { branches }).as_value(),
228
229
  CanonicalView::Reference(uri) => ruby.obj_wrap(ReferenceView { uri }).as_value(),
229
- CanonicalView::Raw(schema) => ruby.obj_wrap(RawView { schema }).as_value(),
230
+ CanonicalView::Raw(raw) => ruby
231
+ .obj_wrap(RawView {
232
+ schema: raw.schema,
233
+ reason: raw.reason,
234
+ pointer: raw.pointer,
235
+ })
236
+ .as_value(),
230
237
  }
231
238
  }
232
239
 
@@ -293,6 +300,8 @@ impl RbCanonicalSchema {
293
300
  #[magnus(class = "JSONSchema::Canonical::RawView", free_immediately)]
294
301
  pub struct RawView {
295
302
  schema: serde_json::Value,
303
+ reason: RawReason,
304
+ pointer: Option<String>,
296
305
  }
297
306
 
298
307
  impl DataTypeFunctions for RawView {}
@@ -302,10 +311,23 @@ impl RawView {
302
311
  value_to_ruby(ruby, &rb_self.schema)
303
312
  }
304
313
 
314
+ fn reason(ruby: &Ruby, rb_self: &Self) -> Value {
315
+ ruby.sym_new(rb_self.reason.as_str()).as_value()
316
+ }
317
+
318
+ fn pointer(ruby: &Ruby, rb_self: &Self) -> Value {
319
+ rb_self.pointer.as_ref().map_or_else(
320
+ || ruby.qnil().as_value(),
321
+ |pointer| ruby.str_new(pointer).as_value(),
322
+ )
323
+ }
324
+
305
325
  fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
306
326
  Ok(format!(
307
- "#<JSONSchema::Canonical::RawView schema={}>",
308
- Self::schema(ruby, rb_self)?.inspect()
327
+ "#<JSONSchema::Canonical::RawView schema={} reason={} pointer={}>",
328
+ Self::schema(ruby, rb_self)?.inspect(),
329
+ Self::reason(ruby, rb_self).inspect(),
330
+ Self::pointer(ruby, rb_self).inspect()
309
331
  ))
310
332
  }
311
333
 
@@ -315,6 +337,8 @@ impl RawView {
315
337
  ruby.sym_new("schema"),
316
338
  value_to_ruby(ruby, &rb_self.schema)?,
317
339
  )?;
340
+ hash.aset(ruby.sym_new("reason"), Self::reason(ruby, rb_self))?;
341
+ hash.aset(ruby.sym_new("pointer"), Self::pointer(ruby, rb_self))?;
318
342
  Ok(hash)
319
343
  }
320
344
  }
@@ -1351,10 +1375,154 @@ impl EnumView {
1351
1375
  }
1352
1376
  }
1353
1377
 
1354
- fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1355
- let parsed = scan_args::<(Value,), (), (), (), _, ()>(args)?;
1356
- let (schema_arg,) = parsed.required;
1357
- 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> {
1358
1526
  let base_kwargs: CanonicalKwArgs = get_kwargs(
1359
1527
  keywords,
1360
1528
  &[],
@@ -1370,7 +1538,6 @@ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1370
1538
  let (draft_val, validate_formats, pattern_options, retriever_val, registry_val, base_uri) =
1371
1539
  base_kwargs.optional;
1372
1540
 
1373
- let schema_value = to_schema_value(ruby, schema_arg)?;
1374
1541
  let mut options = jsonschema::canonical::options();
1375
1542
  if let Some(draft) = draft_val {
1376
1543
  options = options.with_draft(parse_draft_symbol(ruby, draft)?);
@@ -1391,31 +1558,61 @@ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
1391
1558
  has_retriever = true;
1392
1559
  }
1393
1560
  }
1394
- if let Some(val) = registry_val {
1395
- if !val.is_nil() {
1396
- let registry: &Registry = TryConvert::try_convert(val).map_err(|_| {
1397
- Error::new(
1398
- ruby.exception_type_error(),
1399
- "registry must be a JSONSchema::Registry instance",
1400
- )
1401
- })?;
1402
- if !has_retriever {
1403
- if let Some(value) = registry.retriever_value(ruby) {
1404
- if let Some(retriever) = make_retriever(ruby, value)? {
1405
- options = options.with_retriever(retriever);
1406
- }
1407
- }
1408
- }
1409
- options = options.with_registry(registry.inner.as_ref());
1410
- }
1411
- }
1412
1561
  if let Some(base_uri) = base_uri {
1413
1562
  options = options.with_base_uri(base_uri);
1414
1563
  }
1415
- options
1416
- .canonicalize(&schema_value)
1417
- .map(|inner| ruby.obj_wrap(RbCanonicalSchema { inner }).as_value())
1418
- .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
+ })
1419
1616
  }
1420
1617
 
1421
1618
  fn define_labels<Label: Copy + Into<&'static str>>(
@@ -1467,6 +1664,7 @@ pub(crate) fn init_canonical(ruby: &Ruby, module: &RModule) -> Result<(), Error>
1467
1664
  Distinctness::VARIANTS,
1468
1665
  )?;
1469
1666
  define_labels(ruby, &canonical_module, "Kind", CanonicalKind::VARIANTS)?;
1667
+ define_labels(ruby, &canonical_module, "RawReason", RawReason::VARIANTS)?;
1470
1668
 
1471
1669
  let canonical_schema = canonical_module.define_class("CanonicalSchema", ruby.class_object())?;
1472
1670
  canonical_schema.define_method(
@@ -1687,9 +1885,44 @@ pub(crate) fn init_canonical(ruby: &Ruby, module: &RModule) -> Result<(), Error>
1687
1885
 
1688
1886
  let raw_view = canonical_module.define_class("RawView", ruby.class_object())?;
1689
1887
  raw_view.define_method("schema", method!(RawView::schema, 0))?;
1888
+ raw_view.define_method("reason", method!(RawView::reason, 0))?;
1889
+ raw_view.define_method("pointer", method!(RawView::pointer, 0))?;
1690
1890
  raw_view.define_method("inspect", method!(RawView::inspect, 0))?;
1691
1891
  raw_view.define_method("deconstruct_keys", method!(RawView::deconstruct_keys, 1))?;
1692
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
+
1693
1926
  let json_module = canonical_module.define_module("JSON")?;
1694
1927
  json_module
1695
1928
  .define_singleton_method("to_string", function!(crate::canonical_json_to_string, 1))?;
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.55.1
4
+ version: 0.57.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dygalo