Skip to main content

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}