mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 11:39:26 +00:00
rpc: implement json-rpc
crate (#148)
Some checks failed
Audit / audit (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / typo (push) Has been cancelled
CI / ci (macos-latest, stable, bash) (push) Has been cancelled
CI / ci (ubuntu-latest, stable, bash) (push) Has been cancelled
CI / ci (windows-latest, stable-x86_64-pc-windows-gnu, msys2 {0}) (push) Has been cancelled
Deny / audit (push) Has been cancelled
Some checks failed
Audit / audit (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / typo (push) Has been cancelled
CI / ci (macos-latest, stable, bash) (push) Has been cancelled
CI / ci (ubuntu-latest, stable, bash) (push) Has been cancelled
CI / ci (windows-latest, stable-x86_64-pc-windows-gnu, msys2 {0}) (push) Has been cancelled
Deny / audit (push) Has been cancelled
* 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:
parent
663c852b13
commit
a3e34c3ba8
13 changed files with 2414 additions and 135 deletions
275
Cargo.lock
generated
275
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
197
rpc/json-rpc/README.md
Normal 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");
|
||||
```
|
219
rpc/json-rpc/src/error/code.rs
Normal file
219
rpc/json-rpc/src/error/code.rs
Normal 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())
|
||||
}
|
||||
}
|
22
rpc/json-rpc/src/error/constants.rs
Normal file
22
rpc/json-rpc/src/error/constants.rs
Normal 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";
|
14
rpc/json-rpc/src/error/mod.rs
Normal file
14
rpc/json-rpc/src/error/mod.rs
Normal 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;
|
258
rpc/json-rpc/src/error/object.rs
Normal file
258
rpc/json-rpc/src/error/object.rs
Normal 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
242
rpc/json-rpc/src/id.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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
354
rpc/json-rpc/src/request.rs
Normal 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);
|
||||
}
|
||||
}
|
485
rpc/json-rpc/src/response.rs
Normal file
485
rpc/json-rpc/src/response.rs
Normal 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
250
rpc/json-rpc/src/tests.rs
Normal 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
119
rpc/json-rpc/src/version.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue