Skip to main content

frust_devtools_protocol/
types.rs

1//! JSON-RPC 2.0 envelope types: [`Request`], [`Response`]/[`ResponseOutcome`],
2//! [`Notification`], [`RpcError`], and the decoded-line discriminator
3//! [`Incoming`]. Method-specific payloads (handshake info, widget tree,
4//! frame stats, …) live in [`crate::messages`].
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// The JSON-RPC 2.0 `"jsonrpc"` version literal every envelope carries.
10pub const JSONRPC_VERSION: &str = "2.0";
11
12fn jsonrpc_version() -> String {
13    JSONRPC_VERSION.to_string()
14}
15
16/// A client→server call awaiting a [`Response`] correlated by `id`.
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct Request {
19    #[serde(default = "jsonrpc_version")]
20    pub jsonrpc: String,
21    pub id: u64,
22    pub method: String,
23    /// Absent on the wire deserializes as `Value::Null`, matching a
24    /// parameterless method (e.g. `handshake`) — never a decode error.
25    #[serde(default)]
26    pub params: Value,
27}
28
29impl Request {
30    pub fn new(id: u64, method: impl Into<String>, params: Value) -> Self {
31        Self {
32            jsonrpc: jsonrpc_version(),
33            id,
34            method: method.into(),
35            params,
36        }
37    }
38}
39
40/// A server→client push carrying no `id` and expecting no [`Response`].
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct Notification {
43    #[serde(default = "jsonrpc_version")]
44    pub jsonrpc: String,
45    pub method: String,
46    #[serde(default)]
47    pub params: Value,
48}
49
50impl Notification {
51    pub fn new(method: impl Into<String>, params: Value) -> Self {
52        Self {
53            jsonrpc: jsonrpc_version(),
54            method: method.into(),
55            params,
56        }
57    }
58}
59
60/// The reply to a [`Request`], correlated by `id`. Exactly one of `result`/
61/// `error` is present on the wire (JSON-RPC 2.0 §5) — modeled as the
62/// flattened [`ResponseOutcome`] rather than two `Option` fields so an
63/// invalid "both present"/"neither present" shape can't be constructed.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct Response {
66    #[serde(default = "jsonrpc_version")]
67    pub jsonrpc: String,
68    pub id: u64,
69    #[serde(flatten)]
70    pub outcome: ResponseOutcome,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum ResponseOutcome {
76    Success { result: Value },
77    Error { error: RpcError },
78}
79
80impl Response {
81    pub fn success(id: u64, result: Value) -> Self {
82        Self {
83            jsonrpc: jsonrpc_version(),
84            id,
85            outcome: ResponseOutcome::Success { result },
86        }
87    }
88
89    pub fn error(id: u64, error: RpcError) -> Self {
90        Self {
91            jsonrpc: jsonrpc_version(),
92            id,
93            outcome: ResponseOutcome::Error { error },
94        }
95    }
96}
97
98/// A JSON-RPC 2.0 error object (§5.1), plus a small implementation-defined
99/// custom range this protocol occupies for its own conditions.
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct RpcError {
102    pub code: i32,
103    pub message: String,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub data: Option<Value>,
106}
107
108impl RpcError {
109    // Standard JSON-RPC 2.0 codes (§5.1).
110    pub const PARSE_ERROR: i32 = -32700;
111    pub const INVALID_REQUEST: i32 = -32600;
112    pub const METHOD_NOT_FOUND: i32 = -32601;
113    pub const INVALID_PARAMS: i32 = -32602;
114    pub const INTERNAL_ERROR: i32 = -32603;
115
116    /// JSON-RPC 2.0 reserves `-32000..=-32099` for implementation-defined
117    /// server errors (§5.1). `NOT_SUPPORTED` is the first occupant — a
118    /// capability-gated method (e.g. `screenshot`) rejects with it on a
119    /// server whose handshake declared no matching [`crate::Capability`].
120    pub const NOT_SUPPORTED: i32 = -32000;
121
122    /// The second occupant of the implementation-defined range: the
123    /// connection has not presented the server's per-process token
124    /// ([`crate::HandshakeParams`]), so no method but `handshake` is
125    /// dispatched. A client that receives this has the wrong token, or none —
126    /// it must re-read the discovery line, never retry blindly.
127    pub const UNAUTHORIZED: i32 = -32001;
128
129    pub fn new(code: i32, message: impl Into<String>) -> Self {
130        Self {
131            code,
132            message: message.into(),
133            data: None,
134        }
135    }
136
137    pub fn not_supported(message: impl Into<String>) -> Self {
138        Self::new(Self::NOT_SUPPORTED, message)
139    }
140
141    pub fn unauthorized(message: impl Into<String>) -> Self {
142        Self::new(Self::UNAUTHORIZED, message)
143    }
144}
145
146/// A decoded line's discriminated shape — [`crate::decode_line`]'s return
147/// type.
148#[derive(Debug, Clone, PartialEq)]
149pub enum Incoming {
150    Request(Request),
151    Response(Response),
152    Notification(Notification),
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn request_round_trips_through_json() {
161        let req = Request::new(7, "handshake", Value::Null);
162        let json = serde_json::to_string(&req).unwrap();
163        let back: Request = serde_json::from_str(&json).unwrap();
164        assert_eq!(req, back);
165    }
166
167    #[test]
168    fn request_missing_jsonrpc_field_still_decodes() {
169        let line = r#"{"id":1,"method":"handshake","params":null}"#;
170        let req: Request = serde_json::from_str(line).unwrap();
171        assert_eq!(req.jsonrpc, JSONRPC_VERSION);
172    }
173
174    #[test]
175    fn response_success_round_trips_and_omits_error_key() {
176        let resp = Response::success(3, serde_json::json!({"ok": true}));
177        let json = serde_json::to_string(&resp).unwrap();
178        assert!(json.contains("\"result\""));
179        assert!(!json.contains("\"error\""));
180        let back: Response = serde_json::from_str(&json).unwrap();
181        assert_eq!(resp, back);
182    }
183
184    #[test]
185    fn response_error_round_trips_and_omits_result_key() {
186        let resp = Response::error(3, RpcError::not_supported("screenshot disabled"));
187        let json = serde_json::to_string(&resp).unwrap();
188        assert!(json.contains("\"error\""));
189        assert!(!json.contains("\"result\""));
190        let back: Response = serde_json::from_str(&json).unwrap();
191        assert_eq!(resp, back);
192        match back.outcome {
193            ResponseOutcome::Error { error } => {
194                assert_eq!(error.code, RpcError::NOT_SUPPORTED);
195            }
196            ResponseOutcome::Success { .. } => panic!("expected an error outcome"),
197        }
198    }
199
200    #[test]
201    fn the_custom_error_codes_stay_inside_the_reserved_range() {
202        // JSON-RPC 2.0 §5.1 reserves -32000..=-32099 for implementation-
203        // defined server errors; drifting outside it would collide with a
204        // standard code.
205        for code in [RpcError::NOT_SUPPORTED, RpcError::UNAUTHORIZED] {
206            assert!((-32099..=-32000).contains(&code), "out of range: {code}");
207        }
208        assert_ne!(RpcError::NOT_SUPPORTED, RpcError::UNAUTHORIZED);
209        assert_eq!(RpcError::unauthorized("nope").code, RpcError::UNAUTHORIZED);
210    }
211
212    #[test]
213    fn rpc_error_data_field_omitted_when_none() {
214        let err = RpcError::new(RpcError::INTERNAL_ERROR, "boom");
215        let json = serde_json::to_string(&err).unwrap();
216        assert!(!json.contains("\"data\""));
217    }
218
219    #[test]
220    fn unknown_fields_are_tolerated() {
221        // No `deny_unknown_fields` anywhere in this crate — a newer client
222        // or server may add a field a peer running an older crate version
223        // doesn't know about; decoding must not fail on it.
224        let line =
225            r#"{"jsonrpc":"2.0","id":1,"method":"handshake","params":null,"extra":"future field"}"#;
226        let req: Request = serde_json::from_str(line).unwrap();
227        assert_eq!(req.method, "handshake");
228    }
229}