rpc: implement json-rpc crate (#148)

* rpc: add `json-rpc` from https://github.com/Cuprate/cuprate/pull/43

Maintains all the changes made in that branch

* workspace: add `rpc/json-rpc`

* json-rpc: fix cargo.toml

* add todo

* satisfy clippy

* `method/params` -> `body` switch, adjust input types

* add test helpers, test tagged enums and flatten structs

* fix id type `None` <-> `Some(Id::Null)` difference

* lib.rs: add docs

* impl `Version`

* impl `Id`

* impl `Request`

* impl `Response`

* impl `ErrorCode`

* impl `ErrorObject`

* fixes

* add monero jsonrpc tests

* response: add id test

* add display docs to `ErrorObject`

* remove `#[inline]`

* add id null test

* cleanup

* code: clarify Monero's error code usage in docs

* id: fix macro indentation

* readme: fix `Response` -> `Request`

* request: add `lowercase` test

* tests: formatting, more string tests

* readme: add `Serialization changes`

* code: ugly `match` -> `if`

* response: manual deserialization impl

- lowercase keys only
- enforce either `result/error` but not both

* remove unneeded clone bounds

* readme: add implementation comparison tests

* request/response: more tests

* readme: formatting, assert error messages are expected

* request: add unknown field test

* request/response: add unknown field and unicode test
This commit is contained in:
hinto-janai 2024-06-11 21:12:31 -04:00 committed by GitHub
parent 663c852b13
commit a3e34c3ba8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2414 additions and 135 deletions

275
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
@ -68,9 +68,9 @@ dependencies = [
[[package]]
name = "async-lock"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener",
"event-listener-strategy",
@ -107,20 +107,20 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "autocfg"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
version = "0.3.71"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
dependencies = [
"addr2line",
"cc",
@ -143,9 +143,9 @@ dependencies = [
[[package]]
name = "base64"
version = "0.22.0"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
@ -225,9 +225,9 @@ dependencies = [
[[package]]
name = "borsh"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77"
checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed"
dependencies = [
"borsh-derive",
"cfg_aliases",
@ -235,15 +235,15 @@ dependencies = [
[[package]]
name = "borsh-derive"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d"
checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
dependencies = [
"once_cell",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
"syn_derive",
]
@ -270,7 +270,7 @@ checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -287,9 +287,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.96"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd"
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
[[package]]
name = "cfg-if"
@ -299,9 +299,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
@ -439,9 +439,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
@ -686,7 +686,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -799,9 +799,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "epee-encoding"
@ -824,9 +824,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
@ -834,9 +834,9 @@ dependencies = [
[[package]]
name = "event-listener"
version = "4.0.3"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
dependencies = [
"concurrent-queue",
"parking",
@ -845,9 +845,9 @@ dependencies = [
[[package]]
name = "event-listener-strategy"
version = "0.4.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener",
"pin-project-lite",
@ -872,9 +872,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "fixed-bytes"
@ -962,7 +962,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -1007,9 +1007,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
@ -1018,9 +1018,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.28.1"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "group"
@ -1197,9 +1197,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "908bb38696d7a037a01ebcc68a00634112ac2bbf8ca74e30a2c3d2f4f021302b"
checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
dependencies = [
"futures-util",
"http",
@ -1215,9 +1215,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.3"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [
"bytes",
"futures-channel",
@ -1304,6 +1304,12 @@ dependencies = [
[[package]]
name = "json-rpc"
version = "0.0.0"
dependencies = [
"pretty_assertions",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "keccak"
@ -1337,9 +1343,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libm"
@ -1359,9 +1365,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lmdb-master-sys"
@ -1420,9 +1426,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
dependencies = [
"adler",
]
@ -1574,9 +1580,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.18"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
@ -1594,9 +1600,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.32.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
dependencies = [
"memchr",
]
@ -1643,9 +1649,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "parking_lot"
version = "0.12.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@ -1677,9 +1683,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
@ -1729,7 +1735,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -1758,7 +1764,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -1829,9 +1835,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.81"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
@ -1998,22 +2004,22 @@ dependencies = [
[[package]]
name = "ref-cast"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f"
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc"
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -2039,9 +2045,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc_version"
@ -2067,9 +2073,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.5"
version = "0.23.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e"
checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b"
dependencies = [
"once_cell",
"ring",
@ -2104,15 +2110,15 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.5.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]]
name = "rustls-webpki"
version = "0.102.3"
version = "0.102.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
dependencies = [
"ring",
"rustls-pki-types",
@ -2121,9 +2127,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "rusty-fork"
@ -2139,9 +2145,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "schannel"
@ -2167,16 +2173,16 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "security-framework"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.5.0",
"core-foundation",
"core-foundation-sys",
"libc",
@ -2185,9 +2191,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
dependencies = [
"core-foundation-sys",
"libc",
@ -2195,35 +2201,35 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.199"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.199"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
@ -2347,9 +2353,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.60"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
@ -2365,7 +2371,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -2397,22 +2403,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -2451,9 +2457,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@ -2470,13 +2476,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -2517,9 +2523,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
@ -2535,9 +2541,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
[[package]]
name = "toml_edit"
@ -2589,7 +2595,6 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -2603,7 +2608,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
@ -2754,7 +2759,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
"wasm-bindgen-shared",
]
@ -2776,7 +2781,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2811,11 +2816,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.56.0",
"windows-core 0.57.0",
"windows-targets 0.52.5",
]
@ -2830,9 +2835,9 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
@ -2842,31 +2847,31 @@ dependencies = [
[[package]]
name = "windows-implement"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "windows-interface"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "windows-result"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.5",
]
@ -3036,29 +3041,29 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zerocopy"
version = "0.7.32"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
version = "0.7.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]
[[package]]
name = "zeroize"
version = "1.7.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
@ -3071,5 +3076,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn 2.0.66",
]

View file

@ -7,9 +7,14 @@ license = "MIT"
authors = ["hinto-janai"]
repository = "https://github.com/Cuprate/cuprate/tree/main/rpc/json-rpc"
keywords = ["json", "rpc"]
categories = ["encoding"]
[features]
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["std"] }
thiserror = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

197
rpc/json-rpc/README.md Normal file
View file

@ -0,0 +1,197 @@
# `json-rpc`
JSON-RPC 2.0 types and (de)serialization.
## What
This crate implements the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification)
for usage in [Cuprate](https://github.com/Cuprate/cuprate).
It contains slight modifications catered towards Cuprate and isn't
necessarily a general purpose implementation of the specification
(see below).
This crate expects you to read the brief JSON-RPC 2.0 specification for context.
## Batching
This crate does not have any types for [JSON-RPC 2.0 batching](https://www.jsonrpc.org/specification#batch).
This is because `monerod` does not support this,
as such, neither does Cuprate.
TODO: citation needed on `monerod` not supporting batching.
## Request changes
[JSON-RPC 2.0's `Request` object](https://www.jsonrpc.org/specification#request_object) usually contains these 2 fields:
- `method`
- `params`
This crate replaces those two with a `body` field that is `#[serde(flatten)]`ed,
and assumes the type within that `body` field is tagged properly, for example:
```rust
# use pretty_assertions::assert_eq;
use serde::{Deserialize, Serialize};
use json_rpc::{Id, Request};
// Parameter type.
#[derive(Deserialize, Serialize)]
struct GetBlock {
height: u64,
}
// Method enum containing all enums.
// All methods are tagged as `method`
// and their inner parameter types are
// tagged with `params` (in snake case).
#[derive(Deserialize, Serialize)]
#[serde(tag = "method", content = "params")] // INVARIANT: these tags are needed
#[serde(rename_all = "snake_case")] // for proper (de)serialization.
enum Methods {
GetBlock(GetBlock),
/* other methods */
}
// Create the request object.
let request = Request::new_with_id(
Id::Str("hello".into()),
Methods::GetBlock(GetBlock { height: 123 }),
);
// Serializing properly shows the `method/params` fields
// even though `Request` doesn't contain those fields.
let json = serde_json::to_string_pretty(&request).unwrap();
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": "hello",
"method": "get_block",
"params": {
"height": 123
}
}"#;
assert_eq!(json, expected_json);
```
This is how the method/param types are done in Cuprate.
For reasoning, see: <https://github.com/Cuprate/cuprate/pull/146#issuecomment-2145734838>.
## Serialization changes
This crate's serialized field order slightly differs compared to `monerod`.
`monerod`'s JSON objects are serialized in alphabetically order, where as this crate serializes the fields in their defined order (due to [`serde`]).
With that said, parsing should be not affected at all since a key-value map is used:
```rust
# use pretty_assertions::assert_eq;
use json_rpc::{Id, Response};
let response = Response::ok(Id::Num(123), "OK");
let response_json = serde_json::to_string_pretty(&response).unwrap();
// This crate's `Response` result type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": 123,
"result": "OK"
}"#;
assert_eq!(response_json, expected_json);
// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
"id": 123,
"jsonrpc": "2.0",
"result": "OK"
}"#;
///---
let response = Response::<()>::invalid_request(Id::Num(123));
let response_json = serde_json::to_string_pretty(&response).unwrap();
// This crate's `Response` error type will _always_
// serialize fields in the following order:
let expected_json =
r#"{
"jsonrpc": "2.0",
"id": 123,
"error": {
"code": -32600,
"message": "Invalid Request"
}
}"#;
assert_eq!(response_json, expected_json);
// Although, `monerod` will serialize like such:
let monerod_json =
r#"{
"error": {
"code": -32600,
"message": "Invalid Request"
},
"id": 123
"jsonrpc": "2.0",
}"#;
```
## Compared to other implementations
A quick table showing some small differences between this crate and other JSON-RPC 2.0 implementations.
| Implementation | Allows any case for key fields excluding `method/params` | Allows unknown fields in main `{}`, and response/request objects | Allows overwriting previous values upon duplicate fields (except [`Response`]'s `result/error` field) |
|---|---|---|---|
| [`monerod`](https://github.com/monero-project/monero) | ✅ | ✅ | ✅
| [`jsonrpsee`](https://docs.rs/jsonrpsee) | ❌ | ✅ | ❌
| This crate | ❌ | ✅ | ✅
Allows any case for key fields excluding `method/params`:
```rust
# use json_rpc::Response;
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();
// Only `lowercase` is allowed.
let json = r#"{"jsonRPC":"2.0","id":123,"result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "missing field `jsonrpc` at line 1 column 40");
```
Allows unknown fields in main `{}`, and response/request objects:
```rust
# use json_rpc::Response;
# use serde_json::from_str;
// unknown fields are allowed in main `{}`
// v
let json = r#"{"unknown_field":"asdf","jsonrpc":"2.0","id":123,"result":"OK"}"#;
from_str::<Response<String>>(&json).unwrap();
// and within objects
// v
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":"","unknown_field":"asdf"}}"#;
from_str::<Response<String>>(&json).unwrap();
```
Allows overwriting previous values upon duplicate fields (except [`Response`]'s `result/error` field)
```rust
# use json_rpc::{Id, Response};
# use serde_json::from_str;
# use pretty_assertions::assert_eq;
// duplicate fields will get overwritten by the latest one
// v v
let json = r#"{"jsonrpc":"2.0","id":123,"id":321,"result":"OK"}"#;
let response = from_str::<Response<String>>(&json).unwrap();
assert_eq!(response.id, Id::Num(321));
// But 2 results are not allowed.
let json = r#"{"jsonrpc":"2.0","id":123,"result":"OK","result":"OK"}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 48");
// Same with errors.
let json = r#"{"jsonrpc":"2.0","id":123,"error":{"code":-1,"message":""},"error":{"code":-1,"message":""}}"#;
let err = from_str::<Response<String>>(&json).unwrap_err();
assert_eq!(format!("{err}"), "duplicate field `result/error` at line 1 column 66");
```

View file

@ -0,0 +1,219 @@
//! Error codes.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
};
//---------------------------------------------------------------------------------------------------- ErrorCode
/// [Error object code](https://www.jsonrpc.org/specification#error_object).
///
/// This `enum` encapsulates JSON-RPC 2.0's error codes
/// found in [`ErrorObject`](crate::error::ErrorObject).
///
/// It associates the code integer ([`i32`]) with its defined message.
///
/// # Application defined errors
/// The custom error codes past `-32099` (`-31000, -31001`, ...)
/// defined in JSON-RPC 2.0 are not supported by this enum because:
///
/// 1. The `(i32, &'static str)` required makes the enum more than 3x larger
/// 2. It is not used by Cuprate/Monero[^1]
///
/// [^1]: Defined errors used by Monero (also excludes the last defined error `-32000 to -32099 Server error`): <https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454/contrib/epee/include/net/http_server_handlers_map2.h#L150>
///
/// # Display
/// ```rust
/// use json_rpc::error::ErrorCode;
/// use serde_json::{to_value, from_value, Value};
///
/// for e in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ] {
/// // The formatting is `$CODE: $MSG`.
/// let expected_fmt = format!("{}: {}", e.code(), e.msg());
/// assert_eq!(expected_fmt, format!("{e}"));
/// }
/// ```
///
/// # (De)serialization
/// This type gets (de)serialized as the associated `i32`, for example:
/// ```rust
/// use json_rpc::error::ErrorCode;
/// use serde_json::{to_value, from_value, Value};
///
/// for e in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ErrorCode::ServerError(1),
/// ErrorCode::ServerError(2),
/// ] {
/// // Gets serialized into a JSON integer.
/// let value = to_value(&e).unwrap();
/// assert_eq!(value, Value::Number(e.code().into()));
///
/// // Expects a JSON integer when deserializing.
/// assert_eq!(e, from_value(value).unwrap());
/// }
/// ```
///
/// ```rust,should_panic
/// # use json_rpc::error::ErrorCode;
/// # use serde_json::from_value;
/// // A JSON string that contains an integer won't work.
/// from_value::<ErrorCode>("-32700".into()).unwrap();
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
pub enum ErrorCode {
#[error("{}: {}", PARSE_ERROR.0, PARSE_ERROR.1)]
/// Invalid JSON was received by the server.
///
/// An error occurred on the server while parsing the JSON text.
ParseError,
#[error("{}: {}", INVALID_REQUEST.0, INVALID_REQUEST.1)]
/// The JSON sent is not a valid Request object.
InvalidRequest,
#[error("{}: {}", METHOD_NOT_FOUND.0, METHOD_NOT_FOUND.1)]
/// The method does not exist / is not available.
MethodNotFound,
#[error("{}: {}", INVALID_PARAMS.0, INVALID_PARAMS.1)]
/// Invalid method parameters.
InvalidParams,
#[error("{}: {}", INTERNAL_ERROR.0, INTERNAL_ERROR.1)]
/// Internal JSON-RPC error.
InternalError,
#[error("{0}: {SERVER_ERROR}")]
/// Reserved for implementation-defined server-errors.
ServerError(i32),
}
impl ErrorCode {
/// Creates [`Self`] from a [`i32`] code.
///
/// [`From<i32>`] is the same as this function.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
/// };
///
/// assert_eq!(ErrorCode::from_code(PARSE_ERROR.0), ErrorCode::ParseError);
/// assert_eq!(ErrorCode::from_code(INVALID_REQUEST.0), ErrorCode::InvalidRequest);
/// assert_eq!(ErrorCode::from_code(METHOD_NOT_FOUND.0), ErrorCode::MethodNotFound);
/// assert_eq!(ErrorCode::from_code(INVALID_PARAMS.0), ErrorCode::InvalidParams);
/// assert_eq!(ErrorCode::from_code(INTERNAL_ERROR.0), ErrorCode::InternalError);
///
/// // Non-defined code inputs will default to a custom `ServerError`.
/// assert_eq!(ErrorCode::from_code(0), ErrorCode::ServerError(0));
/// assert_eq!(ErrorCode::from_code(1), ErrorCode::ServerError(1));
/// assert_eq!(ErrorCode::from_code(2), ErrorCode::ServerError(2));
/// ```
pub const fn from_code(code: i32) -> Self {
// FIXME: you cannot `match` on tuple fields
// so use `if` (seems to compile to the same
// assembly as matching directly on `i32`s).
if code == PARSE_ERROR.0 {
Self::ParseError
} else if code == INVALID_REQUEST.0 {
Self::InvalidRequest
} else if code == METHOD_NOT_FOUND.0 {
Self::MethodNotFound
} else if code == INVALID_PARAMS.0 {
Self::InvalidParams
} else if code == INTERNAL_ERROR.0 {
Self::InternalError
} else {
Self::ServerError(code)
}
}
/// Returns `self`'s [`i32`] code representation.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
/// };
///
/// assert_eq!(ErrorCode::ParseError.code(), PARSE_ERROR.0);
/// assert_eq!(ErrorCode::InvalidRequest.code(), INVALID_REQUEST.0);
/// assert_eq!(ErrorCode::MethodNotFound.code(), METHOD_NOT_FOUND.0);
/// assert_eq!(ErrorCode::InvalidParams.code(), INVALID_PARAMS.0);
/// assert_eq!(ErrorCode::InternalError.code(), INTERNAL_ERROR.0);
/// assert_eq!(ErrorCode::ServerError(0).code(), 0);
/// assert_eq!(ErrorCode::ServerError(1).code(), 1);
/// ```
pub const fn code(&self) -> i32 {
match self {
Self::ParseError => PARSE_ERROR.0,
Self::InvalidRequest => INVALID_REQUEST.0,
Self::MethodNotFound => METHOD_NOT_FOUND.0,
Self::InvalidParams => INVALID_PARAMS.0,
Self::InternalError => INTERNAL_ERROR.0,
Self::ServerError(code) => *code,
}
}
/// Returns `self`'s human readable [`str`] message.
///
/// ```rust
/// use json_rpc::error::{
/// ErrorCode,
/// INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
/// };
///
/// assert_eq!(ErrorCode::ParseError.msg(), PARSE_ERROR.1);
/// assert_eq!(ErrorCode::InvalidRequest.msg(), INVALID_REQUEST.1);
/// assert_eq!(ErrorCode::MethodNotFound.msg(), METHOD_NOT_FOUND.1);
/// assert_eq!(ErrorCode::InvalidParams.msg(), INVALID_PARAMS.1);
/// assert_eq!(ErrorCode::InternalError.msg(), INTERNAL_ERROR.1);
/// assert_eq!(ErrorCode::ServerError(0).msg(), SERVER_ERROR);
/// ```
pub const fn msg(&self) -> &'static str {
match self {
Self::ParseError => PARSE_ERROR.1,
Self::InvalidRequest => INVALID_REQUEST.1,
Self::MethodNotFound => METHOD_NOT_FOUND.1,
Self::InvalidParams => INVALID_PARAMS.1,
Self::InternalError => INTERNAL_ERROR.1,
Self::ServerError(_) => SERVER_ERROR,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<N: Into<i32>> From<N> for ErrorCode {
fn from(code: N) -> Self {
Self::from_code(code.into())
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
impl<'a> Deserialize<'a> for ErrorCode {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::from_code(Deserialize::deserialize(deserializer)?))
}
}
impl Serialize for ErrorCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_i32(self.code())
}
}

View file

@ -0,0 +1,22 @@
//! [`JSON-RPC 2.0`](https://www.jsonrpc.org/specification#error_object) defined errors as constants.
//---------------------------------------------------------------------------------------------------- JSON-RPC spec errors.
/// Code and message for [`ErrorCode::ParseError`](crate::error::ErrorCode::ParseError).
pub const PARSE_ERROR: (i32, &str) = (-32700, "Parse error");
/// Code and message for [`ErrorCode::InvalidRequest`](crate::error::ErrorCode::InvalidRequest).
pub const INVALID_REQUEST: (i32, &str) = (-32600, "Invalid Request");
/// Code and message for [`ErrorCode::MethodNotFound`](crate::error::ErrorCode::MethodNotFound).
pub const METHOD_NOT_FOUND: (i32, &str) = (-32601, "Method not found");
/// Code and message for [`ErrorCode::InvalidParams`](crate::error::ErrorCode::InvalidParams).
pub const INVALID_PARAMS: (i32, &str) = (-32602, "Invalid params");
/// Code and message for [`ErrorCode::InternalError`](crate::error::ErrorCode::InternalError).
pub const INTERNAL_ERROR: (i32, &str) = (-32603, "Internal error");
/// Message for [`ErrorCode::ServerError`](crate::error::ErrorCode::ServerError).
///
/// The [`i32`] error code is the caller's choice, this is only the message.
pub const SERVER_ERROR: &str = "Server error";

View file

@ -0,0 +1,14 @@
//! [Error codes and objects](https://www.jsonrpc.org/specification#error_object).
//!
//! This module contains JSON-RPC 2.0's error object and codes,
//! as well as some associated constants.
mod code;
mod constants;
mod object;
pub use code::ErrorCode;
pub use constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, SERVER_ERROR,
};
pub use object::ErrorObject;

View file

@ -0,0 +1,258 @@
//! Error object.
//---------------------------------------------------------------------------------------------------- Use
use std::{borrow::Cow, error::Error, fmt::Display};
use serde::{Deserialize, Serialize};
use serde_json::value::Value;
use crate::error::{
constants::{
INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
SERVER_ERROR,
},
ErrorCode,
};
//---------------------------------------------------------------------------------------------------- ErrorObject
/// [The error object](https://www.jsonrpc.org/specification).
///
/// This is the object sent back in a [`Response`](crate::Response)
/// if the method call errored.
///
/// # Display
/// ```rust
/// use json_rpc::error::ErrorObject;
///
/// // The format is `$CODE: $MESSAGE`.
/// // If a message was not passed during construction,
/// // the error code's message will be used.
/// assert_eq!(format!("{}", ErrorObject::parse_error()), "-32700: Parse error");
/// assert_eq!(format!("{}", ErrorObject::invalid_request()), "-32600: Invalid Request");
/// assert_eq!(format!("{}", ErrorObject::method_not_found()), "-32601: Method not found");
/// assert_eq!(format!("{}", ErrorObject::invalid_params()), "-32602: Invalid params");
/// assert_eq!(format!("{}", ErrorObject::internal_error()), "-32603: Internal error");
/// assert_eq!(format!("{}", ErrorObject::server_error(0)), "0: Server error");
///
/// // Set a custom message.
/// let mut e = ErrorObject::server_error(1);
/// e.message = "hello".into();
/// assert_eq!(format!("{e}"), "1: hello");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorObject {
/// The error code.
pub code: ErrorCode,
/// A custom message for this error, distinct from [`ErrorCode::msg`].
///
/// A JSON `string` value.
///
/// This is a `Cow<'static, str>` to support both 0-allocation for
/// `const` string ID's commonly found in programs, as well as support
/// for runtime [`String`]'s.
pub message: Cow<'static, str>,
/// Optional data associated with the error.
///
/// # `None` vs `Some(Value::Null)`
/// This field will be completely omitted during serialization if [`None`],
/// however if it is `Some(Value::Null)`, it will be serialized as `"data": null`.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl ErrorObject {
/// Creates a new error, deriving the message from the code.
///
/// Same as `ErrorObject::from(ErrorCode)`.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// for code in [
/// ErrorCode::ParseError,
/// ErrorCode::InvalidRequest,
/// ErrorCode::MethodNotFound,
/// ErrorCode::InvalidParams,
/// ErrorCode::InternalError,
/// ErrorCode::ServerError(0),
/// ] {
/// let object = ErrorObject::from_code(code);
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
///
/// }
/// ```
pub const fn from_code(code: ErrorCode) -> Self {
Self {
code,
message: Cow::Borrowed(code.msg()),
data: None,
}
}
/// Creates a new error using [`PARSE_ERROR`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::ParseError;
/// let object = ErrorObject::parse_error();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn parse_error() -> Self {
Self {
code: ErrorCode::ParseError,
message: Cow::Borrowed(PARSE_ERROR.1),
data: None,
}
}
/// Creates a new error using [`INVALID_REQUEST`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InvalidRequest;
/// let object = ErrorObject::invalid_request();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn invalid_request() -> Self {
Self {
code: ErrorCode::InvalidRequest,
message: Cow::Borrowed(INVALID_REQUEST.1),
data: None,
}
}
/// Creates a new error using [`METHOD_NOT_FOUND`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::MethodNotFound;
/// let object = ErrorObject::method_not_found();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn method_not_found() -> Self {
Self {
code: ErrorCode::MethodNotFound,
message: Cow::Borrowed(METHOD_NOT_FOUND.1),
data: None,
}
}
/// Creates a new error using [`INVALID_PARAMS`].
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InvalidParams;
/// let object = ErrorObject::invalid_params();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn invalid_params() -> Self {
Self {
code: ErrorCode::InvalidParams,
message: Cow::Borrowed(INVALID_PARAMS.1),
data: None,
}
}
/// Creates a new error using [`INTERNAL_ERROR`].
///
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::InternalError;
/// let object = ErrorObject::internal_error();
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn internal_error() -> Self {
Self {
code: ErrorCode::InternalError,
message: Cow::Borrowed(INTERNAL_ERROR.1),
data: None,
}
}
/// Creates a new error using [`SERVER_ERROR`].
///
/// You must provide the custom [`i32`] error code.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::error::{ErrorCode, ErrorObject};
///
/// let code = ErrorCode::ServerError(0);
/// let object = ErrorObject::server_error(0);
/// assert_eq!(object, ErrorObject {
/// code,
/// message: Cow::Borrowed(code.msg()),
/// data: None,
/// });
/// ```
pub const fn server_error(error_code: i32) -> Self {
Self {
code: ErrorCode::ServerError(error_code),
message: Cow::Borrowed(SERVER_ERROR),
data: None,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl From<ErrorCode> for ErrorObject {
fn from(code: ErrorCode) -> Self {
Self::from_code(code)
}
}
impl Display for ErrorObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Using `self.code`'s formatting will write the
// message twice, so prefer the built-in message.
write!(f, "{}: {}", self.code.code(), self.message)
}
}
impl Error for ErrorObject {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.code)
}
fn description(&self) -> &str {
&self.message
}
}

242
rpc/json-rpc/src/id.rs Normal file
View file

@ -0,0 +1,242 @@
//! [`Id`]: request/response identification.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
//---------------------------------------------------------------------------------------------------- Id
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
/// [Request](crate::Request)/[Response](crate::Response) identification.
///
/// This is the [JSON-RPC 2.0 `id` field](https://www.jsonrpc.org/specification)
/// type found in `Request/Response`s.
///
/// # From
/// This type implements [`From`] on:
/// - [`String`]
/// - [`str`]
/// - [`u8`], [`u16`], [`u32`], [`u64`]
///
/// and all of those wrapped in [`Option`].
///
/// If the `Option` is [`None`], [`Id::Null`] is returned.
///
/// Note that the `&str` implementations will allocate, use [`Id::from_static_str`]
/// (or just manually create the `Cow`) for a non-allocating `Id`.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::from(String::new()), Id::Str("".into()));
/// assert_eq!(Id::from(Some(String::new())), Id::Str("".into()));
/// assert_eq!(Id::from(None::<String>), Id::Null);
/// assert_eq!(Id::from(123_u64), Id::Num(123_u64));
/// assert_eq!(Id::from(Some(123_u64)), Id::Num(123_u64));
/// assert_eq!(Id::from(None::<u64>), Id::Null);
/// ```
pub enum Id {
/// A JSON `null` value.
///
/// ```rust
/// use json_rpc::Id;
/// use serde_json::{from_value,to_value,json,Value};
///
/// assert_eq!(from_value::<Id>(json!(null)).unwrap(), Id::Null);
/// assert_eq!(to_value(Id::Null).unwrap(), Value::Null);
///
/// // Not a real `null`, but a string.
/// assert_eq!(from_value::<Id>(json!("null")).unwrap(), Id::Str("null".into()));
/// ```
Null,
/// A JSON `number` value.
Num(u64),
/// A JSON `string` value.
///
/// This is a `Cow<'static, str>` to support both 0-allocation for
/// `const` string ID's commonly found in programs, as well as support
/// for runtime [`String`]'s.
///
/// ```rust
/// use std::borrow::Cow;
/// use json_rpc::Id;
///
/// /// A program's static ID.
/// const ID: &'static str = "my_id";
///
/// // No allocation.
/// let s = Id::Str(Cow::Borrowed(ID));
///
/// // Runtime allocation.
/// let s = Id::Str(Cow::Owned("runtime_id".to_string()));
/// ```
Str(Cow<'static, str>),
}
impl Id {
/// This returns `Some(u64)` if [`Id`] is a number.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::Num(0).as_u64(), Some(0));
/// assert_eq!(Id::Str("0".into()).as_u64(), None);
/// assert_eq!(Id::Null.as_u64(), None);
/// ```
pub const fn as_u64(&self) -> Option<u64> {
match self {
Self::Num(n) => Some(*n),
_ => None,
}
}
/// This returns `Some(&str)` if [`Id`] is a string.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::Str("0".into()).as_str(), Some("0"));
/// assert_eq!(Id::Num(0).as_str(), None);
/// assert_eq!(Id::Null.as_str(), None);
/// ```
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Str(s) => Some(s.as_ref()),
_ => None,
}
}
/// Returns `true` if `self` is [`Id::Null`].
///
/// ```rust
/// use json_rpc::Id;
///
/// assert!(Id::Null.is_null());
/// assert!(!Id::Num(0).is_null());
/// assert!(!Id::Str("".into()).is_null());
/// ```
pub fn is_null(&self) -> bool {
*self == Self::Null
}
/// Create a new [`Id::Str`] from a static string.
///
/// ```rust
/// use json_rpc::Id;
///
/// assert_eq!(Id::from_static_str("hi"), Id::Str("hi".into()));
/// ```
pub const fn from_static_str(s: &'static str) -> Self {
Self::Str(Cow::Borrowed(s))
}
/// Inner infallible implementation of [`FromStr::from_str`]
const fn from_string(s: String) -> Self {
Self::Str(Cow::Owned(s))
}
}
impl std::str::FromStr for Id {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, std::convert::Infallible> {
Ok(Self::from_string(s.to_string()))
}
}
impl From<String> for Id {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl From<&str> for Id {
fn from(s: &str) -> Self {
Self::from_string(s.to_string())
}
}
impl From<Option<String>> for Id {
fn from(s: Option<String>) -> Self {
match s {
Some(s) => Self::from_string(s),
None => Self::Null,
}
}
}
impl From<Option<&str>> for Id {
fn from(s: Option<&str>) -> Self {
let s = s.map(ToString::to_string);
s.into()
}
}
/// Implement `From<unsigned integer>` for `Id`.
///
/// Not a generic since that clashes with `From<String>`.
macro_rules! impl_u {
($($u:ty),*) => {
$(
impl From<$u> for Id {
fn from(u: $u) -> Self {
Self::Num(u as u64)
}
}
impl From<&$u> for Id {
fn from(u: &$u) -> Self {
Self::Num(*u as u64)
}
}
impl From<Option<$u>> for Id {
fn from(u: Option<$u>) -> Self {
match u {
Some(u) => Self::Num(u as u64),
None => Self::Null,
}
}
}
)*
}
}
impl_u!(u8, u16, u32);
#[cfg(target_pointer_width = "64")]
impl_u!(u64);
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use super::*;
/// Basic [`Id::as_u64()`] tests.
#[test]
fn __as_u64() {
let id = Id::Num(u64::MIN);
assert_eq!(id.as_u64().unwrap(), u64::MIN);
let id = Id::Num(u64::MAX);
assert_eq!(id.as_u64().unwrap(), u64::MAX);
let id = Id::Null;
assert!(id.as_u64().is_none());
let id = Id::Str("".into());
assert!(id.as_u64().is_none());
}
/// Basic [`Id::as_str()`] tests.
#[test]
fn __as_str() {
let id = Id::Str("str".into());
assert_eq!(id.as_str().unwrap(), "str");
let id = Id::Null;
assert!(id.as_str().is_none());
let id = Id::Num(0);
assert!(id.as_str().is_none());
}
}

View file

@ -1 +1,110 @@
#![doc = include_str!("../README.md")]
//---------------------------------------------------------------------------------------------------- Lints
// Forbid lints.
// Our code, and code generated (e.g macros) cannot overrule these.
#![forbid(
// `unsafe` is allowed but it _must_ be
// commented with `SAFETY: reason`.
clippy::undocumented_unsafe_blocks,
// Never.
unused_unsafe,
redundant_semicolons,
unused_allocation,
coherence_leak_check,
while_true,
// Maybe can be put into `#[deny]`.
unconditional_recursion,
for_loops_over_fallibles,
unused_braces,
unused_labels,
keyword_idents,
non_ascii_idents,
variant_size_differences,
single_use_lifetimes,
// Probably can be put into `#[deny]`.
future_incompatible,
let_underscore,
break_with_label_and_loop,
duplicate_macro_attributes,
exported_private_dependencies,
large_assignments,
overlapping_range_endpoints,
semicolon_in_expressions_from_macros,
noop_method_call,
unreachable_pub,
)]
// Deny lints.
// Some of these are `#[allow]`'ed on a per-case basis.
#![deny(
clippy::all,
clippy::correctness,
clippy::suspicious,
clippy::style,
clippy::complexity,
clippy::perf,
clippy::pedantic,
clippy::nursery,
clippy::cargo,
clippy::missing_docs_in_private_items,
unused_mut,
missing_docs,
deprecated,
unused_comparisons,
nonstandard_style
)]
#![allow(
// FIXME: this lint affects crates outside of
// `database/` for some reason, allow for now.
clippy::cargo_common_metadata,
// FIXME: adding `#[must_use]` onto everything
// might just be more annoying than useful...
// although it is sometimes nice.
clippy::must_use_candidate,
// FIXME: good lint but too many false positives
// with our `Env` + `RwLock` setup.
clippy::significant_drop_tightening,
// FIXME: good lint but is less clear in most cases.
clippy::items_after_statements,
clippy::module_name_repetitions,
clippy::module_inception,
clippy::redundant_pub_crate,
clippy::option_if_let_else,
)]
// Allow some lints when running in debug mode.
#![cfg_attr(debug_assertions, allow(clippy::todo, clippy::multiple_crate_versions))]
// Allow some lints in tests.
#![cfg_attr(
test,
allow(
clippy::cognitive_complexity,
clippy::needless_pass_by_value,
clippy::cast_possible_truncation,
clippy::too_many_lines
)
)]
//---------------------------------------------------------------------------------------------------- Mod/Use
pub mod error;
mod id;
pub use id::Id;
mod version;
pub use version::Version;
mod request;
pub use request::Request;
mod response;
pub use response::Response;
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod tests;

354
rpc/json-rpc/src/request.rs Normal file
View file

@ -0,0 +1,354 @@
//! JSON-RPC 2.0 request object.
//---------------------------------------------------------------------------------------------------- Use
use serde::{Deserialize, Serialize};
use crate::{id::Id, version::Version};
//---------------------------------------------------------------------------------------------------- Request
/// [The request object](https://www.jsonrpc.org/specification#request_object).
///
/// The generic `T` is the body type of the request, i.e. it is the
/// type that holds both the `method` and `params`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Request<T> {
/// JSON-RPC protocol version; always `2.0`.
pub jsonrpc: Version,
/// An identifier established by the Client.
///
/// If it is not included it is assumed to be a
/// [notification](https://www.jsonrpc.org/specification#notification).
///
/// ### `None` vs `Some(Id::Null)`
/// This field will be completely omitted during serialization if [`None`],
/// however if it is `Some(Id::Null)`, it will be serialized as `"id": null`.
///
/// Note that the JSON-RPC 2.0 specification discourages the use of `Id::NUll`,
/// so if there is no ID needed, consider using `None`.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
#[serde(flatten)]
/// The `method` and `params` fields.
///
/// - `method`: A type that serializes as the name of the method to be invoked.
/// - `params`: A structured value that holds the parameter values to be used during the invocation of the method.
///
/// As mentioned in the library documentation, there are no `method/params` fields in [`Request`],
/// they are both merged in this `body` field which is `#[serde(flatten)]`ed.
///
/// ### Invariant
/// Your `T` must serialize as `method` and `params` to comply with the specification.
pub body: T,
}
impl<T> Request<T> {
/// Create a new [`Self`] with no [`Id`].
///
/// ```rust
/// use json_rpc::Request;
///
/// assert_eq!(Request::new("").id, None);
/// ```
pub const fn new(body: T) -> Self {
Self {
jsonrpc: Version,
id: None,
body,
}
}
/// Create a new [`Self`] with an [`Id`].
///
/// ```rust
/// use json_rpc::{Id, Request};
///
/// assert_eq!(Request::new_with_id(Id::Num(0), "").id, Some(Id::Num(0)));
/// ```
pub const fn new_with_id(id: Id, body: T) -> Self {
Self {
jsonrpc: Version,
id: Some(id),
body,
}
}
/// Returns `true` if the request is [notification](https://www.jsonrpc.org/specification#notification).
///
/// In other words, if `id` is [`None`], this returns `true`.
///
/// ```rust
/// use json_rpc::{Id, Request};
///
/// assert!(Request::new("").is_notification());
/// assert!(!Request::new_with_id(Id::Null, "").is_notification());
/// ```
pub const fn is_notification(&self) -> bool {
self.id.is_none()
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<T> std::fmt::Display for Request<T>
where
T: std::fmt::Display + Serialize,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_string_pretty(self) {
Ok(json) => write!(f, "{json}"),
Err(_) => Err(std::fmt::Error),
}
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use super::*;
use crate::{
id::Id,
tests::{assert_ser, Body},
};
use pretty_assertions::assert_eq;
use serde_json::{json, Value};
/// Basic serde tests.
#[test]
fn serde() {
let id = Id::Num(123);
let body = Body {
method: "a_method".into(),
params: [0, 1, 2],
};
let req = Request::new_with_id(id, body);
assert!(!req.is_notification());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Request<Body<[u8; 3]>> = serde_json::from_str(&ser).unwrap();
assert_eq!(req, de);
}
/// Asserts that fields must be `lowercase`.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `jsonrpc`\", line: 1, column: 63)"
)]
fn lowercase() {
let id = Id::Num(123);
let body = Body {
method: "a_method".into(),
params: [0, 1, 2],
};
let req = Request::new_with_id(id, body);
let ser: String = serde_json::to_string(&req).unwrap();
assert_eq!(
ser,
r#"{"jsonrpc":"2.0","id":123,"method":"a_method","params":[0,1,2]}"#,
);
let mixed_case = r#"{"jSoNRPC":"2.0","ID":123,"method":"a_method","params":[0,1,2]}"#;
let de: Request<Body<[u8; 3]>> = serde_json::from_str(mixed_case).unwrap();
assert_eq!(de, req);
}
/// Tests that null `id` shows when serializing.
#[test]
fn request_null_id() {
let req = Request::new_with_id(
Id::Null,
Body {
method: "m".into(),
params: "p".to_string(),
},
);
let json = json!({
"jsonrpc": "2.0",
"id": null,
"method": "m",
"params": "p",
});
assert_ser(&req, &json);
}
/// Tests that a `None` `id` omits the field when serializing.
#[test]
fn request_none_id() {
let req = Request::new(Body {
method: "a".into(),
params: "b".to_string(),
});
let json = json!({
"jsonrpc": "2.0",
"method": "a",
"params": "b",
});
assert_ser(&req, &json);
}
/// Tests that omitting `params` omits the field when serializing.
#[test]
fn request_no_params() {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct NoParamMethod {
method: String,
}
let req = Request::new_with_id(
Id::Num(123),
NoParamMethod {
method: "asdf".to_string(),
},
);
let json = json!({
"jsonrpc": "2.0",
"id": 123,
"method": "asdf",
});
assert_ser(&req, &json);
}
/// Tests that tagged enums serialize correctly.
#[test]
fn request_tagged_enums() {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct GetHeight {
height: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "method", content = "params")]
#[serde(rename_all = "snake_case")]
enum Methods {
GetHeight(/* param: */ GetHeight),
}
let req = Request::new_with_id(Id::Num(123), Methods::GetHeight(GetHeight { height: 0 }));
let json = json!({
"jsonrpc": "2.0",
"id": 123,
"method": "get_height",
"params": {
"height": 0,
},
});
assert_ser(&req, &json);
}
/// Tests that requests serialize into the expected JSON value.
#[test]
fn request_is_expected_value() {
// Test values: (request, expected_value)
let array: [(Request<Body<[u8; 3]>>, Value); 3] = [
(
Request::new_with_id(
Id::Num(123),
Body {
method: "method_1".into(),
params: [0, 1, 2],
},
),
json!({
"jsonrpc": "2.0",
"id": 123,
"method": "method_1",
"params": [0, 1, 2],
}),
),
(
Request::new_with_id(
Id::Null,
Body {
method: "method_2".into(),
params: [3, 4, 5],
},
),
json!({
"jsonrpc": "2.0",
"id": null,
"method": "method_2",
"params": [3, 4, 5],
}),
),
(
Request::new_with_id(
Id::Str("string_id".into()),
Body {
method: "method_3".into(),
params: [6, 7, 8],
},
),
json!({
"jsonrpc": "2.0",
"method": "method_3",
"id": "string_id",
"params": [6, 7, 8],
}),
),
];
for (request, expected_value) in array {
assert_ser(&request, &expected_value);
}
}
/// Tests that non-ordered fields still deserialize okay.
#[test]
fn deserialize_out_of_order_keys() {
let expected = Request::new_with_id(
Id::Str("id".into()),
Body {
method: "method".into(),
params: [0, 1, 2],
},
);
let json = json!({
"method": "method",
"id": "id",
"params": [0, 1, 2],
"jsonrpc": "2.0",
});
let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
assert_eq!(resp, expected);
}
/// Tests that unknown fields are ignored, and deserialize continues.
/// Also that unicode and backslashes work.
#[test]
fn unknown_fields_and_unicode() {
let expected = Request::new_with_id(
Id::Str("id".into()),
Body {
method: "method".into(),
params: [0, 1, 2],
},
);
let json = json!({
"unknown_field": 123,
"method": "method",
"unknown_field": 123,
"id": "id",
"\nhello": 123,
"params": [0, 1, 2],
"\u{00f8}": 123,
"jsonrpc": "2.0",
"unknown_field": 123,
});
let resp = serde_json::from_value::<Request<Body<[u8; 3]>>>(json).unwrap();
assert_eq!(resp, expected);
}
}

View file

@ -0,0 +1,485 @@
//! JSON-RPC 2.0 response object.
//---------------------------------------------------------------------------------------------------- Use
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
use crate::{error::ErrorObject, id::Id, version::Version};
//---------------------------------------------------------------------------------------------------- Response
/// [The response object](https://www.jsonrpc.org/specification#response_object).
///
/// The generic `T` is the response payload, i.e. it is the
/// type that holds both the `method` and `params`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response<T> {
/// JSON-RPC protocol version; always `2.0`.
pub jsonrpc: Version,
/// This field must always be present in serialized JSON.
///
/// ### JSON-RPC 2.0 rules
/// - The [`Response`]'s ID must be the same as the [`Request`](crate::Request)
/// - If the `Request` omitted the `id` field, there should be no `Response`
/// - If there was an error in detecting the `Request`'s ID, the `Response` must contain an [`Id::Null`]
pub id: Id,
/// The response payload.
///
/// ### JSON-RPC 2.0 rules
/// - This must be [`Ok`] upon success
/// - This must be [`Err`] upon error
/// - This can be any (de)serializable data `T` on success
/// - This must be [`ErrorObject`] on errors
pub payload: Result<T, ErrorObject>,
}
impl<T> Response<T> {
/// Creates a successful response.
///
/// ```rust
/// use json_rpc::{Id, Response};
///
/// let ok = Response::ok(Id::Num(123), "OK");
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":123,"result":"OK"}"#);
/// ```
pub const fn ok(id: Id, result: T) -> Self {
Self {
jsonrpc: Version,
id,
payload: Ok(result),
}
}
/// Creates an error response.
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let err = ErrorObject {
/// code: 0.into(),
/// message: "m".into(),
/// data: Some("d".into()),
/// };
///
/// let ok = Response::<()>::err(Id::Num(123), err);
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":123,"error":{"code":0,"message":"m","data":"d"}}"#);
/// ```
pub const fn err(id: Id, error: ErrorObject) -> Self {
Self {
jsonrpc: Version,
id,
payload: Err(error),
}
}
/// Creates an error response using [`ErrorObject::parse_error`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::parse_error(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32700,"message":"Parse error"}}"#);
/// ```
pub const fn parse_error(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::parse_error()),
id,
}
}
/// Creates an error response using [`ErrorObject::invalid_request`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::invalid_request(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32600,"message":"Invalid Request"}}"#);
/// ```
pub const fn invalid_request(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::invalid_request()),
id,
}
}
/// Creates an error response using [`ErrorObject::method_not_found`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::method_not_found(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32601,"message":"Method not found"}}"#);
/// ```
pub const fn method_not_found(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::method_not_found()),
id,
}
}
/// Creates an error response using [`ErrorObject::invalid_params`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::invalid_params(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"Invalid params"}}"#);
/// ```
pub const fn invalid_params(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::invalid_params()),
id,
}
}
/// Creates an error response using [`ErrorObject::internal_error`].
///
/// ```rust
/// use json_rpc::{Id, Response, error::{ErrorObject, ErrorCode}};
///
/// let ok = Response::<()>::internal_error(Id::Num(0));
/// let json = serde_json::to_string(&ok).unwrap();
/// assert_eq!(json, r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32603,"message":"Internal error"}}"#);
/// ```
pub const fn internal_error(id: Id) -> Self {
Self {
jsonrpc: Version,
payload: Err(ErrorObject::internal_error()),
id,
}
}
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl<T> std::fmt::Display for Response<T>
where
T: Serialize,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match serde_json::to_string_pretty(self) {
Ok(json) => write!(f, "{json}"),
Err(_) => Err(std::fmt::Error),
}
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
impl<T> Serialize for Response<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("Response", 3)?;
s.serialize_field("jsonrpc", &self.jsonrpc)?;
// This member is required.
//
// Even if `null`, or the client `Request` didn't include one.
s.serialize_field("id", &self.id)?;
match &self.payload {
Ok(r) => s.serialize_field("result", r)?,
Err(e) => s.serialize_field("error", e)?,
}
s.end()
}
}
// [`Response`] has a manual deserialization implementation because
// we need to confirm `result` and `error` don't both exist:
//
// > Either the result member or error member MUST be included, but both members MUST NOT be included.
//
// <https://www.jsonrpc.org/specification#error_object>
impl<'de, T> Deserialize<'de> for Response<T>
where
T: Deserialize<'de> + 'de,
{
fn deserialize<D: Deserializer<'de>>(der: D) -> Result<Self, D::Error> {
use std::marker::PhantomData;
use serde::de::{Error, MapAccess, Visitor};
/// This type represents the key values within [`Response`].
enum Key {
/// "jsonrpc" field.
JsonRpc,
/// "result" field.
Result,
/// "error" field.
Error,
/// "id" field.
Id,
/// Any other unknown field (ignored).
Unknown,
}
// Deserialization for [`Response`]'s key fields.
//
// This ignores unknown keys.
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
/// Serde visitor for [`Response`]'s key fields.
struct KeyVisitor;
impl Visitor<'_> for KeyVisitor {
type Value = Key;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("`jsonrpc`, `id`, `result`, `error`")
}
fn visit_str<E>(self, string: &str) -> Result<Key, E>
where
E: Error,
{
// PERF: this match is in order of how this library serializes fields.
match string {
"jsonrpc" => Ok(Key::JsonRpc),
"id" => Ok(Key::Id),
"result" => Ok(Key::Result),
"error" => Ok(Key::Error),
// Ignore any other keys that appear
// and continue deserialization.
_ => Ok(Key::Unknown),
}
}
}
deserializer.deserialize_identifier(KeyVisitor)
}
}
/// Serde visitor for the key-value map of [`Response`].
struct MapVisit<T>(PhantomData<T>);
// Deserialization for [`Response`]'s key and values (the JSON map).
impl<'de, T> Visitor<'de> for MapVisit<T>
where
T: Deserialize<'de> + 'de,
{
type Value = Response<T>;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("JSON-RPC 2.0 Response")
}
/// This is a loop that goes over every key-value pair
/// and fills out the necessary fields.
///
/// If both `result/error` appear then this
/// deserialization will error, as to
/// follow the JSON-RPC 2.0 specification.
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
// Initialize values.
let mut jsonrpc = None;
let mut payload = None;
let mut id = None;
// Loop over map, filling values.
while let Some(key) = map.next_key::<Key>()? {
// PERF: this match is in order of how this library serializes fields.
match key {
Key::JsonRpc => jsonrpc = Some(map.next_value::<Version>()?),
Key::Id => id = Some(map.next_value::<Id>()?),
Key::Result => {
if payload.is_none() {
payload = Some(Ok(map.next_value::<T>()?));
} else {
return Err(serde::de::Error::duplicate_field("result/error"));
}
}
Key::Error => {
if payload.is_none() {
payload = Some(Err(map.next_value::<ErrorObject>()?));
} else {
return Err(serde::de::Error::duplicate_field("result/error"));
}
}
Key::Unknown => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
// Make sure all our key-value pairs are set and correct.
match (jsonrpc, id, payload) {
// Response with a single `result` or `error`.
(Some(jsonrpc), Some(id), Some(payload)) => Ok(Response {
jsonrpc,
id,
payload,
}),
// No fields existed.
(None, None, None) => Err(Error::missing_field("jsonrpc + id + result/error")),
// Some field was missing.
(None, _, _) => Err(Error::missing_field("jsonrpc")),
(_, None, _) => Err(Error::missing_field("id")),
(_, _, None) => Err(Error::missing_field("result/error")),
}
}
}
/// All expected fields of the [`Response`] type.
const FIELDS: &[&str; 4] = &["jsonrpc", "id", "result", "error"];
der.deserialize_struct("Response", FIELDS, MapVisit(PhantomData))
}
}
//---------------------------------------------------------------------------------------------------- TESTS
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
use crate::id::Id;
/// Basic serde test on OK results.
#[test]
fn serde_result() {
let result = String::from("result_ok");
let id = Id::Num(123);
let req = Response::ok(id.clone(), result.clone());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Response<String> = serde_json::from_str(&ser).unwrap();
assert_eq!(de.payload.unwrap(), result);
assert_eq!(de.id, id);
}
/// Basic serde test on errors.
#[test]
fn serde_error() {
let error = ErrorObject::internal_error();
let id = Id::Num(123);
let req: Response<String> = Response::err(id.clone(), error.clone());
let ser: String = serde_json::to_string(&req).unwrap();
let de: Response<String> = serde_json::from_str(&ser).unwrap();
assert_eq!(de.payload.unwrap_err(), error);
assert_eq!(de.id, id);
}
/// Test that the `result` and `error` fields are mutually exclusive.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 0, column: 0)"
)]
fn result_error_mutually_exclusive() {
let e = ErrorObject::internal_error();
let j = json!({
"jsonrpc": "2.0",
"id": 0,
"result": "",
"error": e
});
serde_json::from_value::<Response<String>>(j).unwrap();
}
/// Test that the `result` and `error` fields can repeat (and get overwritten).
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 1, column: 45)"
)]
fn result_repeat() {
// `result`
let json = r#"{"jsonrpc":"2.0","id":0,"result":"a","result":"b"}"#;
serde_json::from_str::<Response<String>>(json).unwrap();
}
/// Test that the `error` field cannot repeat.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"duplicate field `result/error`\", line: 1, column: 83)"
)]
fn error_repeat() {
let e = ErrorObject::invalid_request();
let e = serde_json::to_string(&e).unwrap();
let json = format!(r#"{{"jsonrpc":"2.0","id":0,"error":{e},"error":{e}}}"#);
serde_json::from_str::<Response<String>>(&json).unwrap();
}
/// Test that the `id` field must exist.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `id`\", line: 0, column: 0)"
)]
fn id_must_exist() {
let j = json!({
"jsonrpc": "2.0",
"result": "",
});
serde_json::from_value::<Response<String>>(j).unwrap();
}
/// Tests that non-ordered fields still deserialize okay.
#[test]
fn deserialize_out_of_order_keys() {
let e = ErrorObject::internal_error();
let j = json!({
"error": e,
"id": 0,
"jsonrpc": "2.0"
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, Response::internal_error(Id::Num(0)));
let ok = Response::ok(Id::Num(0), "OK".to_string());
let j = json!({
"result": "OK",
"id": 0,
"jsonrpc": "2.0"
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, ok);
}
/// Asserts that fields must be `lowercase`.
#[test]
#[should_panic(
expected = "called `Result::unwrap()` on an `Err` value: Error(\"missing field `jsonrpc`\", line: 1, column: 40)"
)]
fn lowercase() {
let mixed_case = r#"{"jSoNRPC":"2.0","id":123,"result":"OK"}"#;
serde_json::from_str::<Response<String>>(mixed_case).unwrap();
}
/// Tests that unknown fields are ignored, and deserialize continues.
/// Also that unicode and backslashes work.
#[test]
fn unknown_fields_and_unicode() {
let e = ErrorObject::internal_error();
let j = json!({
"error": e,
"\u{00f8}": 123,
"id": 0,
"unknown_field": 123,
"jsonrpc": "2.0",
"unknown_field": 123
});
let resp = serde_json::from_value::<Response<String>>(j).unwrap();
assert_eq!(resp, Response::internal_error(Id::Num(0)));
}
}

250
rpc/json-rpc/src/tests.rs Normal file
View file

@ -0,0 +1,250 @@
//! Tests and utilities.
#![cfg(test)]
#![allow(
clippy::unreadable_literal,
clippy::manual_string_new,
clippy::struct_field_names
)]
//---------------------------------------------------------------------------------------------------- Use
use std::borrow::Cow;
use pretty_assertions::assert_eq;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{to_string, to_string_pretty, to_value, Value};
use crate::{Id, Request, Response};
//---------------------------------------------------------------------------------------------------- Body
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct Body<P> {
pub(crate) method: Cow<'static, str>,
pub(crate) params: P,
}
//---------------------------------------------------------------------------------------------------- Free functions
/// Assert input and output of serialization are the same.
pub(crate) fn assert_ser<T>(t: &T, expected_value: &Value)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let value = to_value(t).unwrap();
assert_eq!(value, *expected_value);
}
/// Assert input and output of string serialization are the same.
pub(crate) fn assert_ser_string<T>(t: &T, expected_string: &str)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let string = to_string(t).unwrap();
assert_eq!(string, expected_string);
}
/// Assert input and output of (pretty) string serialization are the same.
pub(crate) fn assert_ser_string_pretty<T>(t: &T, expected_string: &str)
where
T: Serialize + std::fmt::Debug + Clone + PartialEq,
{
let string = to_string_pretty(t).unwrap();
assert_eq!(string, expected_string);
}
/// Tests an input JSON string matches an expected type `T`.
fn assert_de<T>(json: &'static str, expected: T)
where
T: DeserializeOwned + std::fmt::Debug + Clone + PartialEq,
{
let t = serde_json::from_str::<T>(json).unwrap();
assert_eq!(t, expected);
}
//---------------------------------------------------------------------------------------------------- Types
// Parameter type.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct GetBlock {
height: u64,
}
// Method enum containing all params.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "method", content = "params")]
#[serde(rename_all = "snake_case")]
enum Methods {
GetBlock(GetBlock),
GetBlockCount,
}
//---------------------------------------------------------------------------------------------------- TESTS
/// Tests that Monero's `get_block` request and response
/// in JSON string form gets correctly (de)serialized.
#[test]
fn monero_jsonrpc_get_block() {
//--- Request
const REQUEST: &str =
r#"{"jsonrpc":"2.0","id":"0","method":"get_block","params":{"height":123}}"#;
let request = Request::new_with_id(
Id::Str("0".into()),
Methods::GetBlock(GetBlock { height: 123 }),
);
assert_ser_string(&request, REQUEST);
assert_de(REQUEST, request);
//--- Response
const RESPONSE: &str = r#"{
"jsonrpc": "2.0",
"id": "0",
"result": {
"blob": "01008cb1c49a0572244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5aaa8f2ba01b70101ff7b08ebcc2202f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d3880897a0215a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc216380dac40902dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd638193380c2d72f02965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee80364038088aca3cf020235a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec9938090cad2c60e023b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf910580e08d84ddcb010258c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af980c0caf384a30202899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce82101351af203765d1679e2a9458ab6737d289e18c49766d41fc31a2bf0fe32dd196200",
"block_header": {
"block_size": 345,
"block_weight": 345,
"cumulative_difficulty": 4646953,
"cumulative_difficulty_top64": 0,
"depth": 3166236,
"difficulty": 51263,
"difficulty_top64": 0,
"hash": "ff617b489e91f5db76f6f2cc9b3f03236e09fb191b4238f4a1e64185a6c28019",
"height": 123,
"long_term_weight": 345,
"major_version": 1,
"miner_tx_hash": "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f",
"minor_version": 0,
"nonce": 3136465066,
"num_txes": 0,
"orphan_status": false,
"pow_hash": "",
"prev_hash": "72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5",
"reward": 17590122566891,
"timestamp": 1397823628,
"wide_cumulative_difficulty": "0x46e829",
"wide_difficulty": "0xc83f"
},
"credits": 0,
"json": "{\n \"major_version\": 1, \n \"minor_version\": 0, \n \"timestamp\": 1397823628, \n \"prev_id\": \"72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5\", \n \"nonce\": 3136465066, \n \"miner_tx\": {\n \"version\": 1, \n \"unlock_time\": 183, \n \"vin\": [ {\n \"gen\": {\n \"height\": 123\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 566891, \n \"target\": {\n \"key\": \"f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d38\"\n }\n }, {\n \"amount\": 2000000, \n \"target\": {\n \"key\": \"15a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc2163\"\n }\n }, {\n \"amount\": 20000000, \n \"target\": {\n \"key\": \"dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd6381933\"\n }\n }, {\n \"amount\": 100000000, \n \"target\": {\n \"key\": \"965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee8036403\"\n }\n }, {\n \"amount\": 90000000000, \n \"target\": {\n \"key\": \"35a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec993\"\n }\n }, {\n \"amount\": 500000000000, \n \"target\": {\n \"key\": \"3b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf9105\"\n }\n }, {\n \"amount\": 7000000000000, \n \"target\": {\n \"key\": \"58c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af9\"\n }\n }, {\n \"amount\": 10000000000000, \n \"target\": {\n \"key\": \"899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce8\"\n }\n }\n ], \n \"extra\": [ 1, 53, 26, 242, 3, 118, 93, 22, 121, 226, 169, 69, 138, 182, 115, 125, 40, 158, 24, 196, 151, 102, 212, 31, 195, 26, 43, 240, 254, 50, 221, 25, 98\n ], \n \"signatures\": [ ]\n }, \n \"tx_hashes\": [ ]\n}",
"miner_tx_hash": "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f",
"status": "OK",
"top_hash": "",
"untrusted": false
}
}"#;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Json {
blob: String,
block_header: BlockHeader,
credits: u64,
json: String,
miner_tx_hash: String,
status: String,
top_hash: String,
untrusted: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct BlockHeader {
block_size: u64,
block_weight: u64,
cumulative_difficulty: u64,
cumulative_difficulty_top64: u64,
depth: u64,
difficulty: u64,
difficulty_top64: u64,
hash: String,
height: u64,
long_term_weight: u64,
major_version: u64,
miner_tx_hash: String,
minor_version: u64,
nonce: u64,
num_txes: u64,
orphan_status: bool,
pow_hash: String,
prev_hash: String,
reward: u64,
timestamp: u64,
wide_cumulative_difficulty: String,
wide_difficulty: String,
}
let payload = Json {
blob: "01008cb1c49a0572244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5aaa8f2ba01b70101ff7b08ebcc2202f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d3880897a0215a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc216380dac40902dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd638193380c2d72f02965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee80364038088aca3cf020235a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec9938090cad2c60e023b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf910580e08d84ddcb010258c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af980c0caf384a30202899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce82101351af203765d1679e2a9458ab6737d289e18c49766d41fc31a2bf0fe32dd196200".into(),
block_header: BlockHeader {
block_size: 345,
block_weight: 345,
cumulative_difficulty: 4646953,
cumulative_difficulty_top64: 0,
depth: 3166236,
difficulty: 51263,
difficulty_top64: 0,
hash: "ff617b489e91f5db76f6f2cc9b3f03236e09fb191b4238f4a1e64185a6c28019".into(),
height: 123,
long_term_weight: 345,
major_version: 1,
miner_tx_hash: "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f".into(),
minor_version: 0,
nonce: 3136465066,
num_txes: 0,
orphan_status: false,
pow_hash: "".into(),
prev_hash: "72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5".into(),
reward: 17590122566891,
timestamp: 1397823628,
wide_cumulative_difficulty: "0x46e829".into(),
wide_difficulty: "0xc83f".into(),
},
credits: 0,
json: "{\n \"major_version\": 1, \n \"minor_version\": 0, \n \"timestamp\": 1397823628, \n \"prev_id\": \"72244e0c8b2b8b99236e10c03eba53685b346aab525eb20b59a459b5935cd5a5\", \n \"nonce\": 3136465066, \n \"miner_tx\": {\n \"version\": 1, \n \"unlock_time\": 183, \n \"vin\": [ {\n \"gen\": {\n \"height\": 123\n }\n }\n ], \n \"vout\": [ {\n \"amount\": 566891, \n \"target\": {\n \"key\": \"f917ac2dc38c0e0735f2c97df4a307a445b32abaf0ad528c385ae11a7e767d38\"\n }\n }, {\n \"amount\": 2000000, \n \"target\": {\n \"key\": \"15a39af4cf4c67136ecc048d9296b4cb7a6be61275a0ef207eb4cbb427cc2163\"\n }\n }, {\n \"amount\": 20000000, \n \"target\": {\n \"key\": \"dabddeaada9f4ed2512f9b9613a7ced79d3996ad5050ca542f31032bd6381933\"\n }\n }, {\n \"amount\": 100000000, \n \"target\": {\n \"key\": \"965651ab4a26264253bb8a4ccb9b33afbc8c8b4f3e331baf50537b8ee8036403\"\n }\n }, {\n \"amount\": 90000000000, \n \"target\": {\n \"key\": \"35a7367536243629560b8a40f104352c89a2d4719e86f54175c2e4e3ecfec993\"\n }\n }, {\n \"amount\": 500000000000, \n \"target\": {\n \"key\": \"3b623a01eace71e9b37d2bfac84f9aafc85dbf62a0f452446c5de0ca50cf9105\"\n }\n }, {\n \"amount\": 7000000000000, \n \"target\": {\n \"key\": \"58c370ee02069943e5440294aeafae29656f9782b0a565d26065bb7af07a6af9\"\n }\n }, {\n \"amount\": 10000000000000, \n \"target\": {\n \"key\": \"899a53eeb05852a912bcbc6fa78e4c85f0b059726b0b8f0753e7aa54fc9d7ce8\"\n }\n }\n ], \n \"extra\": [ 1, 53, 26, 242, 3, 118, 93, 22, 121, 226, 169, 69, 138, 182, 115, 125, 40, 158, 24, 196, 151, 102, 212, 31, 195, 26, 43, 240, 254, 50, 221, 25, 98\n ], \n \"signatures\": [ ]\n }, \n \"tx_hashes\": [ ]\n}".into(),
miner_tx_hash: "054ba33024e72dfe8dafabc8af7c0e070e9aca3a9df44569cfa3c96669f9542f".into(),
status: "OK".into(),
top_hash: "".into(),
untrusted: false
};
let response = Response::ok(Id::Str("0".into()), payload);
assert_ser_string_pretty(&response, RESPONSE);
assert_de(RESPONSE, response);
}
/// Tests that Monero's `get_block_count` request and response
/// in JSON string form gets correctly (de)serialized.
#[test]
fn monero_jsonrpc_get_block_count() {
//--- Request
const REQUEST: &str = r#"{"jsonrpc":"2.0","id":0,"method":"get_block_count"}"#;
let request = Request::new_with_id(Id::Num(0), Methods::GetBlockCount);
assert_ser_string(&request, REQUEST);
assert_de(REQUEST, request);
//--- Response
const RESPONSE: &str = r#"{
"jsonrpc": "2.0",
"id": 0,
"result": {
"count": 3166375,
"status": "OK",
"untrusted": false
}
}"#;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Json {
count: u64,
status: String,
untrusted: bool,
}
let payload = Json {
count: 3166375,
status: "OK".into(),
untrusted: false,
};
let response = Response::ok(Id::Num(0), payload);
assert_ser_string_pretty(&response, RESPONSE);
assert_de(RESPONSE, response);
}

119
rpc/json-rpc/src/version.rs Normal file
View file

@ -0,0 +1,119 @@
//! [`Version`]: JSON-RPC 2.0 version marker.
//---------------------------------------------------------------------------------------------------- Use
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
//---------------------------------------------------------------------------------------------------- Version
/// [Protocol version marker](https://www.jsonrpc.org/specification#compatibility).
///
/// This represents the JSON-RPC version.
///
/// This is an empty marker type that always gets (de)serialized as [`Self::TWO`].
///
/// It is the only valid value for the `jsonrpc` field in the
/// [`Request`](crate::Request) and [`Response`](crate::Request) objects.
///
/// JSON-RPC 2.0 allows for backwards compatibility with `1.0` but this crate
/// (and this type) will not accept that, and will fail in deserialization
/// when encountering anything but [`Self::TWO`].
///
/// # Formatting
/// When using Rust formatting, [`Version`] is formatted as `2.0`.
///
/// When using JSON serialization, `Version` is formatted with quotes indicating
/// it is a JSON string and not a JSON float, i.e. it gets formatted as `"2.0"`, not `2.0`.
///
/// # Example
/// ```rust
/// use json_rpc::Version;
/// use serde_json::{to_string, to_string_pretty, from_str};
///
/// assert_eq!(Version::TWO, "2.0");
/// let version = Version;
///
/// // All debug/display formats are the same.
/// assert_eq!(format!("{version:?}"), Version::TWO);
/// assert_eq!(format!("{version:#?}"), Version::TWO);
/// assert_eq!(format!("{version}"), Version::TWO);
///
/// // JSON serialization will add extra quotes to
/// // indicate it is a string and not a float.
/// assert_eq!(to_string(&Version).unwrap(), "\"2.0\"");
/// assert_eq!(to_string_pretty(&Version).unwrap(), "\"2.0\"");
///
/// // Deserialization only accepts the JSON string "2.0".
/// assert!(from_str::<Version>(&"\"2.0\"").is_ok());
/// // This is JSON float, not a string.
/// assert!(from_str::<Version>(&"2.0").is_err());
///
/// assert!(from_str::<Version>(&"2").is_err());
/// assert!(from_str::<Version>(&"1.0").is_err());
/// assert!(from_str::<Version>(&"20").is_err());
/// assert!(from_str::<Version>(&"two").is_err());
/// assert!(from_str::<Version>(&"2.1").is_err());
/// assert!(from_str::<Version>(&"v2.0").is_err());
/// assert!(from_str::<Version>("").is_err());
/// ```
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version;
impl Version {
/// The string `2.0`.
///
/// Note that this does not have extra quotes to mark
/// that it's a JSON string and not a float.
/// ```rust
/// use json_rpc::Version;
///
/// let string = format!("{}", Version);
/// assert_eq!(string, "2.0");
/// assert_ne!(string, "\"2.0\"");
/// ```
pub const TWO: &'static str = "2.0";
}
//---------------------------------------------------------------------------------------------------- Trait impl
impl Serialize for Version {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(Self::TWO)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"{}"#, Self::TWO)
}
}
impl std::fmt::Debug for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"{}"#, Self::TWO)
}
}
//---------------------------------------------------------------------------------------------------- Serde impl
/// Empty serde visitor for [`Version`].
struct VersionVisitor;
impl Visitor<'_> for VersionVisitor {
type Value = Version;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("Identifier must be the exact string: \"2.0\"")
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
if v == Version::TWO {
Ok(Version)
} else {
Err(Error::invalid_value(serde::de::Unexpected::Str(v), &self))
}
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
d.deserialize_str(VersionVisitor)
}
}