1use serde::Deserialize;
2use serde::Serialize;
3use serde_json::Value;
4use serde_json::json;
5
6use crate::ipc::error_codes;
7
8#[derive(Debug, Deserialize)]
9pub struct RpcRequest {
10 #[allow(dead_code)]
11 jsonrpc: String,
12 pub id: u64,
13 pub method: String,
14 #[serde(default)]
15 pub params: Option<Value>,
16}
17
18impl RpcRequest {
19 pub fn new(id: u64, method: String, params: Option<Value>) -> Self {
21 Self {
22 jsonrpc: "2.0".to_string(),
23 id,
24 method,
25 params,
26 }
27 }
28
29 pub fn param_str(&self, key: &str) -> Option<&str> {
30 self.params
31 .as_ref()
32 .and_then(|p| p.get(key))
33 .and_then(|v| v.as_str())
34 }
35
36 pub fn param_bool_opt(&self, key: &str) -> Option<bool> {
37 self.params.as_ref()?.get(key)?.as_bool()
38 }
39
40 pub fn param_bool(&self, key: &str, default: bool) -> bool {
41 self.param_bool_opt(key).unwrap_or(default)
42 }
43
44 pub fn param_array(&self, key: &str) -> Option<&Vec<Value>> {
45 self.params.as_ref()?.get(key)?.as_array()
46 }
47
48 pub fn param_u64_opt(&self, key: &str) -> Option<u64> {
49 self.params
50 .as_ref()
51 .and_then(|p| p.get(key))
52 .and_then(|v| v.as_u64())
53 }
54
55 pub fn param_u64(&self, key: &str, default: u64) -> u64 {
56 self.param_u64_opt(key).unwrap_or(default)
57 }
58
59 pub fn param_u16(&self, key: &str, default: u16) -> u16 {
60 self.param_u64(key, default as u64) as u16
61 }
62
63 pub fn param_i32(&self, key: &str, default: i32) -> i32 {
64 self.params
65 .as_ref()
66 .and_then(|p| p.get(key))
67 .and_then(|v| v.as_i64())
68 .map(|n| n as i32)
69 .unwrap_or(default)
70 }
71
72 #[allow(clippy::result_large_err)]
73 pub fn require_str(&self, key: &str) -> Result<&str, RpcResponse> {
74 self.param_str(key)
75 .ok_or_else(|| RpcResponse::error(self.id, -32602, &format!("Missing '{}' param", key)))
76 }
77
78 #[allow(clippy::result_large_err)]
79 pub fn require_array(&self, key: &str) -> Result<&Vec<Value>, RpcResponse> {
80 self.param_array(key)
81 .ok_or_else(|| RpcResponse::error(self.id, -32602, &format!("Missing '{}' param", key)))
82 }
83}
84
85#[derive(Debug, Serialize)]
86pub struct RpcResponse {
87 jsonrpc: String,
88 id: u64,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 result: Option<Value>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 error: Option<RpcServerError>,
93}
94
95#[derive(Debug, Serialize)]
96pub struct RpcServerError {
97 code: i32,
98 message: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 data: Option<Value>,
101}
102
103#[derive(Debug, Serialize)]
111pub struct ErrorData {
112 pub category: String,
114 pub retryable: bool,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub context: Option<Value>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub suggestion: Option<String>,
122}
123
124impl RpcResponse {
125 pub fn success(id: u64, result: Value) -> Self {
126 Self {
127 jsonrpc: "2.0".to_string(),
128 id,
129 result: Some(result),
130 error: None,
131 }
132 }
133
134 pub fn error(id: u64, code: i32, message: &str) -> Self {
135 Self {
136 jsonrpc: "2.0".to_string(),
137 id,
138 result: None,
139 error: Some(RpcServerError {
140 code,
141 message: message.to_string(),
142 data: None,
143 }),
144 }
145 }
146
147 pub fn error_with_context(id: u64, code: i32, message: &str, session_id: Option<&str>) -> Self {
148 let data = session_id.map(|sid| json!({ "session_id": sid }));
149 Self {
150 jsonrpc: "2.0".to_string(),
151 id,
152 result: None,
153 error: Some(RpcServerError {
154 code,
155 message: message.to_string(),
156 data,
157 }),
158 }
159 }
160
161 pub fn error_with_data(id: u64, code: i32, message: &str, error_data: ErrorData) -> Self {
166 Self {
167 jsonrpc: "2.0".to_string(),
168 id,
169 result: None,
170 error: Some(RpcServerError {
171 code,
172 message: message.to_string(),
173 data: Some(serde_json::to_value(error_data).unwrap_or(json!({}))),
174 }),
175 }
176 }
177
178 pub fn domain_error(
185 id: u64,
186 code: i32,
187 message: &str,
188 category: &str,
189 context: Option<Value>,
190 suggestion: Option<String>,
191 ) -> Self {
192 Self::error_with_data(
193 id,
194 code,
195 message,
196 ErrorData {
197 category: category.to_string(),
198 retryable: error_codes::is_retryable(code),
199 context,
200 suggestion,
201 },
202 )
203 }
204
205 pub fn action_success(id: u64) -> Self {
206 Self::success(id, json!({ "success": true }))
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn make_request(params: Option<Value>) -> RpcRequest {
215 RpcRequest {
216 jsonrpc: "2.0".to_string(),
217 id: 1,
218 method: "test".to_string(),
219 params,
220 }
221 }
222
223 #[test]
224 fn test_param_str_extracts_string() {
225 let req = make_request(Some(json!({"name": "test-value"})));
226 assert_eq!(req.param_str("name"), Some("test-value"));
227 }
228
229 #[test]
230 fn test_param_str_returns_none_for_missing_key() {
231 let req = make_request(Some(json!({"other": "value"})));
232 assert_eq!(req.param_str("name"), None);
233 }
234
235 #[test]
236 fn test_param_bool_opt_extracts_boolean() {
237 let req = make_request(Some(json!({"enabled": true, "disabled": false})));
238 assert_eq!(req.param_bool_opt("enabled"), Some(true));
239 assert_eq!(req.param_bool_opt("disabled"), Some(false));
240 }
241
242 #[test]
243 fn test_param_bool_with_default() {
244 let req = make_request(Some(json!({"enabled": true})));
245 assert!(req.param_bool("enabled", false));
246 assert!(!req.param_bool("missing", false));
247 }
248
249 #[test]
250 fn test_param_array_extracts_array() {
251 let req = make_request(Some(json!({"items": ["a", "b", "c"]})));
252 let arr = req.param_array("items").unwrap();
253 assert_eq!(arr.len(), 3);
254 }
255
256 #[test]
257 fn test_param_u64_extracts_number() {
258 let req = make_request(Some(json!({"timeout": 5000})));
259 assert_eq!(req.param_u64("timeout", 0), 5000);
260 }
261
262 #[test]
263 fn test_param_u64_returns_default_for_missing() {
264 let req = make_request(Some(json!({})));
265 assert_eq!(req.param_u64("timeout", 30000), 30000);
266 }
267
268 #[test]
269 fn test_response_success_format() {
270 let resp = RpcResponse::success(42, json!({"data": "test"}));
271 let json = serde_json::to_string(&resp).unwrap();
272 assert!(json.contains("\"jsonrpc\":\"2.0\""));
273 assert!(json.contains("\"id\":42"));
274 assert!(json.contains("\"result\""));
275 assert!(!json.contains("\"error\""));
276 }
277
278 #[test]
279 fn test_response_error_format() {
280 let resp = RpcResponse::error(99, -32600, "Invalid Request");
281 let json = serde_json::to_string(&resp).unwrap();
282 assert!(json.contains("\"error\""));
283 assert!(json.contains("\"code\":-32600"));
284 assert!(!json.contains("\"result\""));
285 }
286
287 #[test]
288 fn test_action_success_shorthand() {
289 let resp = RpcResponse::action_success(1);
290 let json = serde_json::to_string(&resp).unwrap();
291 assert!(json.contains("\"success\":true"));
292 }
293
294 #[test]
295 fn test_error_with_data_includes_structured_error() {
296 let error_data = ErrorData {
297 category: "not_found".to_string(),
298 retryable: false,
299 context: Some(json!({"element_ref": "@btn1"})),
300 suggestion: Some("Run 'snapshot -i' to see elements.".to_string()),
301 };
302 let resp = RpcResponse::error_with_data(42, -32003, "Element not found", error_data);
303 let json_str = serde_json::to_string(&resp).unwrap();
304 let parsed: Value = serde_json::from_str(&json_str).unwrap();
305
306 assert_eq!(parsed["error"]["code"], -32003);
307 assert_eq!(parsed["error"]["data"]["category"], "not_found");
308 assert_eq!(parsed["error"]["data"]["retryable"], false);
309 assert_eq!(parsed["error"]["data"]["context"]["element_ref"], "@btn1");
310 }
311
312 #[test]
313 fn test_domain_error_sets_retryable_for_lock_timeout() {
314 let resp = RpcResponse::domain_error(
315 1,
316 -32007, "Lock timeout",
318 "busy",
319 None,
320 Some("Try again".to_string()),
321 );
322 let json_str = serde_json::to_string(&resp).unwrap();
323 let parsed: Value = serde_json::from_str(&json_str).unwrap();
324
325 assert_eq!(parsed["error"]["data"]["retryable"], true);
326 }
327
328 #[test]
329 fn test_domain_error_not_retryable_for_element_not_found() {
330 let resp = RpcResponse::domain_error(
331 1,
332 -32003, "Element not found",
334 "not_found",
335 Some(json!({"element_ref": "@btn1"})),
336 None,
337 );
338 let json_str = serde_json::to_string(&resp).unwrap();
339 let parsed: Value = serde_json::from_str(&json_str).unwrap();
340
341 assert_eq!(parsed["error"]["data"]["retryable"], false);
342 }
343}