jsonschema_rs 0.43.0 → 0.44.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 +4 -4
- data/CHANGELOG.md +12 -1
- data/Cargo.toml +2 -2
- data/README.md +16 -0
- data/ext/jsonschema/Cargo.lock +51 -52
- data/ext/jsonschema/Cargo.toml +3 -3
- data/lib/jsonschema/version.rb +1 -1
- data/sig/jsonschema.rbs +9 -0
- data/src/lib.rs +15 -1
- data/src/ser.rs +599 -96
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51236b02371703841ea00783cfc0a9d5b97281a27a527c390795ab02f5e88aac
|
|
4
|
+
data.tar.gz: 87fb69c295b4a82d16d20fab2368aa9eb7b90167f2135b411ee8b0279db0399d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abc58a0cf0669380c5c3035a76586e45d952584cf5742878a451a10dab18ebfb5a7210bf80f022da8a3b302ead4364337554544537f7432e46161343b8cf7888
|
|
7
|
+
data.tar.gz: 991fdcf4e5661180a09918c7bce74c3afd9adf73543b8f1ba4d0f2c046e2feccb4cb9bb757e3f315c126665d67455eaecd938d24338b8dd18c0f30b4b897ca59
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.44.0] - 2026-03-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Canonical::JSON.to_string(object)` for canonical JSON serialization (stable key ordering and numeric normalization), useful for deduplicating equivalent JSON Schemas.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Do not produce annotations for non-string instances from `contentMediaType`, `contentEncoding`, and `contentSchema` keywords.
|
|
14
|
+
|
|
5
15
|
## [0.43.0] - 2026-02-28
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -43,7 +53,8 @@
|
|
|
43
53
|
|
|
44
54
|
- Initial public release
|
|
45
55
|
|
|
46
|
-
[Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.
|
|
56
|
+
[Unreleased]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.44.0...HEAD
|
|
57
|
+
[0.44.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.43.0...ruby-v0.44.0
|
|
47
58
|
[0.43.0]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.42.2...ruby-v0.43.0
|
|
48
59
|
[0.42.2]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.42.1...ruby-v0.42.2
|
|
49
60
|
[0.42.1]: https://github.com/Stranger6667/jsonschema/compare/ruby-v0.42.0...ruby-v0.42.1
|
data/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsonschema-rb"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.44.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -13,7 +13,7 @@ publish = false
|
|
|
13
13
|
crate-type = ["cdylib"]
|
|
14
14
|
|
|
15
15
|
[dependencies]
|
|
16
|
-
jsonschema = { version = "0.
|
|
16
|
+
jsonschema = { version = "0.44.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
|
|
17
17
|
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
18
18
|
rb-sys = "0.9"
|
|
19
19
|
serde = { workspace = true }
|
data/README.md
CHANGED
|
@@ -251,6 +251,22 @@ valid_eval.annotations
|
|
|
251
251
|
# instanceLocation: "", annotations: ["age", "name"]}]
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
+
### Canonical JSON serialization
|
|
255
|
+
|
|
256
|
+
Use `Canonical::JSON.to_string` when you need a stable JSON representation:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
schema_a = { "type" => "object", "properties" => { "b" => { "type" => "integer" }, "a" => { "type" => "string" } } }
|
|
260
|
+
schema_b = { "properties" => { "a" => { "type" => "string" }, "b" => { "type" => "integer" } }, "type" => "object" }
|
|
261
|
+
|
|
262
|
+
dump_a = JSONSchema::Canonical::JSON.to_string(schema_a)
|
|
263
|
+
dump_b = JSONSchema::Canonical::JSON.to_string(schema_b)
|
|
264
|
+
|
|
265
|
+
dump_a == dump_b # => true
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Main use case: deduplicating equivalent JSON Schemas.
|
|
269
|
+
|
|
254
270
|
## Meta-Schema Validation
|
|
255
271
|
|
|
256
272
|
Validate that a JSON Schema document is itself valid:
|
data/ext/jsonschema/Cargo.lock
CHANGED
|
@@ -98,9 +98,9 @@ checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
|
|
|
98
98
|
|
|
99
99
|
[[package]]
|
|
100
100
|
name = "bumpalo"
|
|
101
|
-
version = "3.
|
|
101
|
+
version = "3.20.2"
|
|
102
102
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
103
|
-
checksum = "
|
|
103
|
+
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
|
104
104
|
|
|
105
105
|
[[package]]
|
|
106
106
|
name = "bytecount"
|
|
@@ -281,9 +281,9 @@ dependencies = [
|
|
|
281
281
|
|
|
282
282
|
[[package]]
|
|
283
283
|
name = "futures-channel"
|
|
284
|
-
version = "0.3.
|
|
284
|
+
version = "0.3.32"
|
|
285
285
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
286
|
-
checksum = "
|
|
286
|
+
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
|
287
287
|
dependencies = [
|
|
288
288
|
"futures-core",
|
|
289
289
|
"futures-sink",
|
|
@@ -291,33 +291,33 @@ dependencies = [
|
|
|
291
291
|
|
|
292
292
|
[[package]]
|
|
293
293
|
name = "futures-core"
|
|
294
|
-
version = "0.3.
|
|
294
|
+
version = "0.3.32"
|
|
295
295
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
296
|
-
checksum = "
|
|
296
|
+
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
|
297
297
|
|
|
298
298
|
[[package]]
|
|
299
299
|
name = "futures-io"
|
|
300
|
-
version = "0.3.
|
|
300
|
+
version = "0.3.32"
|
|
301
301
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
302
|
-
checksum = "
|
|
302
|
+
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
|
303
303
|
|
|
304
304
|
[[package]]
|
|
305
305
|
name = "futures-sink"
|
|
306
|
-
version = "0.3.
|
|
306
|
+
version = "0.3.32"
|
|
307
307
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
308
|
-
checksum = "
|
|
308
|
+
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
|
309
309
|
|
|
310
310
|
[[package]]
|
|
311
311
|
name = "futures-task"
|
|
312
|
-
version = "0.3.
|
|
312
|
+
version = "0.3.32"
|
|
313
313
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
314
|
-
checksum = "
|
|
314
|
+
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
|
315
315
|
|
|
316
316
|
[[package]]
|
|
317
317
|
name = "futures-util"
|
|
318
|
-
version = "0.3.
|
|
318
|
+
version = "0.3.32"
|
|
319
319
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
320
|
-
checksum = "
|
|
320
|
+
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
|
321
321
|
dependencies = [
|
|
322
322
|
"futures-core",
|
|
323
323
|
"futures-io",
|
|
@@ -325,7 +325,6 @@ dependencies = [
|
|
|
325
325
|
"futures-task",
|
|
326
326
|
"memchr",
|
|
327
327
|
"pin-project-lite",
|
|
328
|
-
"pin-utils",
|
|
329
328
|
"slab",
|
|
330
329
|
]
|
|
331
330
|
|
|
@@ -657,9 +656,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|
|
657
656
|
|
|
658
657
|
[[package]]
|
|
659
658
|
name = "js-sys"
|
|
660
|
-
version = "0.3.
|
|
659
|
+
version = "0.3.91"
|
|
661
660
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
662
|
-
checksum = "
|
|
661
|
+
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
|
663
662
|
dependencies = [
|
|
664
663
|
"once_cell",
|
|
665
664
|
"wasm-bindgen",
|
|
@@ -667,9 +666,9 @@ dependencies = [
|
|
|
667
666
|
|
|
668
667
|
[[package]]
|
|
669
668
|
name = "jsonschema"
|
|
670
|
-
version = "0.
|
|
669
|
+
version = "0.44.0"
|
|
671
670
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
672
|
-
checksum = "
|
|
671
|
+
checksum = "267fb27be492e66ab147d2ce0233d88ae465b93c3565016e73998729bf3fe60f"
|
|
673
672
|
dependencies = [
|
|
674
673
|
"ahash",
|
|
675
674
|
"bytecount",
|
|
@@ -697,7 +696,7 @@ dependencies = [
|
|
|
697
696
|
|
|
698
697
|
[[package]]
|
|
699
698
|
name = "jsonschema-rb-ext"
|
|
700
|
-
version = "0.
|
|
699
|
+
version = "0.44.0"
|
|
701
700
|
dependencies = [
|
|
702
701
|
"jsonschema",
|
|
703
702
|
"magnus",
|
|
@@ -940,9 +939,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|
|
940
939
|
|
|
941
940
|
[[package]]
|
|
942
941
|
name = "pin-project-lite"
|
|
943
|
-
version = "0.2.
|
|
942
|
+
version = "0.2.17"
|
|
944
943
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
945
|
-
checksum = "
|
|
944
|
+
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|
946
945
|
|
|
947
946
|
[[package]]
|
|
948
947
|
name = "pin-utils"
|
|
@@ -1044,9 +1043,9 @@ dependencies = [
|
|
|
1044
1043
|
|
|
1045
1044
|
[[package]]
|
|
1046
1045
|
name = "referencing"
|
|
1047
|
-
version = "0.
|
|
1046
|
+
version = "0.44.0"
|
|
1048
1047
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1049
|
-
checksum = "
|
|
1048
|
+
checksum = "12ecd0f3daefd4faff2e0821310c18e6e9d1fd00550bbd7e5a59d78184a071bc"
|
|
1050
1049
|
dependencies = [
|
|
1051
1050
|
"ahash",
|
|
1052
1051
|
"fluent-uri",
|
|
@@ -1082,9 +1081,9 @@ dependencies = [
|
|
|
1082
1081
|
|
|
1083
1082
|
[[package]]
|
|
1084
1083
|
name = "regex-syntax"
|
|
1085
|
-
version = "0.8.
|
|
1084
|
+
version = "0.8.10"
|
|
1086
1085
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1087
|
-
checksum = "
|
|
1086
|
+
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|
1088
1087
|
|
|
1089
1088
|
[[package]]
|
|
1090
1089
|
name = "reqwest"
|
|
@@ -1147,9 +1146,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
|
1147
1146
|
|
|
1148
1147
|
[[package]]
|
|
1149
1148
|
name = "rustls"
|
|
1150
|
-
version = "0.23.
|
|
1149
|
+
version = "0.23.37"
|
|
1151
1150
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1152
|
-
checksum = "
|
|
1151
|
+
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
|
1153
1152
|
dependencies = [
|
|
1154
1153
|
"once_cell",
|
|
1155
1154
|
"ring",
|
|
@@ -1250,9 +1249,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
1250
1249
|
|
|
1251
1250
|
[[package]]
|
|
1252
1251
|
name = "security-framework"
|
|
1253
|
-
version = "3.
|
|
1252
|
+
version = "3.7.0"
|
|
1254
1253
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1255
|
-
checksum = "
|
|
1254
|
+
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
|
1256
1255
|
dependencies = [
|
|
1257
1256
|
"bitflags",
|
|
1258
1257
|
"core-foundation",
|
|
@@ -1263,9 +1262,9 @@ dependencies = [
|
|
|
1263
1262
|
|
|
1264
1263
|
[[package]]
|
|
1265
1264
|
name = "security-framework-sys"
|
|
1266
|
-
version = "2.
|
|
1265
|
+
version = "2.17.0"
|
|
1267
1266
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1268
|
-
checksum = "
|
|
1267
|
+
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
|
1269
1268
|
dependencies = [
|
|
1270
1269
|
"core-foundation-sys",
|
|
1271
1270
|
"libc",
|
|
@@ -1368,9 +1367,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|
|
1368
1367
|
|
|
1369
1368
|
[[package]]
|
|
1370
1369
|
name = "syn"
|
|
1371
|
-
version = "2.0.
|
|
1370
|
+
version = "2.0.117"
|
|
1372
1371
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1373
|
-
checksum = "
|
|
1372
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
1374
1373
|
dependencies = [
|
|
1375
1374
|
"proc-macro2",
|
|
1376
1375
|
"quote",
|
|
@@ -1542,9 +1541,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
|
|
1542
1541
|
|
|
1543
1542
|
[[package]]
|
|
1544
1543
|
name = "unicode-ident"
|
|
1545
|
-
version = "1.0.
|
|
1544
|
+
version = "1.0.24"
|
|
1546
1545
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1547
|
-
checksum = "
|
|
1546
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
1548
1547
|
|
|
1549
1548
|
[[package]]
|
|
1550
1549
|
name = "untrusted"
|
|
@@ -1628,9 +1627,9 @@ dependencies = [
|
|
|
1628
1627
|
|
|
1629
1628
|
[[package]]
|
|
1630
1629
|
name = "wasm-bindgen"
|
|
1631
|
-
version = "0.2.
|
|
1630
|
+
version = "0.2.114"
|
|
1632
1631
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1633
|
-
checksum = "
|
|
1632
|
+
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
|
1634
1633
|
dependencies = [
|
|
1635
1634
|
"cfg-if",
|
|
1636
1635
|
"once_cell",
|
|
@@ -1641,9 +1640,9 @@ dependencies = [
|
|
|
1641
1640
|
|
|
1642
1641
|
[[package]]
|
|
1643
1642
|
name = "wasm-bindgen-futures"
|
|
1644
|
-
version = "0.4.
|
|
1643
|
+
version = "0.4.64"
|
|
1645
1644
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1646
|
-
checksum = "
|
|
1645
|
+
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
|
|
1647
1646
|
dependencies = [
|
|
1648
1647
|
"cfg-if",
|
|
1649
1648
|
"futures-util",
|
|
@@ -1655,9 +1654,9 @@ dependencies = [
|
|
|
1655
1654
|
|
|
1656
1655
|
[[package]]
|
|
1657
1656
|
name = "wasm-bindgen-macro"
|
|
1658
|
-
version = "0.2.
|
|
1657
|
+
version = "0.2.114"
|
|
1659
1658
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1660
|
-
checksum = "
|
|
1659
|
+
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
|
1661
1660
|
dependencies = [
|
|
1662
1661
|
"quote",
|
|
1663
1662
|
"wasm-bindgen-macro-support",
|
|
@@ -1665,9 +1664,9 @@ dependencies = [
|
|
|
1665
1664
|
|
|
1666
1665
|
[[package]]
|
|
1667
1666
|
name = "wasm-bindgen-macro-support"
|
|
1668
|
-
version = "0.2.
|
|
1667
|
+
version = "0.2.114"
|
|
1669
1668
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1670
|
-
checksum = "
|
|
1669
|
+
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
|
1671
1670
|
dependencies = [
|
|
1672
1671
|
"bumpalo",
|
|
1673
1672
|
"proc-macro2",
|
|
@@ -1678,18 +1677,18 @@ dependencies = [
|
|
|
1678
1677
|
|
|
1679
1678
|
[[package]]
|
|
1680
1679
|
name = "wasm-bindgen-shared"
|
|
1681
|
-
version = "0.2.
|
|
1680
|
+
version = "0.2.114"
|
|
1682
1681
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1683
|
-
checksum = "
|
|
1682
|
+
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
|
1684
1683
|
dependencies = [
|
|
1685
1684
|
"unicode-ident",
|
|
1686
1685
|
]
|
|
1687
1686
|
|
|
1688
1687
|
[[package]]
|
|
1689
1688
|
name = "web-sys"
|
|
1690
|
-
version = "0.3.
|
|
1689
|
+
version = "0.3.91"
|
|
1691
1690
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1692
|
-
checksum = "
|
|
1691
|
+
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
|
1693
1692
|
dependencies = [
|
|
1694
1693
|
"js-sys",
|
|
1695
1694
|
"wasm-bindgen",
|
|
@@ -1978,18 +1977,18 @@ dependencies = [
|
|
|
1978
1977
|
|
|
1979
1978
|
[[package]]
|
|
1980
1979
|
name = "zerocopy"
|
|
1981
|
-
version = "0.8.
|
|
1980
|
+
version = "0.8.40"
|
|
1982
1981
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1983
|
-
checksum = "
|
|
1982
|
+
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
|
|
1984
1983
|
dependencies = [
|
|
1985
1984
|
"zerocopy-derive",
|
|
1986
1985
|
]
|
|
1987
1986
|
|
|
1988
1987
|
[[package]]
|
|
1989
1988
|
name = "zerocopy-derive"
|
|
1990
|
-
version = "0.8.
|
|
1989
|
+
version = "0.8.40"
|
|
1991
1990
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1992
|
-
checksum = "
|
|
1991
|
+
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
|
|
1993
1992
|
dependencies = [
|
|
1994
1993
|
"proc-macro2",
|
|
1995
1994
|
"quote",
|
data/ext/jsonschema/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsonschema-rb-ext"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.44.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.
|
|
13
|
+
jsonschema = { version = "0.44.0", default-features = false, features = ["arbitrary-precision", "resolve-http", "resolve-file", "tls-ring"] }
|
|
14
14
|
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
15
15
|
rb-sys = "0.9"
|
|
16
|
-
referencing = "0.
|
|
16
|
+
referencing = "0.44.0"
|
|
17
17
|
serde = { version = "1", features = ["derive"] }
|
|
18
18
|
serde_json = { version = "1", features = ["arbitrary_precision"] }
|
|
19
19
|
|
data/lib/jsonschema/version.rb
CHANGED
data/sig/jsonschema.rbs
CHANGED
|
@@ -7,6 +7,15 @@ module JSONSchema
|
|
|
7
7
|
# Valid draft version symbols
|
|
8
8
|
type draft = :draft4 | :draft6 | :draft7 | :draft201909 | :draft202012
|
|
9
9
|
|
|
10
|
+
module Canonical
|
|
11
|
+
module JSON
|
|
12
|
+
# Serialize a Ruby value to canonical JSON.
|
|
13
|
+
#
|
|
14
|
+
# Main use case: deduplicate equivalent JSON Schemas by using a stable string form.
|
|
15
|
+
def self.to_string: (untyped object) -> String
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
# Create a validator with auto-detected draft version.
|
|
11
20
|
#
|
|
12
21
|
# @param schema The JSON Schema (Hash or JSON string)
|
data/src/lib.rs
CHANGED
|
@@ -42,7 +42,7 @@ use crate::{
|
|
|
42
42
|
},
|
|
43
43
|
registry::Registry,
|
|
44
44
|
retriever::{retriever_error_message, RubyRetriever},
|
|
45
|
-
ser::{to_schema_value, to_value},
|
|
45
|
+
ser::{to_canonical_string, to_schema_value, to_value},
|
|
46
46
|
static_id::define_rb_intern,
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -838,6 +838,15 @@ fn validator_for(ruby: &Ruby, args: &[Value]) -> Result<Validator, Error> {
|
|
|
838
838
|
})
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
+
/// to_string(object) -> String
|
|
842
|
+
///
|
|
843
|
+
/// Serialize a Ruby value to canonical JSON.
|
|
844
|
+
///
|
|
845
|
+
/// Main use case: deduplicating equivalent JSON Schemas using a stable string form.
|
|
846
|
+
fn canonical_json_to_string(ruby: &Ruby, object: Value) -> Result<String, Error> {
|
|
847
|
+
to_canonical_string(ruby, object)
|
|
848
|
+
}
|
|
849
|
+
|
|
841
850
|
#[allow(unsafe_code)]
|
|
842
851
|
fn is_valid(ruby: &Ruby, args: &[Value]) -> Result<bool, Error> {
|
|
843
852
|
let parsed_args = scan_args::<(Value, Value), (), (), (), _, ()>(args)?;
|
|
@@ -1327,6 +1336,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1327
1336
|
module.define_singleton_method("each_error", function!(each_error, -1))?;
|
|
1328
1337
|
module.define_singleton_method("evaluate", function!(evaluate, -1))?;
|
|
1329
1338
|
|
|
1339
|
+
let canonical_module = module.define_module("Canonical")?;
|
|
1340
|
+
let canonical_json_module = canonical_module.define_module("JSON")?;
|
|
1341
|
+
canonical_json_module
|
|
1342
|
+
.define_singleton_method("to_string", function!(canonical_json_to_string, 1))?;
|
|
1343
|
+
|
|
1330
1344
|
// Validator class
|
|
1331
1345
|
let validator_class = module.define_class("Validator", ruby.class_object())?;
|
|
1332
1346
|
validator_class.define_method("valid?", method!(Validator::is_valid, 1))?;
|
data/src/ser.rs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//! Serialization between Ruby values and `serde_json::Value`.
|
|
2
2
|
use magnus::{
|
|
3
|
+
error::ErrorType,
|
|
4
|
+
exception::ExceptionClass,
|
|
3
5
|
gc::register_mark_object,
|
|
4
6
|
prelude::*,
|
|
5
7
|
rb_sys::AsRawValue,
|
|
@@ -7,8 +9,15 @@ use magnus::{
|
|
|
7
9
|
Error, Integer, RArray, RClass, RHash, RString, Ruby, Symbol, TryConvert, Value,
|
|
8
10
|
};
|
|
9
11
|
use rb_sys::{ruby_value_type, RB_TYPE};
|
|
12
|
+
use serde::{
|
|
13
|
+
ser::{SerializeMap, SerializeSeq},
|
|
14
|
+
Serialize, Serializer,
|
|
15
|
+
};
|
|
10
16
|
use serde_json::{Map, Number, Value as JsonValue};
|
|
11
|
-
use std::
|
|
17
|
+
use std::{
|
|
18
|
+
cell::RefCell,
|
|
19
|
+
fmt::{self, Write},
|
|
20
|
+
};
|
|
12
21
|
|
|
13
22
|
static BIG_DECIMAL_CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
14
23
|
// Ensure bigdecimal is loaded
|
|
@@ -23,6 +32,15 @@ static BIG_DECIMAL_CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
|
23
32
|
});
|
|
24
33
|
|
|
25
34
|
const RECURSION_LIMIT: u16 = 255;
|
|
35
|
+
const I64_LOWER_INCLUSIVE_F64: f64 = -9_223_372_036_854_775_808.0;
|
|
36
|
+
const I64_UPPER_EXCLUSIVE_F64: f64 = 9_223_372_036_854_775_808.0;
|
|
37
|
+
const U64_UPPER_EXCLUSIVE_F64: f64 = 18_446_744_073_709_551_616.0;
|
|
38
|
+
const IEEE754_F64_FRAC_BITS: u32 = 52;
|
|
39
|
+
const IEEE754_F64_EXP_BIAS: i32 = 1023;
|
|
40
|
+
const DECIMAL_BASE_U64: u64 = 1_000_000_000;
|
|
41
|
+
const DECIMAL_CHUNK_WIDTH: usize = 9;
|
|
42
|
+
const CANONICAL_ERROR_PREFIX: &str = "__jsonschema_rb_canonical_error__";
|
|
43
|
+
const DUPLICATE_CANONICAL_KEY_MESSAGE: &str = "Hash contains duplicate keys after normalization";
|
|
26
44
|
|
|
27
45
|
#[inline]
|
|
28
46
|
pub fn to_value(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
@@ -52,6 +70,25 @@ pub fn to_schema_value(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
|
52
70
|
to_value_typed(ruby, value, value_type, 0)
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
/// Serialize a Ruby value to canonical JSON.
|
|
74
|
+
///
|
|
75
|
+
/// Used to generate a stable string form for deduplicating equivalent schemas.
|
|
76
|
+
#[inline]
|
|
77
|
+
pub fn to_canonical_string(ruby: &Ruby, value: Value) -> Result<String, Error> {
|
|
78
|
+
let mut output = Vec::new();
|
|
79
|
+
let mut serializer = serde_json::Serializer::new(&mut output);
|
|
80
|
+
let scratch_pool = RefCell::new(Vec::new());
|
|
81
|
+
CanonicalRubyValue::new(ruby, value, 0, &scratch_pool)
|
|
82
|
+
.serialize(&mut serializer)
|
|
83
|
+
.map_err(|error| canonical_serde_error_to_ruby(ruby, &error))?;
|
|
84
|
+
String::from_utf8(output).map_err(|_| {
|
|
85
|
+
Error::new(
|
|
86
|
+
ruby.exception_runtime_error(),
|
|
87
|
+
"Internal UTF-8 serialization error",
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
fn to_value_recursive(ruby: &Ruby, value: Value, depth: u16) -> Result<JsonValue, Error> {
|
|
56
93
|
if value.is_nil() {
|
|
57
94
|
return Ok(JsonValue::Null);
|
|
@@ -76,96 +113,576 @@ fn to_value_typed(
|
|
|
76
113
|
ruby_value_type::RUBY_T_FIXNUM | ruby_value_type::RUBY_T_BIGNUM => {
|
|
77
114
|
convert_integer(ruby, value)
|
|
78
115
|
}
|
|
79
|
-
ruby_value_type::RUBY_T_FLOAT =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
})
|
|
116
|
+
ruby_value_type::RUBY_T_FLOAT => convert_float(ruby, value),
|
|
117
|
+
ruby_value_type::RUBY_T_STRING => convert_string(ruby, value),
|
|
118
|
+
ruby_value_type::RUBY_T_SYMBOL => convert_symbol(value),
|
|
119
|
+
ruby_value_type::RUBY_T_ARRAY => convert_array(ruby, value, depth),
|
|
120
|
+
ruby_value_type::RUBY_T_HASH => convert_hash(ruby, value, depth),
|
|
121
|
+
ruby_value_type::RUBY_T_DATA if value.is_kind_of(ruby.get_inner(&BIG_DECIMAL_CLASS)) => {
|
|
122
|
+
convert_big_decimal(ruby, value)
|
|
87
123
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
_ => unsupported_type_error(ruby, value),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[inline]
|
|
129
|
+
fn convert_float(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
130
|
+
let float = f64::try_convert(value)?;
|
|
131
|
+
Number::from_f64(float)
|
|
132
|
+
.map(JsonValue::Number)
|
|
133
|
+
.ok_or_else(|| {
|
|
134
|
+
Error::new(
|
|
135
|
+
ruby.exception_arg_error(),
|
|
136
|
+
"Cannot convert NaN or Infinity to JSON",
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[inline]
|
|
142
|
+
fn convert_string(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
143
|
+
utf8_string_from_value(ruby, value, "String is not valid UTF-8").map(JsonValue::String)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[inline]
|
|
147
|
+
fn convert_symbol(value: Value) -> Result<JsonValue, Error> {
|
|
148
|
+
symbol_name_from_value(value).map(JsonValue::String)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn convert_array(ruby: &Ruby, value: Value, depth: u16) -> Result<JsonValue, Error> {
|
|
152
|
+
if depth >= RECURSION_LIMIT {
|
|
153
|
+
return Err(Error::new(
|
|
154
|
+
ruby.exception_arg_error(),
|
|
155
|
+
format!("Exceeded maximum nesting depth ({RECURSION_LIMIT})"),
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
let Some(arr) = RArray::from_value(value) else {
|
|
159
|
+
unreachable!("We checked the type tag")
|
|
160
|
+
};
|
|
161
|
+
let len = arr.len();
|
|
162
|
+
let mut json_arr = Vec::with_capacity(len);
|
|
163
|
+
// Do not use `RArray::as_slice` here: recursive conversion may call
|
|
164
|
+
// Ruby APIs for nested values, and `as_slice` borrows Ruby-managed
|
|
165
|
+
// memory that must not be held across Ruby calls/GC.
|
|
166
|
+
for idx in 0..len {
|
|
167
|
+
let idx = isize::try_from(idx).map_err(|_| {
|
|
168
|
+
Error::new(
|
|
169
|
+
ruby.exception_arg_error(),
|
|
170
|
+
"Array index exceeds supported range",
|
|
171
|
+
)
|
|
172
|
+
})?;
|
|
173
|
+
let item: Value = arr.entry(idx)?;
|
|
174
|
+
json_arr.push(to_value_recursive(ruby, item, depth + 1)?);
|
|
175
|
+
}
|
|
176
|
+
Ok(JsonValue::Array(json_arr))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn convert_hash(ruby: &Ruby, value: Value, depth: u16) -> Result<JsonValue, Error> {
|
|
180
|
+
if depth >= RECURSION_LIMIT {
|
|
181
|
+
return Err(Error::new(
|
|
182
|
+
ruby.exception_arg_error(),
|
|
183
|
+
format!("Exceeded maximum nesting depth ({RECURSION_LIMIT})"),
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
let Some(hash) = RHash::from_value(value) else {
|
|
187
|
+
unreachable!("We checked the type tag")
|
|
188
|
+
};
|
|
189
|
+
let mut map = Map::with_capacity(hash.len());
|
|
190
|
+
hash.foreach(|key: Value, val: Value| {
|
|
191
|
+
let key_str = hash_key_to_string(ruby, key)?;
|
|
192
|
+
let json_val = to_value_recursive(ruby, val, depth + 1)?;
|
|
193
|
+
map.insert(key_str, json_val);
|
|
194
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
195
|
+
})?;
|
|
196
|
+
Ok(JsonValue::Object(map))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[inline]
|
|
200
|
+
fn unsupported_type_error(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
201
|
+
let class = value.class();
|
|
202
|
+
#[allow(unsafe_code)]
|
|
203
|
+
let class_name = unsafe { class.name() };
|
|
204
|
+
Err(Error::new(
|
|
205
|
+
ruby.exception_type_error(),
|
|
206
|
+
format!("Unsupported type: '{class_name}'"),
|
|
207
|
+
))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[derive(Clone, Copy)]
|
|
211
|
+
enum CanonicalErrorKind {
|
|
212
|
+
Argument,
|
|
213
|
+
Type,
|
|
214
|
+
Encoding,
|
|
215
|
+
Runtime,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
impl CanonicalErrorKind {
|
|
219
|
+
#[inline]
|
|
220
|
+
fn as_tag(self) -> &'static str {
|
|
221
|
+
match self {
|
|
222
|
+
Self::Argument => "argument",
|
|
223
|
+
Self::Type => "type",
|
|
224
|
+
Self::Encoding => "encoding",
|
|
225
|
+
Self::Runtime => "runtime",
|
|
102
226
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[inline]
|
|
230
|
+
fn from_tag(tag: &str) -> Option<Self> {
|
|
231
|
+
match tag {
|
|
232
|
+
"argument" => Some(Self::Argument),
|
|
233
|
+
"type" => Some(Self::Type),
|
|
234
|
+
"encoding" => Some(Self::Encoding),
|
|
235
|
+
"runtime" => Some(Self::Runtime),
|
|
236
|
+
_ => None,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[inline]
|
|
241
|
+
fn exception(self, ruby: &Ruby) -> ExceptionClass {
|
|
242
|
+
match self {
|
|
243
|
+
Self::Argument => ruby.exception_arg_error(),
|
|
244
|
+
Self::Type => ruby.exception_type_error(),
|
|
245
|
+
Self::Encoding => ruby.exception_encoding_error(),
|
|
246
|
+
Self::Runtime => ruby.exception_runtime_error(),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[inline]
|
|
252
|
+
fn canonical_error_kind_from_ruby_error(ruby: &Ruby, error: &Error) -> CanonicalErrorKind {
|
|
253
|
+
if error.is_kind_of(ruby.exception_type_error()) {
|
|
254
|
+
CanonicalErrorKind::Type
|
|
255
|
+
} else if error.is_kind_of(ruby.exception_encoding_error()) {
|
|
256
|
+
CanonicalErrorKind::Encoding
|
|
257
|
+
} else if error.is_kind_of(ruby.exception_runtime_error()) {
|
|
258
|
+
CanonicalErrorKind::Runtime
|
|
259
|
+
} else {
|
|
260
|
+
CanonicalErrorKind::Argument
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[inline]
|
|
265
|
+
fn ruby_error_message(error: &Error) -> String {
|
|
266
|
+
match error.error_type() {
|
|
267
|
+
ErrorType::Error(_, message) => message.to_string(),
|
|
268
|
+
_ => error.to_string(),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[inline]
|
|
273
|
+
fn encode_canonical_error(kind: CanonicalErrorKind, message: &str) -> String {
|
|
274
|
+
format!("{CANONICAL_ERROR_PREFIX}{}|{message}", kind.as_tag())
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[inline]
|
|
278
|
+
fn decode_canonical_error(message: &str) -> Option<(CanonicalErrorKind, &str)> {
|
|
279
|
+
let payload = message.strip_prefix(CANONICAL_ERROR_PREFIX)?;
|
|
280
|
+
let (tag, body) = payload.split_once('|')?;
|
|
281
|
+
Some((CanonicalErrorKind::from_tag(tag)?, body))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[inline]
|
|
285
|
+
fn canonical_serde_error_to_ruby(ruby: &Ruby, error: &serde_json::Error) -> Error {
|
|
286
|
+
let rendered = error.to_string();
|
|
287
|
+
let suffix = format!(" at line {} column {}", error.line(), error.column());
|
|
288
|
+
let without_location = rendered.strip_suffix(&suffix).unwrap_or(&rendered);
|
|
289
|
+
if let Some((kind, message)) = decode_canonical_error(without_location) {
|
|
290
|
+
return Error::new(kind.exception(ruby), message.to_string());
|
|
291
|
+
}
|
|
292
|
+
Error::new(ruby.exception_arg_error(), rendered)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[inline]
|
|
296
|
+
fn ruby_error_to_canonical_serde<S>(ruby: &Ruby, error: &Error) -> S::Error
|
|
297
|
+
where
|
|
298
|
+
S: Serializer,
|
|
299
|
+
{
|
|
300
|
+
let kind = canonical_error_kind_from_ruby_error(ruby, error);
|
|
301
|
+
let message = ruby_error_message(error);
|
|
302
|
+
serde::ser::Error::custom(encode_canonical_error(kind, &message))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[inline]
|
|
306
|
+
fn canonical_serde_error<S>(kind: CanonicalErrorKind, message: &str) -> S::Error
|
|
307
|
+
where
|
|
308
|
+
S: Serializer,
|
|
309
|
+
{
|
|
310
|
+
serde::ser::Error::custom(encode_canonical_error(kind, message))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[inline]
|
|
314
|
+
fn utf8_string_from_rstring(
|
|
315
|
+
ruby: &Ruby,
|
|
316
|
+
rstring: RString,
|
|
317
|
+
error_message: &'static str,
|
|
318
|
+
) -> Result<String, Error> {
|
|
319
|
+
// SAFETY: rstring is valid and we're in Ruby VM context.
|
|
320
|
+
#[allow(unsafe_code)]
|
|
321
|
+
let bytes = unsafe { rstring.as_slice() };
|
|
322
|
+
std::str::from_utf8(bytes)
|
|
323
|
+
.map(str::to_owned)
|
|
324
|
+
.map_err(|_| Error::new(ruby.exception_encoding_error(), error_message))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[inline]
|
|
328
|
+
fn utf8_string_from_value(
|
|
329
|
+
ruby: &Ruby,
|
|
330
|
+
value: Value,
|
|
331
|
+
error_message: &'static str,
|
|
332
|
+
) -> Result<String, Error> {
|
|
333
|
+
let Some(rstring) = RString::from_value(value) else {
|
|
334
|
+
unreachable!("We checked the type tag")
|
|
335
|
+
};
|
|
336
|
+
utf8_string_from_rstring(ruby, rstring, error_message)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[inline]
|
|
340
|
+
fn symbol_name_from_value(value: Value) -> Result<String, Error> {
|
|
341
|
+
let Some(sym) = Symbol::from_value(value) else {
|
|
342
|
+
unreachable!("We checked the type tag")
|
|
343
|
+
};
|
|
344
|
+
Ok(sym.name()?.to_string())
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const MAX_CANONICAL_SCRATCH_POOL_SIZE: usize = 8;
|
|
348
|
+
const MAX_CANONICAL_SCRATCH_CAPACITY: usize = 16_384;
|
|
349
|
+
|
|
350
|
+
struct HashEntry {
|
|
351
|
+
key: String,
|
|
352
|
+
value: Value,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
struct HashEntryScratch<'a> {
|
|
356
|
+
entries: Vec<HashEntry>,
|
|
357
|
+
pool: &'a RefCell<Vec<Vec<HashEntry>>>,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
impl<'a> HashEntryScratch<'a> {
|
|
361
|
+
fn with_capacity(pool: &'a RefCell<Vec<Vec<HashEntry>>>, capacity: usize) -> Self {
|
|
362
|
+
let mut entries = pool.borrow_mut().pop().unwrap_or_default();
|
|
363
|
+
if entries.capacity() < capacity {
|
|
364
|
+
entries.reserve(capacity - entries.capacity());
|
|
365
|
+
}
|
|
366
|
+
Self { entries, pool }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[inline]
|
|
370
|
+
fn entries_mut(&mut self) -> &mut Vec<HashEntry> {
|
|
371
|
+
&mut self.entries
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[inline]
|
|
375
|
+
fn entries(&self) -> &[HashEntry] {
|
|
376
|
+
&self.entries
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
impl Drop for HashEntryScratch<'_> {
|
|
381
|
+
fn drop(&mut self) {
|
|
382
|
+
self.entries.clear();
|
|
383
|
+
if self.entries.capacity() > MAX_CANONICAL_SCRATCH_CAPACITY {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
let mut pool = self.pool.borrow_mut();
|
|
387
|
+
if pool.len() < MAX_CANONICAL_SCRATCH_POOL_SIZE {
|
|
388
|
+
pool.push(std::mem::take(&mut self.entries));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
struct CanonicalRubyValue<'scratch> {
|
|
394
|
+
ruby: &'scratch Ruby,
|
|
395
|
+
value: Value,
|
|
396
|
+
depth: u16,
|
|
397
|
+
scratch_pool: &'scratch RefCell<Vec<Vec<HashEntry>>>,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
impl<'scratch> CanonicalRubyValue<'scratch> {
|
|
401
|
+
fn new(
|
|
402
|
+
ruby: &'scratch Ruby,
|
|
403
|
+
value: Value,
|
|
404
|
+
depth: u16,
|
|
405
|
+
scratch_pool: &'scratch RefCell<Vec<Vec<HashEntry>>>,
|
|
406
|
+
) -> Self {
|
|
407
|
+
Self {
|
|
408
|
+
ruby,
|
|
409
|
+
value,
|
|
410
|
+
depth,
|
|
411
|
+
scratch_pool,
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
impl Serialize for CanonicalRubyValue<'_> {
|
|
417
|
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
418
|
+
where
|
|
419
|
+
S: Serializer,
|
|
420
|
+
{
|
|
421
|
+
if self.value.is_nil() {
|
|
422
|
+
return serializer.serialize_unit();
|
|
109
423
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
424
|
+
|
|
425
|
+
// SAFETY: We're reading the type tag of a valid Ruby value
|
|
426
|
+
#[allow(unsafe_code)]
|
|
427
|
+
let value_type = unsafe { RB_TYPE(self.value.as_raw()) };
|
|
428
|
+
|
|
429
|
+
match value_type {
|
|
430
|
+
ruby_value_type::RUBY_T_TRUE => serializer.serialize_bool(true),
|
|
431
|
+
ruby_value_type::RUBY_T_FALSE => serializer.serialize_bool(false),
|
|
432
|
+
ruby_value_type::RUBY_T_FIXNUM | ruby_value_type::RUBY_T_BIGNUM => {
|
|
433
|
+
let number = convert_integer(self.ruby, self.value)
|
|
434
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
435
|
+
number.serialize(serializer)
|
|
116
436
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let mut json_arr = Vec::with_capacity(len);
|
|
122
|
-
// Do not use `RArray::as_slice` here: recursive conversion may call
|
|
123
|
-
// Ruby APIs for nested values, and `as_slice` borrows Ruby-managed
|
|
124
|
-
// memory that must not be held across Ruby calls/GC.
|
|
125
|
-
for idx in 0..len {
|
|
126
|
-
let idx = isize::try_from(idx).map_err(|_| {
|
|
127
|
-
Error::new(
|
|
128
|
-
ruby.exception_arg_error(),
|
|
129
|
-
"Array index exceeds supported range",
|
|
130
|
-
)
|
|
131
|
-
})?;
|
|
132
|
-
let item: Value = arr.entry(idx)?;
|
|
133
|
-
json_arr.push(to_value_recursive(ruby, item, depth + 1)?);
|
|
437
|
+
ruby_value_type::RUBY_T_FLOAT => {
|
|
438
|
+
let number = convert_float_for_canonical(self.ruby, self.value)
|
|
439
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
440
|
+
number.serialize(serializer)
|
|
134
441
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
442
|
+
ruby_value_type::RUBY_T_STRING => {
|
|
443
|
+
let value =
|
|
444
|
+
utf8_string_from_value(self.ruby, self.value, "String is not valid UTF-8")
|
|
445
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
446
|
+
serializer.serialize_str(value.as_str())
|
|
447
|
+
}
|
|
448
|
+
ruby_value_type::RUBY_T_SYMBOL => {
|
|
449
|
+
let name = symbol_name_from_value(self.value)
|
|
450
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
451
|
+
serializer.serialize_str(name.as_str())
|
|
452
|
+
}
|
|
453
|
+
ruby_value_type::RUBY_T_ARRAY => {
|
|
454
|
+
if self.depth >= RECURSION_LIMIT {
|
|
455
|
+
return Err(canonical_serde_error::<S>(
|
|
456
|
+
CanonicalErrorKind::Argument,
|
|
457
|
+
&format!("Exceeded maximum nesting depth ({RECURSION_LIMIT})"),
|
|
458
|
+
));
|
|
459
|
+
}
|
|
460
|
+
let Some(arr) = RArray::from_value(self.value) else {
|
|
461
|
+
unreachable!("We checked the type tag")
|
|
462
|
+
};
|
|
463
|
+
let len = arr.len();
|
|
464
|
+
let mut sequence = serializer.serialize_seq(Some(len))?;
|
|
465
|
+
for idx in 0..len {
|
|
466
|
+
let idx = isize::try_from(idx).map_err(|_| {
|
|
467
|
+
canonical_serde_error::<S>(
|
|
468
|
+
CanonicalErrorKind::Argument,
|
|
469
|
+
"Array index exceeds supported range",
|
|
470
|
+
)
|
|
471
|
+
})?;
|
|
472
|
+
let item: Value = arr
|
|
473
|
+
.entry(idx)
|
|
474
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
475
|
+
sequence.serialize_element(&CanonicalRubyValue::new(
|
|
476
|
+
self.ruby,
|
|
477
|
+
item,
|
|
478
|
+
self.depth + 1,
|
|
479
|
+
self.scratch_pool,
|
|
480
|
+
))?;
|
|
481
|
+
}
|
|
482
|
+
sequence.end()
|
|
483
|
+
}
|
|
484
|
+
ruby_value_type::RUBY_T_HASH => {
|
|
485
|
+
if self.depth >= RECURSION_LIMIT {
|
|
486
|
+
return Err(canonical_serde_error::<S>(
|
|
487
|
+
CanonicalErrorKind::Argument,
|
|
488
|
+
&format!("Exceeded maximum nesting depth ({RECURSION_LIMIT})"),
|
|
489
|
+
));
|
|
490
|
+
}
|
|
491
|
+
let Some(hash) = RHash::from_value(self.value) else {
|
|
492
|
+
unreachable!("We checked the type tag")
|
|
493
|
+
};
|
|
494
|
+
let len = hash.len();
|
|
495
|
+
let mut scratch = HashEntryScratch::with_capacity(self.scratch_pool, len);
|
|
496
|
+
hash.foreach(|key: Value, value: Value| {
|
|
497
|
+
let key = hash_key_to_string(self.ruby, key)?;
|
|
498
|
+
scratch.entries_mut().push(HashEntry { key, value });
|
|
499
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
500
|
+
})
|
|
501
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
502
|
+
let entries = scratch.entries_mut();
|
|
503
|
+
entries.sort_by(|left, right| left.key.as_bytes().cmp(right.key.as_bytes()));
|
|
504
|
+
if entries
|
|
505
|
+
.windows(2)
|
|
506
|
+
.any(|window| window[0].key == window[1].key)
|
|
507
|
+
{
|
|
508
|
+
return Err(canonical_serde_error::<S>(
|
|
509
|
+
CanonicalErrorKind::Type,
|
|
510
|
+
DUPLICATE_CANONICAL_KEY_MESSAGE,
|
|
511
|
+
));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let mut map = serializer.serialize_map(Some(len))?;
|
|
515
|
+
for entry in scratch.entries() {
|
|
516
|
+
map.serialize_entry(
|
|
517
|
+
entry.key.as_str(),
|
|
518
|
+
&CanonicalRubyValue::new(
|
|
519
|
+
self.ruby,
|
|
520
|
+
entry.value,
|
|
521
|
+
self.depth + 1,
|
|
522
|
+
self.scratch_pool,
|
|
523
|
+
),
|
|
524
|
+
)?;
|
|
525
|
+
}
|
|
526
|
+
map.end()
|
|
527
|
+
}
|
|
528
|
+
ruby_value_type::RUBY_T_DATA
|
|
529
|
+
if self
|
|
530
|
+
.value
|
|
531
|
+
.is_kind_of(self.ruby.get_inner(&BIG_DECIMAL_CLASS)) =>
|
|
532
|
+
{
|
|
533
|
+
let number = convert_big_decimal_for_canonical(self.ruby, self.value)
|
|
534
|
+
.map_err(|error| ruby_error_to_canonical_serde::<S>(self.ruby, &error))?;
|
|
535
|
+
number.serialize(serializer)
|
|
536
|
+
}
|
|
537
|
+
_ => {
|
|
538
|
+
let class = self.value.class();
|
|
539
|
+
#[allow(unsafe_code)]
|
|
540
|
+
let class_name = unsafe { class.name() };
|
|
541
|
+
Err(canonical_serde_error::<S>(
|
|
542
|
+
CanonicalErrorKind::Type,
|
|
543
|
+
&format!("Unsupported type: '{class_name}'"),
|
|
544
|
+
))
|
|
143
545
|
}
|
|
144
|
-
let Some(hash) = RHash::from_value(value) else {
|
|
145
|
-
unreachable!("We checked the type tag")
|
|
146
|
-
};
|
|
147
|
-
let mut map = Map::with_capacity(hash.len());
|
|
148
|
-
hash.foreach(|key: Value, val: Value| {
|
|
149
|
-
let key_str = hash_key_to_string(ruby, key)?;
|
|
150
|
-
let json_val = to_value_recursive(ruby, val, depth + 1)?;
|
|
151
|
-
map.insert(key_str, json_val);
|
|
152
|
-
Ok(magnus::r_hash::ForEach::Continue)
|
|
153
|
-
})?;
|
|
154
|
-
Ok(JsonValue::Object(map))
|
|
155
546
|
}
|
|
156
|
-
|
|
157
|
-
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[inline]
|
|
551
|
+
fn convert_float_for_canonical(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
552
|
+
let float = f64::try_convert(value)?;
|
|
553
|
+
if !float.is_finite() {
|
|
554
|
+
return Ok(JsonValue::Null);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if float == 0.0 {
|
|
558
|
+
return Ok(JsonValue::Number(Number::from(0)));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if float.fract() == 0.0 {
|
|
562
|
+
if (0.0..U64_UPPER_EXCLUSIVE_F64).contains(&float) {
|
|
563
|
+
// SAFETY: range check above guarantees a lossless conversion.
|
|
564
|
+
#[allow(unsafe_code)]
|
|
565
|
+
let integer = unsafe { float.to_int_unchecked::<u64>() };
|
|
566
|
+
return Ok(JsonValue::Number(Number::from(integer)));
|
|
158
567
|
}
|
|
159
|
-
|
|
160
|
-
|
|
568
|
+
if (I64_LOWER_INCLUSIVE_F64..I64_UPPER_EXCLUSIVE_F64).contains(&float) {
|
|
569
|
+
// SAFETY: range check above guarantees a lossless conversion.
|
|
161
570
|
#[allow(unsafe_code)]
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
))
|
|
571
|
+
let integer = unsafe { float.to_int_unchecked::<i64>() };
|
|
572
|
+
return Ok(JsonValue::Number(Number::from(integer)));
|
|
573
|
+
}
|
|
574
|
+
if let Some(integer_text) = integer_text_from_float(float) {
|
|
575
|
+
if let Ok(JsonValue::Number(number)) = serde_json::from_str::<JsonValue>(&integer_text)
|
|
576
|
+
{
|
|
577
|
+
return Ok(JsonValue::Number(number));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return Err(Error::new(
|
|
581
|
+
ruby.exception_arg_error(),
|
|
582
|
+
"Cannot convert float to JSON",
|
|
583
|
+
));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
Number::from_f64(float)
|
|
587
|
+
.map(JsonValue::Number)
|
|
588
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "Cannot convert float to JSON"))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fn integer_text_from_float(float: f64) -> Option<String> {
|
|
592
|
+
const IEEE754_F64_FRAC_BITS_I32: i32 = 52;
|
|
593
|
+
let bits = float.to_bits();
|
|
594
|
+
let negative = (bits >> 63) != 0;
|
|
595
|
+
let exp_bits = i32::try_from((bits >> IEEE754_F64_FRAC_BITS) & 0x7ff).ok()?;
|
|
596
|
+
let frac_mask = (1_u64 << IEEE754_F64_FRAC_BITS) - 1;
|
|
597
|
+
let frac = bits & frac_mask;
|
|
598
|
+
|
|
599
|
+
let (mantissa, exponent) = if exp_bits == 0 {
|
|
600
|
+
(frac, 1 - IEEE754_F64_EXP_BIAS - IEEE754_F64_FRAC_BITS_I32)
|
|
601
|
+
} else {
|
|
602
|
+
(
|
|
603
|
+
(1_u64 << IEEE754_F64_FRAC_BITS) | frac,
|
|
604
|
+
exp_bits - IEEE754_F64_EXP_BIAS - IEEE754_F64_FRAC_BITS_I32,
|
|
605
|
+
)
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
if mantissa == 0 {
|
|
609
|
+
return Some("0".to_string());
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if exponent < 0 {
|
|
613
|
+
let shift = exponent.unsigned_abs();
|
|
614
|
+
if shift >= u64::BITS {
|
|
615
|
+
return Some("0".to_string());
|
|
616
|
+
}
|
|
617
|
+
let remainder_mask = (1_u64 << shift) - 1;
|
|
618
|
+
if mantissa & remainder_mask != 0 {
|
|
619
|
+
return None;
|
|
620
|
+
}
|
|
621
|
+
let integer = mantissa >> shift;
|
|
622
|
+
let mut output = String::new();
|
|
623
|
+
if negative {
|
|
624
|
+
output.push('-');
|
|
625
|
+
}
|
|
626
|
+
let _ = write!(&mut output, "{integer}");
|
|
627
|
+
return Some(output);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let mut chunks = Vec::new();
|
|
631
|
+
let mut value = mantissa;
|
|
632
|
+
while value > 0 {
|
|
633
|
+
chunks.push((value % DECIMAL_BASE_U64) as u32);
|
|
634
|
+
value /= DECIMAL_BASE_U64;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let exponent_u32 = u32::try_from(exponent).ok()?;
|
|
638
|
+
for _ in 0..exponent_u32 {
|
|
639
|
+
let mut carry = 0_u64;
|
|
640
|
+
for chunk in &mut chunks {
|
|
641
|
+
let doubled = u64::from(*chunk) * 2 + carry;
|
|
642
|
+
*chunk = (doubled % DECIMAL_BASE_U64) as u32;
|
|
643
|
+
carry = doubled / DECIMAL_BASE_U64;
|
|
167
644
|
}
|
|
645
|
+
if carry != 0 {
|
|
646
|
+
chunks.push(u32::try_from(carry).ok()?);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let mut output = String::new();
|
|
651
|
+
if negative {
|
|
652
|
+
output.push('-');
|
|
168
653
|
}
|
|
654
|
+
|
|
655
|
+
let most_significant = chunks.pop()?;
|
|
656
|
+
let _ = write!(&mut output, "{most_significant}");
|
|
657
|
+
for chunk in chunks.iter().rev() {
|
|
658
|
+
let _ = write!(&mut output, "{chunk:0DECIMAL_CHUNK_WIDTH$}");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
Some(output)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#[inline]
|
|
665
|
+
fn convert_big_decimal_for_canonical(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
666
|
+
let is_finite: bool = value.funcall("finite?", ())?;
|
|
667
|
+
if !is_finite {
|
|
668
|
+
return Ok(JsonValue::Null);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let fractional_part: Value = value.funcall("frac", ())?;
|
|
672
|
+
let is_integer: bool = fractional_part.funcall("zero?", ())?;
|
|
673
|
+
if is_integer {
|
|
674
|
+
let integer_value: Value = value.funcall("to_i", ())?;
|
|
675
|
+
return convert_integer(ruby, integer_value);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
let decimal_text: String = value.funcall("to_s", ("F",))?;
|
|
679
|
+
if let Ok(JsonValue::Number(number)) = serde_json::from_str::<JsonValue>(&decimal_text) {
|
|
680
|
+
return Ok(JsonValue::Number(number));
|
|
681
|
+
}
|
|
682
|
+
Err(Error::new(
|
|
683
|
+
ruby.exception_arg_error(),
|
|
684
|
+
"Cannot convert BigDecimal to JSON",
|
|
685
|
+
))
|
|
169
686
|
}
|
|
170
687
|
|
|
171
688
|
/// Convert Ruby BigDecimal to JSON Number while preserving precision.
|
|
@@ -216,24 +733,10 @@ fn hash_key_to_string(ruby: &Ruby, key: Value) -> Result<String, Error> {
|
|
|
216
733
|
|
|
217
734
|
match key_type {
|
|
218
735
|
ruby_value_type::RUBY_T_STRING => {
|
|
219
|
-
|
|
220
|
-
// SAFETY: rstring is valid
|
|
221
|
-
#[allow(unsafe_code)]
|
|
222
|
-
let bytes = unsafe { rstring.as_slice() };
|
|
223
|
-
return std::str::from_utf8(bytes)
|
|
224
|
-
.map(std::borrow::ToOwned::to_owned)
|
|
225
|
-
.map_err(|_| {
|
|
226
|
-
Error::new(
|
|
227
|
-
ruby.exception_encoding_error(),
|
|
228
|
-
"Hash key is not valid UTF-8",
|
|
229
|
-
)
|
|
230
|
-
});
|
|
231
|
-
}
|
|
736
|
+
return utf8_string_from_value(ruby, key, "Hash key is not valid UTF-8");
|
|
232
737
|
}
|
|
233
738
|
ruby_value_type::RUBY_T_SYMBOL => {
|
|
234
|
-
|
|
235
|
-
return Ok(sym.name()?.to_string());
|
|
236
|
-
}
|
|
739
|
+
return symbol_name_from_value(key);
|
|
237
740
|
}
|
|
238
741
|
_ => {}
|
|
239
742
|
}
|