dig_rpc_types/envelope.rs
1//! JSON-RPC 2.0 envelope types.
2//!
3//! The DIG-node RPC wire is strict [JSON-RPC 2.0](https://www.jsonrpc.org/specification):
4//! every request carries `jsonrpc: "2.0"`, an `id`, a `method`, and optional
5//! `params`; every response carries `jsonrpc`, the echoed `id`, and exactly one
6//! of `result` or `error`.
7//!
8//! # The [`Version`] marker
9//!
10//! [`Version`] is zero-sized and (de)serializes as the literal string `"2.0"`.
11//! A response whose `jsonrpc` field is anything else fails to deserialize into
12//! [`JsonRpcResponse`] with no hand-written check — the version guard is
13//! structural.
14//!
15//! # The error envelope
16//!
17//! Errors follow the canonical DIG envelope
18//! `{"code": <int>, "message": <str>, "data": {"code": <UPPER_SNAKE>, "origin": <origin>}}`.
19//! See [`crate::error`] for the [`RpcError`] type and the single constructor
20//! helper that mints it.
21
22use serde::{Deserialize, Serialize};
23
24use crate::error::RpcError;
25
26/// A JSON-RPC 2.0 request envelope.
27///
28/// `P` is the method-specific parameter type. Concrete callers use a
29/// `{Method}Params` struct from [`crate::types`]; generic callers (the server
30/// dispatch, proxies, middleware) use `serde_json::Value`.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct JsonRpcRequest<P = serde_json::Value> {
33 /// Protocol version; serializes to the literal `"2.0"`.
34 pub jsonrpc: Version,
35 /// Correlation id. Echoed unchanged in the response.
36 pub id: RequestId,
37 /// The method name (e.g. `"dig.getContent"`).
38 pub method: String,
39 /// Method-specific parameters. Absent for methods that take none.
40 #[serde(skip_serializing_if = "Option::is_none", default)]
41 pub params: Option<P>,
42}
43
44impl<P> JsonRpcRequest<P> {
45 /// Build a request with a numeric id and the given method + params.
46 pub fn new(id: u64, method: impl Into<String>, params: P) -> Self {
47 Self {
48 jsonrpc: Version,
49 id: RequestId::Num(id),
50 method: method.into(),
51 params: Some(params),
52 }
53 }
54}
55
56/// A JSON-RPC 2.0 response envelope.
57///
58/// Exactly one of `result` or `error` is present — enforced structurally by the
59/// untagged [`JsonRpcResponseBody`].
60///
61/// `R` is the method-specific result type; generic callers use
62/// `serde_json::Value`.
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct JsonRpcResponse<R = serde_json::Value> {
65 /// Protocol version; serializes to the literal `"2.0"`.
66 pub jsonrpc: Version,
67 /// Correlation id copied from the request.
68 pub id: RequestId,
69 /// Either `result` or `error`, never both, never neither.
70 #[serde(flatten)]
71 pub body: JsonRpcResponseBody<R>,
72}
73
74impl<R> JsonRpcResponse<R> {
75 /// Build a success response echoing `id`.
76 pub fn success(id: RequestId, result: R) -> Self {
77 Self {
78 jsonrpc: Version,
79 id,
80 body: JsonRpcResponseBody::Success { result },
81 }
82 }
83
84 /// Build an error response echoing `id`.
85 pub fn error(id: RequestId, error: RpcError) -> Self {
86 Self {
87 jsonrpc: Version,
88 id,
89 body: JsonRpcResponseBody::Error { error },
90 }
91 }
92
93 /// The error, if this is an error response.
94 pub fn as_error(&self) -> Option<&RpcError> {
95 match &self.body {
96 JsonRpcResponseBody::Error { error } => Some(error),
97 JsonRpcResponseBody::Success { .. } => None,
98 }
99 }
100
101 /// The result, if this is a success response.
102 pub fn as_result(&self) -> Option<&R> {
103 match &self.body {
104 JsonRpcResponseBody::Success { result } => Some(result),
105 JsonRpcResponseBody::Error { .. } => None,
106 }
107 }
108}
109
110/// The body of a response: either a successful result or an error.
111///
112/// `#[serde(untagged)]` keeps the JSON flat — `{"result": …}` or
113/// `{"error": …}` — as the spec requires.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(untagged)]
116pub enum JsonRpcResponseBody<R> {
117 /// The method succeeded; `result` carries the response.
118 Success {
119 /// The method-specific result payload.
120 result: R,
121 },
122 /// The method failed; `error` carries the canonical error envelope.
123 Error {
124 /// The error object.
125 error: RpcError,
126 },
127}
128
129/// Per-request correlation id.
130///
131/// JSON-RPC 2.0 permits numeric, string, or null ids; a server echoes the id
132/// unchanged. Null is reserved for notifications (DIG RPC does not use them but
133/// accepts the variant for spec compliance).
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
135#[serde(untagged)]
136pub enum RequestId {
137 /// Numeric id (the typical choice).
138 Num(u64),
139 /// String id (useful for UUID correlation).
140 Str(String),
141 /// Null id (spec-allowed for notifications).
142 Null,
143}
144
145impl Default for RequestId {
146 fn default() -> Self {
147 RequestId::Num(1)
148 }
149}
150
151/// The JSON-RPC protocol version marker.
152///
153/// Zero-sized; (de)serializes as the literal string `"2.0"`. Any other value
154/// fails deserialization with no manual validation required.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct Version;
157
158impl Serialize for Version {
159 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
160 s.serialize_str("2.0")
161 }
162}
163
164impl<'de> Deserialize<'de> for Version {
165 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
166 let s = String::deserialize(d)?;
167 if s == "2.0" {
168 Ok(Version)
169 } else {
170 Err(serde::de::Error::custom(format!(
171 "expected jsonrpc version \"2.0\", got {s:?}"
172 )))
173 }
174 }
175}
176
177impl Default for Version {
178 fn default() -> Self {
179 Self
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::error::{ErrorCode, ErrorOrigin};
187
188 /// **Proves:** `Version` serializes to the literal `"2.0"`.
189 /// **Catches:** a `#[derive(Serialize)]` regression that would emit `null`
190 /// for a unit struct, or a swap to a typed wrapper.
191 #[test]
192 fn version_serialises_as_two_point_zero() {
193 assert_eq!(serde_json::to_string(&Version).unwrap(), "\"2.0\"");
194 }
195
196 /// **Proves:** a non-`"2.0"` jsonrpc field is rejected.
197 /// **Catches:** a `Version::deserialize` that accepts any string.
198 #[test]
199 fn version_rejects_non_2_0() {
200 assert!(serde_json::from_str::<Version>(r#""1.0""#).is_err());
201 }
202
203 /// **Proves:** `RequestId` round-trips all three variants untagged.
204 /// **Catches:** loss of `#[serde(untagged)]` (which would force
205 /// `{"Num": 42}` JSON).
206 #[test]
207 fn request_id_roundtrip() {
208 for (rid, expected) in [
209 (RequestId::Num(42), "42"),
210 (RequestId::Str("abc".into()), "\"abc\""),
211 (RequestId::Null, "null"),
212 ] {
213 let s = serde_json::to_string(&rid).unwrap();
214 assert_eq!(s, expected);
215 assert_eq!(serde_json::from_str::<RequestId>(&s).unwrap(), rid);
216 }
217 }
218
219 /// **Proves:** success serializes as `{"result": …}` and error as
220 /// `{"error": …}`, never both.
221 /// **Catches:** dropping `#[serde(untagged)]` from the response body.
222 #[test]
223 fn response_body_success_and_error_shape() {
224 let ok = JsonRpcResponse::success(RequestId::Num(1), serde_json::json!({"ok": true}));
225 let s = serde_json::to_string(&ok).unwrap();
226 assert!(s.contains("\"result\""), "{s}");
227 assert!(!s.contains("\"error\""), "{s}");
228
229 let err = JsonRpcResponse::<serde_json::Value>::error(
230 RequestId::Num(2),
231 RpcError::new(
232 ErrorCode::MethodNotFound,
233 "no such method",
234 ErrorOrigin::Node,
235 ),
236 );
237 let s = serde_json::to_string(&err).unwrap();
238 assert!(s.contains("\"error\""), "{s}");
239 assert!(!s.contains("\"result\""), "{s}");
240 }
241
242 /// **Proves:** a request without `params` deserializes cleanly.
243 /// **Catches:** dropping the `Option<P>`/`skip_serializing_if` on `params`.
244 #[test]
245 fn request_without_params_deserialises() {
246 let raw = r#"{"jsonrpc":"2.0","id":1,"method":"dig.health"}"#;
247 let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
248 assert_eq!(req.method, "dig.health");
249 assert!(req.params.is_none());
250 }
251
252 /// **Proves:** the `as_error` / `as_result` accessors branch correctly.
253 #[test]
254 fn accessors_branch() {
255 let ok = JsonRpcResponse::success(RequestId::Num(1), 7u32);
256 assert_eq!(ok.as_result(), Some(&7));
257 assert!(ok.as_error().is_none());
258 }
259}