jsonschema_rs 0.48.1 → 0.48.5

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: dff84fb88165fc24b6035c6b100e89c83c4b6f7194fc945c3ec73e93f5119178
4
- data.tar.gz: feac688548b70b5cc57f455c5f78c76fbe3107f9b38f03220ab98ce110018668
3
+ metadata.gz: 60ae1d2ffaa7543f17f5a7953a2ed92520f3887f7e92c8c8ac632f7c56e515ef
4
+ data.tar.gz: 8c3ac5d3f66016a881eed8a6f9f78327e32373e738ab453c39f0449365fce8a9
5
5
  SHA512:
6
- metadata.gz: 64315bbd608bd3a80a74fa392e15c9eb57a6887dbe913d68a194f2d58c889c35c8e798b7abfb3ef619775ba9e34378de6ae3425ff4221b630f0788b8cebf40d5
7
- data.tar.gz: a86e31921a0498b479e841ea25cb4bc5083dd54f9a055513c8fda434c865216992561bd92ec7a6a5543671c919227a5039f976a6f882e1cb7576dc741dedde34
6
+ metadata.gz: 3878552e31d235939c2fc5f4a7665002fa7ee604d4fed06a899ea78679b5ea76183322524b828376e5d891c154426a2fb1167604444eefcb8509afda8b477501
7
+ data.tar.gz: 9e6c6b67a2d3bea2ce93c31c1da0947832708ee55941950860ace90959ad4eb1d66c31e6a66273d17a3bd567f6fec739046c7b18deb6335e326cddfaab5fcdf1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.48.5] - 2026-07-22
6
+
7
+ ### Performance
8
+
9
+ - Avoid map lookups in some `properties` validators.
10
+ - Faster validation of `{"type": "array", "items": {...}}` schemas.
11
+
12
+ ## [0.48.2] - 2026-07-21
13
+
14
+ ### Fixed
15
+
16
+ - `Canonical::JSON.to_string` incorrectly emitting exponent form for small `Float` values (e.g. `1e-7` instead of `0.0000001`).
17
+
18
+ ### Performance
19
+
20
+ - Faster validator compilation by pre-sizing internal caches.
21
+
5
22
  ## [0.48.1] - 2026-07-17
6
23
 
7
24
  ### Fixed
@@ -182,7 +199,9 @@
182
199
 
183
200
  - Initial public release
184
201
 
185
- [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.48.1...HEAD
202
+ [Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.48.5...HEAD
203
+ [0.48.5]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.48.2...ruby-v0.48.5
204
+ [0.48.2]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.48.1...ruby-v0.48.2
186
205
  [0.48.1]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.48.0...ruby-v0.48.1
187
206
  [0.48.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.47.0...ruby-v0.48.0
188
207
  [0.47.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.46.10...ruby-v0.47.0
data/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb"
3
- version = "0.48.1"
3
+ version = "0.48.5"
4
4
  edition = "2021"
5
5
  authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
6
6
  license = "MIT"
@@ -13,12 +13,13 @@ publish = false
13
13
  crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
- jsonschema = { version = "0.48.1", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros"] }
16
+ jsonschema = { version = "0.48.5", 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 }
20
20
  serde_json = { workspace = true, features = ["arbitrary_precision"] }
21
21
  referencing = { version = "*", path = "../jsonschema-referencing" }
22
+ zmij = "1.0.23"
22
23
 
23
24
  [lints]
24
25
  workspace = true
@@ -387,6 +387,12 @@ dependencies = [
387
387
  "foldhash",
388
388
  ]
389
389
 
390
+ [[package]]
391
+ name = "heck"
392
+ version = "0.5.0"
393
+ source = "registry+https://github.com/rust-lang/crates.io-index"
394
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
395
+
390
396
  [[package]]
391
397
  name = "http"
392
398
  version = "1.4.0"
@@ -687,9 +693,9 @@ dependencies = [
687
693
 
688
694
  [[package]]
689
695
  name = "jsonschema"
690
- version = "0.48.1"
696
+ version = "0.48.5"
691
697
  source = "registry+https://github.com/rust-lang/crates.io-index"
692
- checksum = "9baf521a926fd1e6f94af6922be9686b61c8a89a5fb7e14d6a1e21b79738d4cc"
698
+ checksum = "f05a6cff806294a7699d48761d4ae2bdebab7fa2eef05e4a0b83320f0ff46901"
693
699
  dependencies = [
694
700
  "ahash",
695
701
  "bytecount",
@@ -702,6 +708,7 @@ dependencies = [
702
708
  "itoa",
703
709
  "jsonschema-macros",
704
710
  "jsonschema-regex",
711
+ "jsonschema-value",
705
712
  "num-bigint",
706
713
  "num-cmp",
707
714
  "num-traits",
@@ -712,15 +719,16 @@ dependencies = [
712
719
  "rustls",
713
720
  "serde",
714
721
  "serde_json",
722
+ "strum",
715
723
  "unicode-general-category",
716
724
  "uuid-simd",
717
725
  ]
718
726
 
719
727
  [[package]]
720
728
  name = "jsonschema-macros"
721
- version = "0.48.1"
729
+ version = "0.48.5"
722
730
  source = "registry+https://github.com/rust-lang/crates.io-index"
723
- checksum = "11efb171797c352f167fd4f3754b3a5e969d814222e457e63136cffca111b014"
731
+ checksum = "40bda6e08b09b66cc0f70c0768118b51e8da7cef847036c88a68471b15101bcb"
724
732
  dependencies = [
725
733
  "jsonschema-macros-core",
726
734
  "proc-macro2",
@@ -728,9 +736,9 @@ dependencies = [
728
736
 
729
737
  [[package]]
730
738
  name = "jsonschema-macros-core"
731
- version = "0.48.1"
739
+ version = "0.48.5"
732
740
  source = "registry+https://github.com/rust-lang/crates.io-index"
733
- checksum = "a3ce6144063dbeca5c841049bc0958d2eaf7d0a20d600be0907c29a2d1a6267a"
741
+ checksum = "5f884bf73113c53f6b1d23f14c68a09faebf118854ce122a1afe91ce73d050d3"
734
742
  dependencies = [
735
743
  "fancy-regex",
736
744
  "indexmap",
@@ -747,7 +755,7 @@ dependencies = [
747
755
 
748
756
  [[package]]
749
757
  name = "jsonschema-rb-ext"
750
- version = "0.48.1"
758
+ version = "0.48.5"
751
759
  dependencies = [
752
760
  "jsonschema",
753
761
  "magnus",
@@ -755,17 +763,33 @@ dependencies = [
755
763
  "referencing",
756
764
  "serde",
757
765
  "serde_json",
766
+ "zmij",
758
767
  ]
759
768
 
760
769
  [[package]]
761
770
  name = "jsonschema-regex"
762
- version = "0.48.1"
771
+ version = "0.48.5"
763
772
  source = "registry+https://github.com/rust-lang/crates.io-index"
764
- checksum = "d668f4775619127513d9a4f190704d20a66d20ccf0efc258f8ed42548e5fd7cd"
773
+ checksum = "84708dd3f6c5327478a96f37378adb27deed6a626cf84d19de780d9e8ae84b17"
765
774
  dependencies = [
766
775
  "regex-syntax",
767
776
  ]
768
777
 
778
+ [[package]]
779
+ name = "jsonschema-value"
780
+ version = "0.48.5"
781
+ source = "registry+https://github.com/rust-lang/crates.io-index"
782
+ checksum = "fd8c04ab4ce60e42c4e302d3bd9d184652bb56f212495d2eea23573bcbf72556"
783
+ dependencies = [
784
+ "ahash",
785
+ "bytecount",
786
+ "fraction",
787
+ "num-bigint",
788
+ "num-cmp",
789
+ "num-traits",
790
+ "serde_json",
791
+ ]
792
+
769
793
  [[package]]
770
794
  name = "lazy_static"
771
795
  version = "1.5.0"
@@ -1106,9 +1130,9 @@ dependencies = [
1106
1130
 
1107
1131
  [[package]]
1108
1132
  name = "referencing"
1109
- version = "0.48.1"
1133
+ version = "0.48.5"
1110
1134
  source = "registry+https://github.com/rust-lang/crates.io-index"
1111
- checksum = "df843f41f0cb459649f64a8b6b10579cf454f7a7420dc702767c81a26f23d780"
1135
+ checksum = "980e77f5cfffc592c0e7dfb6d20770a0b96a6ab94d403de7454ed9799505e4ba"
1112
1136
  dependencies = [
1113
1137
  "ahash",
1114
1138
  "fluent-uri",
@@ -1424,6 +1448,27 @@ version = "1.2.1"
1424
1448
  source = "registry+https://github.com/rust-lang/crates.io-index"
1425
1449
  checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1426
1450
 
1451
+ [[package]]
1452
+ name = "strum"
1453
+ version = "0.28.0"
1454
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1455
+ checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
1456
+ dependencies = [
1457
+ "strum_macros",
1458
+ ]
1459
+
1460
+ [[package]]
1461
+ name = "strum_macros"
1462
+ version = "0.28.0"
1463
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1464
+ checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
1465
+ dependencies = [
1466
+ "heck",
1467
+ "proc-macro2",
1468
+ "quote",
1469
+ "syn",
1470
+ ]
1471
+
1427
1472
  [[package]]
1428
1473
  name = "subtle"
1429
1474
  version = "2.6.1"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonschema-rb-ext"
3
- version = "0.48.1"
3
+ version = "0.48.5"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -10,11 +10,12 @@ name = "jsonschema_rb"
10
10
  path = "../../src/lib.rs"
11
11
 
12
12
  [dependencies]
13
- jsonschema = { version = "0.48.1", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring", "macros"] }
13
+ jsonschema = { version = "0.48.5", 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.48.1"
16
+ referencing = "0.48.5"
17
17
  serde = { version = "1", features = ["derive"] }
18
18
  serde_json = { version = "1", features = ["arbitrary_precision"] }
19
+ zmij = "1"
19
20
 
20
21
  [workspace]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSchema
4
- VERSION = "0.48.1"
4
+ VERSION = "0.48.5"
5
5
  end
data/sig/jsonschema.rbs CHANGED
@@ -14,8 +14,110 @@ module JSONSchema
14
14
  # Main use case: deduplicate equivalent JSON Schemas by using a stable string form.
15
15
  def self.to_string: (untyped object) -> String
16
16
  end
17
+
18
+ # Structural discriminant of a canonical schema, one per view class.
19
+ type kind = :multi_type | :typed_group | :string | :integer | :const | :enum
20
+ | :any_of | :true | :false | :raw
21
+
22
+ type view = TrueView | FalseView | MultiTypeView | TypedGroupView | StringView
23
+ | IntegerView | AnyOfView | ConstView | EnumView | RawView
24
+
25
+ class CanonicalizationError < StandardError
26
+ end
27
+
28
+ class InvalidSchemaType < CanonicalizationError
29
+ end
30
+
31
+ # A schema reduced to canonical form: schemas accepting the same values share one form.
32
+ class CanonicalSchema
33
+ def to_json_schema: () -> untyped
34
+ def draft: () -> JSONSchema::draft
35
+ def kind: () -> kind
36
+ def view: () -> view
37
+ def definitions: () -> Hash[String, CanonicalSchema]
38
+ def satisfiable?: () -> bool
39
+ def inspect: () -> String
40
+ def ==: (untyped other) -> bool
41
+ def eql?: (untyped other) -> bool
42
+ def hash: () -> Integer
43
+ end
44
+
45
+ # Matches any value.
46
+ class TrueView
47
+ end
48
+
49
+ # Matches no value.
50
+ class FalseView
51
+ end
52
+
53
+ # A value matches iff its JSON type is in the set.
54
+ class MultiTypeView
55
+ def types: () -> Array[Symbol]
56
+ def inspect: () -> String
57
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
58
+ end
59
+
60
+ # A value matches iff its JSON type is `type_name` and it satisfies `body`.
61
+ class TypedGroupView
62
+ def type_name: () -> Symbol
63
+ def body: () -> CanonicalSchema
64
+ def inspect: () -> String
65
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
66
+ end
67
+
68
+ # A string value within a length window, matching every pattern.
69
+ class StringView
70
+ def min_length: () -> Integer?
71
+ def max_length: () -> Integer?
72
+ def patterns: () -> Array[String]
73
+ def inspect: () -> String
74
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
75
+ end
76
+
77
+ # An integer value within a closed interval.
78
+ class IntegerView
79
+ def minimum: () -> Integer?
80
+ def maximum: () -> Integer?
81
+ def inspect: () -> String
82
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
83
+ end
84
+
85
+ # A value matches iff at least one branch matches.
86
+ class AnyOfView
87
+ def branches: () -> Array[CanonicalSchema]
88
+ def inspect: () -> String
89
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
90
+ end
91
+
92
+ # Exactly one admitted value.
93
+ class ConstView
94
+ def value: () -> untyped
95
+ def inspect: () -> String
96
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
97
+ end
98
+
99
+ # A sorted, deduplicated finite set of admitted values.
100
+ class EnumView
101
+ def values: () -> Array[untyped]
102
+ def inspect: () -> String
103
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
104
+ end
105
+
106
+ # A schema the canonical form does not model structurally, kept verbatim.
107
+ class RawView
108
+ def schema: () -> untyped
109
+ def inspect: () -> String
110
+ def deconstruct_keys: (untyped keys) -> Hash[Symbol, untyped]
111
+ end
17
112
  end
18
113
 
114
+ # Reduce a schema to its canonical form.
115
+ def self.canonicalize: (
116
+ untyped schema,
117
+ ?draft: draft?,
118
+ ?validate_formats: bool?
119
+ ) -> Canonical::CanonicalSchema
120
+
19
121
  # Create a validator with auto-detected draft version.
20
122
  #
21
123
  # @param schema The JSON Schema (Hash or JSON string)
data/src/canonical.rs ADDED
@@ -0,0 +1,592 @@
1
+ use jsonschema::{
2
+ canonical::{CanonicalSchema, CanonicalView, CanonicalizationError},
3
+ JsonType,
4
+ };
5
+ use magnus::{
6
+ function, method,
7
+ prelude::*,
8
+ scan_args::{get_kwargs, scan_args},
9
+ typed_data,
10
+ value::Lazy,
11
+ DataTypeFunctions, Error, ExceptionClass, RClass, RHash, RModule, Ruby, TryConvert, Value,
12
+ };
13
+
14
+ use crate::{
15
+ options::parse_draft_symbol,
16
+ ser::{to_schema_value, value_to_ruby},
17
+ static_id::define_rb_intern,
18
+ };
19
+
20
+ define_rb_intern!(static CANONICAL_KW_DRAFT: "draft");
21
+ define_rb_intern!(static CANONICAL_KW_VALIDATE_FORMATS: "validate_formats");
22
+
23
+ macro_rules! canonical_error_class {
24
+ ($static_name:ident, $class_name:literal) => {
25
+ static $static_name: Lazy<ExceptionClass> = Lazy::new(|ruby| {
26
+ let json_schema: RModule = ruby
27
+ .class_object()
28
+ .const_get("JSONSchema")
29
+ .expect("JSONSchema");
30
+ let canonical: RModule = json_schema.const_get("Canonical").expect("Canonical");
31
+ let error_class: RClass = canonical.const_get($class_name).expect($class_name);
32
+ let exception_class =
33
+ ExceptionClass::from_value(error_class.as_value()).expect("ExceptionClass");
34
+ // The cached handle is invisible to Ruby's GC; registration pins it across compaction.
35
+ magnus::gc::register_mark_object(exception_class);
36
+ exception_class
37
+ });
38
+ };
39
+ }
40
+
41
+ canonical_error_class!(CANONICALIZATION_ERROR_CLASS, "CanonicalizationError");
42
+ canonical_error_class!(INVALID_SCHEMA_TYPE_CLASS, "InvalidSchemaType");
43
+
44
+ fn canonicalization_error(ruby: &Ruby, error: CanonicalizationError) -> Error {
45
+ if let CanonicalizationError::ValidationError(validation_error) = error {
46
+ return crate::raise_validation_error(ruby, validation_error, None, None);
47
+ }
48
+ let message = error.to_string();
49
+ match error {
50
+ CanonicalizationError::InvalidSchemaType(_) => {
51
+ Error::new(ruby.get_inner(&INVALID_SCHEMA_TYPE_CLASS), message)
52
+ }
53
+ // `ValidationError` returns above; future variants fall back to the base canonical error.
54
+ _ => Error::new(ruby.get_inner(&CANONICALIZATION_ERROR_CLASS), message),
55
+ }
56
+ }
57
+
58
+ #[derive(magnus::TypedData, PartialEq, Eq, Hash)]
59
+ #[magnus(
60
+ class = "JSONSchema::Canonical::CanonicalSchema",
61
+ free_immediately,
62
+ size
63
+ )]
64
+ pub struct RbCanonicalSchema {
65
+ inner: CanonicalSchema,
66
+ }
67
+
68
+ impl DataTypeFunctions for RbCanonicalSchema {}
69
+
70
+ impl RbCanonicalSchema {
71
+ fn to_json_schema(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
72
+ value_to_ruby(ruby, &rb_self.inner.to_json_schema())
73
+ }
74
+
75
+ fn draft(ruby: &Ruby, rb_self: &Self) -> Value {
76
+ let name = match rb_self.inner.draft() {
77
+ jsonschema::Draft::Draft4 => "draft4",
78
+ jsonschema::Draft::Draft6 => "draft6",
79
+ jsonschema::Draft::Draft7 => "draft7",
80
+ jsonschema::Draft::Draft201909 => "draft201909",
81
+ _ => "draft202012",
82
+ };
83
+ ruby.sym_new(name).as_value()
84
+ }
85
+
86
+ fn kind(ruby: &Ruby, rb_self: &Self) -> Value {
87
+ ruby.sym_new(rb_self.inner.kind().as_str()).as_value()
88
+ }
89
+
90
+ fn satisfiable(rb_self: &Self) -> bool {
91
+ rb_self.inner.is_satisfiable()
92
+ }
93
+
94
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> String {
95
+ // Bounded: `inspect` runs implicitly (IRB, error messages) and a full
96
+ // `to_json_schema` re-emits the whole document.
97
+ format!(
98
+ "#<JSONSchema::Canonical::CanonicalSchema kind={} draft={}>",
99
+ Self::kind(ruby, rb_self).inspect(),
100
+ Self::draft(ruby, rb_self).inspect()
101
+ )
102
+ }
103
+
104
+ fn eq(rb_self: &Self, other: Value) -> bool {
105
+ let Ok(other_ref) = <&RbCanonicalSchema>::try_convert(other) else {
106
+ return false;
107
+ };
108
+ rb_self.inner == other_ref.inner
109
+ }
110
+
111
+ fn view(ruby: &Ruby, rb_self: &Self) -> Value {
112
+ match rb_self.inner.view() {
113
+ CanonicalView::MultiType(set) => ruby
114
+ .obj_wrap(MultiTypeView {
115
+ types: set.iter().map(JsonType::as_str).collect(),
116
+ })
117
+ .as_value(),
118
+ CanonicalView::TypedGroup(group) => ruby
119
+ .obj_wrap(TypedGroupView {
120
+ type_name: group.ty.as_str(),
121
+ body: group.body,
122
+ })
123
+ .as_value(),
124
+ CanonicalView::Const(value) => ruby.obj_wrap(ConstView { value }).as_value(),
125
+ CanonicalView::Enum(values) => ruby.obj_wrap(EnumView { values }).as_value(),
126
+ CanonicalView::True => ruby.obj_wrap(TrueView).as_value(),
127
+ CanonicalView::False => ruby.obj_wrap(FalseView).as_value(),
128
+ CanonicalView::String(view) => ruby
129
+ .obj_wrap(StringView {
130
+ min_length: view.min_length,
131
+ max_length: view.max_length,
132
+ patterns: view.patterns,
133
+ })
134
+ .as_value(),
135
+ CanonicalView::Integer(view) => ruby
136
+ .obj_wrap(IntegerView {
137
+ minimum: view.minimum,
138
+ maximum: view.maximum,
139
+ })
140
+ .as_value(),
141
+ CanonicalView::AnyOf(branches) => ruby.obj_wrap(AnyOfView { branches }).as_value(),
142
+ CanonicalView::Raw(schema) => ruby.obj_wrap(RawView { schema }).as_value(),
143
+ // TODO(canonical): new `CanonicalView` variants need view classes here.
144
+ other => unreachable!("unsupported canonical view: {other:?}"),
145
+ }
146
+ }
147
+
148
+ fn definitions(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
149
+ let hash = ruby.hash_new();
150
+ for (uri, target) in rb_self.inner.definitions() {
151
+ let wrapped = ruby.obj_wrap(RbCanonicalSchema { inner: target });
152
+ hash.aset(uri, wrapped)?;
153
+ }
154
+ Ok(hash)
155
+ }
156
+ }
157
+
158
+ /// A schema the canonical form does not model structurally, kept verbatim.
159
+ #[derive(magnus::TypedData)]
160
+ #[magnus(class = "JSONSchema::Canonical::RawView", free_immediately)]
161
+ pub struct RawView {
162
+ schema: serde_json::Value,
163
+ }
164
+
165
+ impl DataTypeFunctions for RawView {}
166
+
167
+ impl RawView {
168
+ fn schema(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
169
+ value_to_ruby(ruby, &rb_self.schema)
170
+ }
171
+
172
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
173
+ Ok(format!(
174
+ "#<JSONSchema::Canonical::RawView schema={}>",
175
+ Self::schema(ruby, rb_self)?.inspect()
176
+ ))
177
+ }
178
+
179
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
180
+ let hash = ruby.hash_new();
181
+ hash.aset(
182
+ ruby.sym_new("schema"),
183
+ value_to_ruby(ruby, &rb_self.schema)?,
184
+ )?;
185
+ Ok(hash)
186
+ }
187
+ }
188
+
189
+ /// Matches any value.
190
+ #[derive(magnus::TypedData)]
191
+ #[magnus(class = "JSONSchema::Canonical::TrueView", free_immediately)]
192
+ pub struct TrueView;
193
+
194
+ impl TrueView {
195
+ fn inspect(_rb_self: &Self) -> String {
196
+ "#<JSONSchema::Canonical::TrueView>".to_string()
197
+ }
198
+ }
199
+
200
+ impl DataTypeFunctions for TrueView {}
201
+
202
+ /// Matches no value.
203
+ #[derive(magnus::TypedData)]
204
+ #[magnus(class = "JSONSchema::Canonical::FalseView", free_immediately)]
205
+ pub struct FalseView;
206
+
207
+ impl FalseView {
208
+ fn inspect(_rb_self: &Self) -> String {
209
+ "#<JSONSchema::Canonical::FalseView>".to_string()
210
+ }
211
+ }
212
+
213
+ impl DataTypeFunctions for FalseView {}
214
+
215
+ /// A value matches iff its JSON type is in `types`.
216
+ #[derive(magnus::TypedData)]
217
+ #[magnus(class = "JSONSchema::Canonical::MultiTypeView", free_immediately)]
218
+ pub struct MultiTypeView {
219
+ types: Vec<&'static str>,
220
+ }
221
+
222
+ impl DataTypeFunctions for MultiTypeView {}
223
+
224
+ impl MultiTypeView {
225
+ fn types(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
226
+ let array = ruby.ary_new_capa(rb_self.types.len());
227
+ for type_name in &rb_self.types {
228
+ array.push(ruby.sym_new(*type_name).as_value())?;
229
+ }
230
+ Ok(array.as_value())
231
+ }
232
+
233
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
234
+ Ok(format!(
235
+ "#<JSONSchema::Canonical::MultiTypeView types={}>",
236
+ Self::types(ruby, rb_self)?.inspect()
237
+ ))
238
+ }
239
+
240
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
241
+ let hash = ruby.hash_new();
242
+ hash.aset(ruby.sym_new("types"), Self::types(ruby, rb_self)?)?;
243
+ Ok(hash)
244
+ }
245
+ }
246
+
247
+ /// A value matches iff its JSON type is `type_name` *and* it satisfies `body`.
248
+ #[derive(magnus::TypedData)]
249
+ #[magnus(class = "JSONSchema::Canonical::TypedGroupView", free_immediately)]
250
+ pub struct TypedGroupView {
251
+ type_name: &'static str,
252
+ body: CanonicalSchema,
253
+ }
254
+
255
+ impl DataTypeFunctions for TypedGroupView {}
256
+
257
+ impl TypedGroupView {
258
+ fn type_name(ruby: &Ruby, rb_self: &Self) -> Value {
259
+ ruby.sym_new(rb_self.type_name).as_value()
260
+ }
261
+
262
+ fn body(ruby: &Ruby, rb_self: &Self) -> Value {
263
+ ruby.obj_wrap(RbCanonicalSchema {
264
+ inner: rb_self.body.clone(),
265
+ })
266
+ .as_value()
267
+ }
268
+
269
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> String {
270
+ format!(
271
+ "#<JSONSchema::Canonical::TypedGroupView type_name={}>",
272
+ Self::type_name(ruby, rb_self).inspect()
273
+ )
274
+ }
275
+
276
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
277
+ let hash = ruby.hash_new();
278
+ hash.aset(ruby.sym_new("type_name"), Self::type_name(ruby, rb_self))?;
279
+ hash.aset(ruby.sym_new("body"), Self::body(ruby, rb_self))?;
280
+ Ok(hash)
281
+ }
282
+ }
283
+
284
+ /// An absent bound is `nil`; a present one keeps its exact value.
285
+ fn bound_to_ruby(ruby: &Ruby, bound: Option<&serde_json::Number>) -> Result<Value, Error> {
286
+ match bound {
287
+ Some(number) => value_to_ruby(ruby, &serde_json::Value::Number(number.clone())),
288
+ None => Ok(ruby.qnil().as_value()),
289
+ }
290
+ }
291
+
292
+ /// A string value within a length window, matching every pattern.
293
+ #[derive(magnus::TypedData)]
294
+ #[magnus(class = "JSONSchema::Canonical::StringView", free_immediately)]
295
+ pub struct StringView {
296
+ min_length: Option<serde_json::Number>,
297
+ max_length: Option<serde_json::Number>,
298
+ patterns: Vec<String>,
299
+ }
300
+
301
+ impl DataTypeFunctions for StringView {}
302
+
303
+ impl StringView {
304
+ fn min_length(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
305
+ bound_to_ruby(ruby, rb_self.min_length.as_ref())
306
+ }
307
+
308
+ fn max_length(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
309
+ bound_to_ruby(ruby, rb_self.max_length.as_ref())
310
+ }
311
+
312
+ fn patterns(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
313
+ let array = ruby.ary_new_capa(rb_self.patterns.len());
314
+ for pattern in &rb_self.patterns {
315
+ array.push(ruby.str_new(pattern).as_value())?;
316
+ }
317
+ Ok(array.as_value())
318
+ }
319
+
320
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
321
+ Ok(format!(
322
+ "#<JSONSchema::Canonical::StringView min_length={} max_length={} patterns={}>",
323
+ Self::min_length(ruby, rb_self)?.inspect(),
324
+ Self::max_length(ruby, rb_self)?.inspect(),
325
+ Self::patterns(ruby, rb_self)?.inspect()
326
+ ))
327
+ }
328
+
329
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
330
+ let hash = ruby.hash_new();
331
+ hash.aset(ruby.sym_new("min_length"), Self::min_length(ruby, rb_self)?)?;
332
+ hash.aset(ruby.sym_new("max_length"), Self::max_length(ruby, rb_self)?)?;
333
+ hash.aset(ruby.sym_new("patterns"), Self::patterns(ruby, rb_self)?)?;
334
+ Ok(hash)
335
+ }
336
+ }
337
+
338
+ /// An integer value within a closed interval.
339
+ #[derive(magnus::TypedData)]
340
+ #[magnus(class = "JSONSchema::Canonical::IntegerView", free_immediately)]
341
+ pub struct IntegerView {
342
+ minimum: Option<serde_json::Number>,
343
+ maximum: Option<serde_json::Number>,
344
+ }
345
+
346
+ impl DataTypeFunctions for IntegerView {}
347
+
348
+ impl IntegerView {
349
+ fn minimum(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
350
+ bound_to_ruby(ruby, rb_self.minimum.as_ref())
351
+ }
352
+
353
+ fn maximum(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
354
+ bound_to_ruby(ruby, rb_self.maximum.as_ref())
355
+ }
356
+
357
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
358
+ Ok(format!(
359
+ "#<JSONSchema::Canonical::IntegerView minimum={} maximum={}>",
360
+ Self::minimum(ruby, rb_self)?.inspect(),
361
+ Self::maximum(ruby, rb_self)?.inspect()
362
+ ))
363
+ }
364
+
365
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
366
+ let hash = ruby.hash_new();
367
+ hash.aset(ruby.sym_new("minimum"), Self::minimum(ruby, rb_self)?)?;
368
+ hash.aset(ruby.sym_new("maximum"), Self::maximum(ruby, rb_self)?)?;
369
+ Ok(hash)
370
+ }
371
+ }
372
+
373
+ /// A value matches iff at least one branch matches.
374
+ #[derive(magnus::TypedData)]
375
+ #[magnus(class = "JSONSchema::Canonical::AnyOfView", free_immediately)]
376
+ pub struct AnyOfView {
377
+ branches: Vec<CanonicalSchema>,
378
+ }
379
+
380
+ impl DataTypeFunctions for AnyOfView {}
381
+
382
+ impl AnyOfView {
383
+ fn branches(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
384
+ let array = ruby.ary_new_capa(rb_self.branches.len());
385
+ for branch in &rb_self.branches {
386
+ array.push(
387
+ ruby.obj_wrap(RbCanonicalSchema {
388
+ inner: branch.clone(),
389
+ })
390
+ .as_value(),
391
+ )?;
392
+ }
393
+ Ok(array.as_value())
394
+ }
395
+
396
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
397
+ let kinds = ruby.ary_new_capa(rb_self.branches.len());
398
+ for branch in &rb_self.branches {
399
+ kinds.push(ruby.sym_new(branch.kind().as_str()).as_value())?;
400
+ }
401
+ Ok(format!(
402
+ "#<JSONSchema::Canonical::AnyOfView branches={}>",
403
+ kinds.as_value().inspect()
404
+ ))
405
+ }
406
+
407
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
408
+ let hash = ruby.hash_new();
409
+ hash.aset(ruby.sym_new("branches"), Self::branches(ruby, rb_self)?)?;
410
+ Ok(hash)
411
+ }
412
+ }
413
+
414
+ /// Exactly one admitted value.
415
+ #[derive(magnus::TypedData)]
416
+ #[magnus(class = "JSONSchema::Canonical::ConstView", free_immediately)]
417
+ pub struct ConstView {
418
+ value: serde_json::Value,
419
+ }
420
+
421
+ impl DataTypeFunctions for ConstView {}
422
+
423
+ impl ConstView {
424
+ fn value(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
425
+ value_to_ruby(ruby, &rb_self.value)
426
+ }
427
+
428
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
429
+ Ok(format!(
430
+ "#<JSONSchema::Canonical::ConstView value={}>",
431
+ Self::value(ruby, rb_self)?.inspect()
432
+ ))
433
+ }
434
+
435
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
436
+ let hash = ruby.hash_new();
437
+ hash.aset(ruby.sym_new("value"), Self::value(ruby, rb_self)?)?;
438
+ Ok(hash)
439
+ }
440
+ }
441
+
442
+ /// A sorted, deduplicated finite set of admitted values.
443
+ #[derive(magnus::TypedData)]
444
+ #[magnus(class = "JSONSchema::Canonical::EnumView", free_immediately)]
445
+ pub struct EnumView {
446
+ values: Vec<serde_json::Value>,
447
+ }
448
+
449
+ impl DataTypeFunctions for EnumView {}
450
+
451
+ impl EnumView {
452
+ fn values(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
453
+ let array = ruby.ary_new_capa(rb_self.values.len());
454
+ for value in &rb_self.values {
455
+ array.push(value_to_ruby(ruby, value)?)?;
456
+ }
457
+ Ok(array.as_value())
458
+ }
459
+
460
+ fn inspect(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
461
+ Ok(format!(
462
+ "#<JSONSchema::Canonical::EnumView values={}>",
463
+ Self::values(ruby, rb_self)?.inspect()
464
+ ))
465
+ }
466
+
467
+ fn deconstruct_keys(ruby: &Ruby, rb_self: &Self, _keys: Value) -> Result<RHash, Error> {
468
+ let hash = ruby.hash_new();
469
+ hash.aset(ruby.sym_new("values"), Self::values(ruby, rb_self)?)?;
470
+ Ok(hash)
471
+ }
472
+ }
473
+
474
+ fn canonicalize(ruby: &Ruby, args: &[Value]) -> Result<Value, Error> {
475
+ let parsed = scan_args::<(Value,), (), (), (), _, ()>(args)?;
476
+ let (schema_arg,) = parsed.required;
477
+ let keywords: RHash = parsed.keywords;
478
+ let base_kwargs: magnus::scan_args::KwArgs<(), (Option<Value>, Option<bool>), ()> = get_kwargs(
479
+ keywords,
480
+ &[],
481
+ &[*CANONICAL_KW_DRAFT, *CANONICAL_KW_VALIDATE_FORMATS],
482
+ )?;
483
+ let (draft_val, validate_formats) = base_kwargs.optional;
484
+
485
+ let schema_value = to_schema_value(ruby, schema_arg)?;
486
+ let mut options = jsonschema::canonical::options();
487
+ if let Some(draft) = draft_val {
488
+ options = options.with_draft(parse_draft_symbol(ruby, draft)?);
489
+ }
490
+ if let Some(validate_formats) = validate_formats {
491
+ options = options.should_validate_formats(validate_formats);
492
+ }
493
+ options
494
+ .canonicalize(&schema_value)
495
+ .map(|inner| ruby.obj_wrap(RbCanonicalSchema { inner }).as_value())
496
+ .map_err(|error| canonicalization_error(ruby, error))
497
+ }
498
+
499
+ pub(crate) fn init_canonical(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
500
+ let canonical_module = module.define_module("Canonical")?;
501
+
502
+ let base_error =
503
+ canonical_module.define_error("CanonicalizationError", ruby.exception_standard_error())?;
504
+ canonical_module.define_error("InvalidSchemaType", base_error)?;
505
+
506
+ let canonical_schema = canonical_module.define_class("CanonicalSchema", ruby.class_object())?;
507
+ canonical_schema.define_method(
508
+ "to_json_schema",
509
+ method!(RbCanonicalSchema::to_json_schema, 0),
510
+ )?;
511
+ canonical_schema.define_method("draft", method!(RbCanonicalSchema::draft, 0))?;
512
+ canonical_schema.define_method("kind", method!(RbCanonicalSchema::kind, 0))?;
513
+ canonical_schema.define_method("inspect", method!(RbCanonicalSchema::inspect, 0))?;
514
+ canonical_schema.define_method("==", method!(RbCanonicalSchema::eq, 1))?;
515
+ canonical_schema.define_method(
516
+ "eql?",
517
+ method!(<RbCanonicalSchema as typed_data::IsEql>::is_eql, 1),
518
+ )?;
519
+ canonical_schema.define_method(
520
+ "hash",
521
+ method!(<RbCanonicalSchema as typed_data::Hash>::hash, 0),
522
+ )?;
523
+ canonical_schema.define_method("view", method!(RbCanonicalSchema::view, 0))?;
524
+ canonical_schema.define_method("definitions", method!(RbCanonicalSchema::definitions, 0))?;
525
+ canonical_schema.define_method("satisfiable?", method!(RbCanonicalSchema::satisfiable, 0))?;
526
+
527
+ let true_view = canonical_module.define_class("TrueView", ruby.class_object())?;
528
+ true_view.define_method("inspect", method!(TrueView::inspect, 0))?;
529
+ let false_view = canonical_module.define_class("FalseView", ruby.class_object())?;
530
+ false_view.define_method("inspect", method!(FalseView::inspect, 0))?;
531
+
532
+ let multi_type_view = canonical_module.define_class("MultiTypeView", ruby.class_object())?;
533
+ multi_type_view.define_method("types", method!(MultiTypeView::types, 0))?;
534
+ multi_type_view.define_method("inspect", method!(MultiTypeView::inspect, 0))?;
535
+ multi_type_view.define_method(
536
+ "deconstruct_keys",
537
+ method!(MultiTypeView::deconstruct_keys, 1),
538
+ )?;
539
+
540
+ let typed_group_view = canonical_module.define_class("TypedGroupView", ruby.class_object())?;
541
+ typed_group_view.define_method("type_name", method!(TypedGroupView::type_name, 0))?;
542
+ typed_group_view.define_method("body", method!(TypedGroupView::body, 0))?;
543
+ typed_group_view.define_method("inspect", method!(TypedGroupView::inspect, 0))?;
544
+ typed_group_view.define_method(
545
+ "deconstruct_keys",
546
+ method!(TypedGroupView::deconstruct_keys, 1),
547
+ )?;
548
+
549
+ let const_view = canonical_module.define_class("ConstView", ruby.class_object())?;
550
+ const_view.define_method("value", method!(ConstView::value, 0))?;
551
+ const_view.define_method("inspect", method!(ConstView::inspect, 0))?;
552
+ const_view.define_method("deconstruct_keys", method!(ConstView::deconstruct_keys, 1))?;
553
+
554
+ let enum_view = canonical_module.define_class("EnumView", ruby.class_object())?;
555
+ enum_view.define_method("values", method!(EnumView::values, 0))?;
556
+ enum_view.define_method("inspect", method!(EnumView::inspect, 0))?;
557
+ enum_view.define_method("deconstruct_keys", method!(EnumView::deconstruct_keys, 1))?;
558
+
559
+ let string_view = canonical_module.define_class("StringView", ruby.class_object())?;
560
+ string_view.define_method("min_length", method!(StringView::min_length, 0))?;
561
+ string_view.define_method("max_length", method!(StringView::max_length, 0))?;
562
+ string_view.define_method("patterns", method!(StringView::patterns, 0))?;
563
+ string_view.define_method("inspect", method!(StringView::inspect, 0))?;
564
+ string_view.define_method("deconstruct_keys", method!(StringView::deconstruct_keys, 1))?;
565
+
566
+ let integer_view = canonical_module.define_class("IntegerView", ruby.class_object())?;
567
+ integer_view.define_method("minimum", method!(IntegerView::minimum, 0))?;
568
+ integer_view.define_method("maximum", method!(IntegerView::maximum, 0))?;
569
+ integer_view.define_method("inspect", method!(IntegerView::inspect, 0))?;
570
+ integer_view.define_method(
571
+ "deconstruct_keys",
572
+ method!(IntegerView::deconstruct_keys, 1),
573
+ )?;
574
+
575
+ let any_of_view = canonical_module.define_class("AnyOfView", ruby.class_object())?;
576
+ any_of_view.define_method("branches", method!(AnyOfView::branches, 0))?;
577
+ any_of_view.define_method("inspect", method!(AnyOfView::inspect, 0))?;
578
+ any_of_view.define_method("deconstruct_keys", method!(AnyOfView::deconstruct_keys, 1))?;
579
+
580
+ let raw_view = canonical_module.define_class("RawView", ruby.class_object())?;
581
+ raw_view.define_method("schema", method!(RawView::schema, 0))?;
582
+ raw_view.define_method("inspect", method!(RawView::inspect, 0))?;
583
+ raw_view.define_method("deconstruct_keys", method!(RawView::deconstruct_keys, 1))?;
584
+
585
+ let json_module = canonical_module.define_module("JSON")?;
586
+ json_module
587
+ .define_singleton_method("to_string", function!(crate::canonical_json_to_string, 1))?;
588
+
589
+ module.define_singleton_method("canonicalize", function!(canonicalize, -1))?;
590
+
591
+ Ok(())
592
+ }
data/src/lib.rs CHANGED
@@ -5,6 +5,7 @@
5
5
  #![allow(clippy::unused_self)]
6
6
  #![allow(clippy::struct_field_names)]
7
7
 
8
+ mod canonical;
8
9
  mod error_kind;
9
10
  mod evaluation;
10
11
  mod options;
@@ -1513,10 +1514,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1513
1514
  module.define_singleton_method("each_error", function!(each_error, -1))?;
1514
1515
  module.define_singleton_method("evaluate", function!(evaluate, -1))?;
1515
1516
 
1516
- let canonical_module = module.define_module("Canonical")?;
1517
- let canonical_json_module = canonical_module.define_module("JSON")?;
1518
- canonical_json_module
1519
- .define_singleton_method("to_string", function!(canonical_json_to_string, 1))?;
1517
+ canonical::init_canonical(ruby, &module)?;
1520
1518
 
1521
1519
  // Validator class
1522
1520
  let validator_class = module.define_class("Validator", ruby.class_object())?;
data/src/ser.rs CHANGED
@@ -1,4 +1,5 @@
1
1
  //! Serialization between Ruby values and `serde_json::Value`.
2
+ use jsonschema::canonical::json::{canonical_number, BorrowedNumber};
2
3
  use magnus::{
3
4
  error::ErrorType,
4
5
  exception::ExceptionClass,
@@ -435,7 +436,12 @@ impl Serialize for CanonicalRubyValue<'_> {
435
436
  number.serialize(serializer)
436
437
  }
437
438
  ruby_value_type::RUBY_T_FLOAT => {
438
- let number = convert_float_for_canonical(self.ruby, self.value)
439
+ let float = f64::try_convert(self.value)
440
+ .map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
441
+ if float.fract() != 0.0 && float.is_finite() {
442
+ return serialize_fractional_float(float, serializer);
443
+ }
444
+ let number = convert_float_for_canonical(self.ruby, float)
439
445
  .map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
440
446
  number.serialize(serializer)
441
447
  }
@@ -547,9 +553,18 @@ impl Serialize for CanonicalRubyValue<'_> {
547
553
  }
548
554
  }
549
555
 
556
+ fn serialize_fractional_float<S>(float: f64, serializer: S) -> Result<S::Ok, S::Error>
557
+ where
558
+ S: Serializer,
559
+ {
560
+ let mut buffer = zmij::Buffer::new();
561
+ let text = buffer.format_finite(float);
562
+ let canonical = canonical_number(text);
563
+ BorrowedNumber(canonical.as_deref().unwrap_or(text)).serialize(serializer)
564
+ }
565
+
550
566
  #[inline]
551
- fn convert_float_for_canonical(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
552
- let float = f64::try_convert(value)?;
567
+ fn convert_float_for_canonical(ruby: &Ruby, float: f64) -> Result<JsonValue, Error> {
553
568
  if !float.is_finite() {
554
569
  return Ok(JsonValue::Null);
555
570
  }
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.48.1
4
+ version: 0.48.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dygalo
@@ -63,6 +63,7 @@ files:
63
63
  - lib/jsonschema/version.rb
64
64
  - lib/jsonschema_rs.rb
65
65
  - sig/jsonschema.rbs
66
+ - src/canonical.rs
66
67
  - src/error_kind.rs
67
68
  - src/evaluation.rs
68
69
  - src/lib.rs