spotify_cli/rpc/
protocol.rs

1//! JSON-RPC 2.0 protocol types
2//!
3//! Implements the JSON-RPC 2.0 specification for request/response/notification.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::io::output::Response;
9
10/// JSON-RPC 2.0 request
11#[derive(Debug, Clone, Deserialize)]
12pub struct RpcRequest {
13    pub jsonrpc: String,
14    pub method: String,
15    #[serde(default)]
16    pub params: Option<Value>,
17    pub id: Option<Value>,
18}
19
20impl RpcRequest {
21    /// Check if this is a notification (no id = no response expected)
22    pub fn is_notification(&self) -> bool {
23        self.id.is_none()
24    }
25}
26
27/// JSON-RPC 2.0 response
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RpcResponse {
30    pub jsonrpc: String,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub result: Option<Value>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub error: Option<RpcError>,
35    pub id: Value,
36}
37
38impl RpcResponse {
39    /// Create a success response
40    pub fn success(id: Value, result: Value) -> Self {
41        Self {
42            jsonrpc: "2.0".to_string(),
43            result: Some(result),
44            error: None,
45            id,
46        }
47    }
48
49    /// Create an error response
50    pub fn error(id: Value, code: i32, message: &str, data: Option<Value>) -> Self {
51        Self {
52            jsonrpc: "2.0".to_string(),
53            result: None,
54            error: Some(RpcError {
55                code,
56                message: message.to_string(),
57                data,
58            }),
59            id,
60        }
61    }
62
63    /// Create from CLI Response type
64    pub fn from_response(id: Value, response: Response) -> Self {
65        match response.status {
66            crate::io::output::Status::Success => {
67                let result = serde_json::json!({
68                    "message": response.message,
69                    "payload": response.payload,
70                });
71                Self::success(id, result)
72            }
73            crate::io::output::Status::Error => {
74                let data = response.error.map(|e| {
75                    serde_json::json!({
76                        "kind": format!("{:?}", e.kind),
77                        "details": e.details,
78                    })
79                });
80                Self::error(id, response.code as i32, &response.message, data)
81            }
82        }
83    }
84}
85
86/// JSON-RPC 2.0 error object
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct RpcError {
89    pub code: i32,
90    pub message: String,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub data: Option<Value>,
93}
94
95/// JSON-RPC 2.0 notification (server → client, no id)
96#[derive(Debug, Clone, Serialize)]
97pub struct RpcNotification {
98    pub jsonrpc: String,
99    pub method: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub params: Option<Value>,
102}
103
104impl RpcNotification {
105    pub fn new(method: &str, params: Option<Value>) -> Self {
106        Self {
107            jsonrpc: "2.0".to_string(),
108            method: method.to_string(),
109            params,
110        }
111    }
112}
113
114/// Standard JSON-RPC error codes
115pub mod error_codes {
116    pub const PARSE_ERROR: i32 = -32700;
117    pub const INVALID_REQUEST: i32 = -32600;
118    pub const METHOD_NOT_FOUND: i32 = -32601;
119    pub const INVALID_PARAMS: i32 = -32602;
120    pub const INTERNAL_ERROR: i32 = -32603;
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn parse_request_with_params() {
129        let json = r#"{"jsonrpc": "2.0", "method": "player.play", "params": {"uri": "spotify:track:123"}, "id": 1}"#;
130        let req: RpcRequest = serde_json::from_str(json).unwrap();
131        assert_eq!(req.method, "player.play");
132        assert!(req.params.is_some());
133        assert!(!req.is_notification());
134    }
135
136    #[test]
137    fn parse_notification() {
138        let json = r#"{"jsonrpc": "2.0", "method": "player.next"}"#;
139        let req: RpcRequest = serde_json::from_str(json).unwrap();
140        assert!(req.is_notification());
141    }
142
143    #[test]
144    fn serialize_success_response() {
145        let resp = RpcResponse::success(serde_json::json!(1), serde_json::json!({"status": "ok"}));
146        let json = serde_json::to_string(&resp).unwrap();
147        assert!(json.contains("result"));
148        assert!(!json.contains("error"));
149    }
150
151    #[test]
152    fn serialize_error_response() {
153        let resp = RpcResponse::error(serde_json::json!(1), -32601, "Method not found", None);
154        let json = serde_json::to_string(&resp).unwrap();
155        assert!(json.contains("error"));
156        assert!(!json.contains("result"));
157    }
158
159    #[test]
160    fn serialize_notification() {
161        let notif = RpcNotification::new(
162            "event.trackChanged",
163            Some(serde_json::json!({"track": "test"})),
164        );
165        let json = serde_json::to_string(&notif).unwrap();
166        assert!(json.contains("event.trackChanged"));
167        assert!(!json.contains("id"));
168    }
169}