Skip to main content

zeph_a2a/
jsonrpc.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5
6use crate::types::Message;
7
8pub const METHOD_SEND_MESSAGE: &str = "message/send";
9pub const METHOD_SEND_STREAMING_MESSAGE: &str = "message/stream";
10pub const METHOD_GET_TASK: &str = "tasks/get";
11pub const METHOD_CANCEL_TASK: &str = "tasks/cancel";
12
13pub const ERR_TASK_NOT_FOUND: i32 = -32001;
14pub const ERR_TASK_NOT_CANCELABLE: i32 = -32002;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct JsonRpcRequest<P> {
18    pub jsonrpc: String,
19    pub id: serde_json::Value,
20    pub method: String,
21    pub params: P,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(bound(deserialize = "R: Deserialize<'de>"))]
26pub struct JsonRpcResponse<R> {
27    pub jsonrpc: String,
28    pub id: serde_json::Value,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub result: Option<R>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub error: Option<JsonRpcError>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct JsonRpcError {
37    pub code: i32,
38    pub message: String,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub data: Option<serde_json::Value>,
41}
42
43impl std::fmt::Display for JsonRpcError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "JSON-RPC error {}: {}", self.code, self.message)
46    }
47}
48
49impl std::error::Error for JsonRpcError {}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct SendMessageParams {
54    pub message: Message,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub configuration: Option<TaskConfiguration>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct TaskConfiguration {
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub blocking: Option<bool>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct TaskIdParams {
69    pub id: String,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub history_length: Option<u32>,
72}
73
74impl<P: Serialize> JsonRpcRequest<P> {
75    #[must_use]
76    pub fn new(method: &str, params: P) -> Self {
77        Self {
78            jsonrpc: "2.0".into(),
79            id: serde_json::Value::String(uuid::Uuid::new_v4().to_string()),
80            method: method.into(),
81            params,
82        }
83    }
84}
85
86impl<R: DeserializeOwned> JsonRpcResponse<R> {
87    /// # Errors
88    /// Returns `JsonRpcError` if the response contains an error or no result.
89    pub fn into_result(self) -> Result<R, JsonRpcError> {
90        if let Some(err) = self.error {
91            return Err(err);
92        }
93        self.result.ok_or_else(|| JsonRpcError {
94            code: -32603,
95            message: "response contains neither result nor error".into(),
96            data: None,
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn request_new_sets_jsonrpc_and_uuid_id() {
107        let req = JsonRpcRequest::new(
108            METHOD_SEND_MESSAGE,
109            TaskIdParams {
110                id: "task-1".into(),
111                history_length: None,
112            },
113        );
114        assert_eq!(req.jsonrpc, "2.0");
115        assert_eq!(req.method, "message/send");
116        let id_str = req.id.as_str().unwrap();
117        assert!(uuid::Uuid::parse_str(id_str).is_ok());
118    }
119
120    #[test]
121    fn request_serde_round_trip() {
122        let req = JsonRpcRequest::new(
123            METHOD_GET_TASK,
124            TaskIdParams {
125                id: "t-1".into(),
126                history_length: Some(10),
127            },
128        );
129        let json = serde_json::to_string(&req).unwrap();
130        let back: JsonRpcRequest<TaskIdParams> = serde_json::from_str(&json).unwrap();
131        assert_eq!(back.method, METHOD_GET_TASK);
132        assert_eq!(back.params.id, "t-1");
133        assert_eq!(back.params.history_length, Some(10));
134    }
135
136    #[test]
137    fn response_into_result_ok() {
138        let resp = JsonRpcResponse {
139            jsonrpc: "2.0".into(),
140            id: serde_json::Value::String("1".into()),
141            result: Some(serde_json::json!({"id": "task-1"})),
142            error: None,
143        };
144        let val: serde_json::Value = resp.into_result().unwrap();
145        assert_eq!(val["id"], "task-1");
146    }
147
148    #[test]
149    fn response_into_result_error() {
150        let resp: JsonRpcResponse<serde_json::Value> = JsonRpcResponse {
151            jsonrpc: "2.0".into(),
152            id: serde_json::Value::String("1".into()),
153            result: None,
154            error: Some(JsonRpcError {
155                code: ERR_TASK_NOT_FOUND,
156                message: "task not found".into(),
157                data: None,
158            }),
159        };
160        let err = resp.into_result().unwrap_err();
161        assert_eq!(err.code, ERR_TASK_NOT_FOUND);
162    }
163
164    #[test]
165    fn response_into_result_neither() {
166        let resp: JsonRpcResponse<serde_json::Value> = JsonRpcResponse {
167            jsonrpc: "2.0".into(),
168            id: serde_json::Value::String("1".into()),
169            result: None,
170            error: None,
171        };
172        let err = resp.into_result().unwrap_err();
173        assert_eq!(err.code, -32603);
174    }
175
176    #[test]
177    fn send_message_params_serde() {
178        let params = SendMessageParams {
179            message: Message::user_text("hello"),
180            configuration: Some(TaskConfiguration {
181                blocking: Some(true),
182            }),
183        };
184        let json = serde_json::to_string(&params).unwrap();
185        let back: SendMessageParams = serde_json::from_str(&json).unwrap();
186        assert_eq!(back.message.text_content(), Some("hello"));
187        assert_eq!(back.configuration.unwrap().blocking, Some(true));
188    }
189
190    #[test]
191    fn task_id_params_skips_none() {
192        let params = TaskIdParams {
193            id: "t-1".into(),
194            history_length: None,
195        };
196        let json = serde_json::to_string(&params).unwrap();
197        assert!(!json.contains("historyLength"));
198    }
199
200    #[test]
201    fn jsonrpc_error_display() {
202        let err = JsonRpcError {
203            code: -32001,
204            message: "not found".into(),
205            data: None,
206        };
207        assert_eq!(err.to_string(), "JSON-RPC error -32001: not found");
208    }
209}