Skip to main content

mika_a2a/
jsonrpc.rs

1use serde::{Deserialize, Serialize};
2
3// Standard JSON-RPC 2.0 error codes
4pub const PARSE_ERROR: i32 = -32700;
5pub const INVALID_REQUEST: i32 = -32600;
6pub const METHOD_NOT_FOUND: i32 = -32601;
7pub const INVALID_PARAMS: i32 = -32602;
8pub const INTERNAL_ERROR: i32 = -32603;
9
10// A2A-specific error codes
11pub const TASK_NOT_FOUND: i32 = -32001;
12pub const TASK_NOT_CANCELABLE: i32 = -32002;
13pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
14pub const UNSUPPORTED_OPERATION: i32 = -32004;
15pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
16pub const INVALID_AGENT_RESPONSE: i32 = -32006;
17
18/// JSON-RPC 2.0 request/notification ID.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(untagged)]
21pub enum JsonRpcId {
22    Number(i64),
23    String(String),
24}
25
26/// JSON-RPC 2.0 request.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct JsonRpcRequest {
29    pub jsonrpc: String,
30    pub method: String,
31    #[serde(default)]
32    pub params: serde_json::Value,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub id: Option<JsonRpcId>,
35}
36
37/// JSON-RPC 2.0 response.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct JsonRpcResponse {
40    pub jsonrpc: String,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub result: Option<serde_json::Value>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub error: Option<JsonRpcError>,
45    pub id: Option<JsonRpcId>,
46}
47
48impl JsonRpcResponse {
49    /// Create a success response.
50    pub fn success(id: Option<JsonRpcId>, result: serde_json::Value) -> Self {
51        Self {
52            jsonrpc: "2.0".to_string(),
53            result: Some(result),
54            error: None,
55            id,
56        }
57    }
58
59    /// Create an error response.
60    pub fn error(id: Option<JsonRpcId>, error: JsonRpcError) -> Self {
61        Self {
62            jsonrpc: "2.0".to_string(),
63            result: None,
64            error: Some(error),
65            id,
66        }
67    }
68}
69
70/// JSON-RPC 2.0 error object.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct JsonRpcError {
73    pub code: i32,
74    pub message: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub data: Option<serde_json::Value>,
77}
78
79impl JsonRpcError {
80    /// Create an error from a standard code with default message.
81    pub fn from_code(code: i32) -> Self {
82        let message = match code {
83            PARSE_ERROR => "Parse error",
84            INVALID_REQUEST => "Invalid Request",
85            METHOD_NOT_FOUND => "Method not found",
86            INVALID_PARAMS => "Invalid params",
87            INTERNAL_ERROR => "Internal error",
88            TASK_NOT_FOUND => "Task not found",
89            TASK_NOT_CANCELABLE => "Task not cancelable",
90            PUSH_NOTIFICATION_NOT_SUPPORTED => "Push notification not supported",
91            UNSUPPORTED_OPERATION => "Unsupported operation",
92            CONTENT_TYPE_NOT_SUPPORTED => "Incompatible content types",
93            INVALID_AGENT_RESPONSE => "Invalid agent response",
94            _ => "Unknown error",
95        };
96        Self {
97            code,
98            message: message.to_string(),
99            data: None,
100        }
101    }
102
103    /// Create an error with a custom message.
104    pub fn with_message(code: i32, message: impl Into<String>) -> Self {
105        Self {
106            code,
107            message: message.into(),
108            data: None,
109        }
110    }
111}
112
113// Note: PartialEq not derived on JsonRpcError/JsonRpcResponse, so tests compare fields directly.
114
115/// A2A protocol methods.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum A2aMethod {
118    MessageSend,
119    MessageStream,
120    TasksGet,
121    TasksCancel,
122    TasksResubscribe,
123    PushNotificationConfigSet,
124    PushNotificationConfigGet,
125    PushNotificationConfigList,
126    PushNotificationConfigDelete,
127}
128
129impl A2aMethod {
130    /// Parse a method string into an A2aMethod.
131    pub fn parse(method: &str) -> Option<Self> {
132        match method {
133            "message/send" => Some(Self::MessageSend),
134            "message/stream" => Some(Self::MessageStream),
135            "tasks/get" => Some(Self::TasksGet),
136            "tasks/cancel" => Some(Self::TasksCancel),
137            "tasks/resubscribe" => Some(Self::TasksResubscribe),
138            "tasks/pushNotificationConfig/set" => Some(Self::PushNotificationConfigSet),
139            "tasks/pushNotificationConfig/get" => Some(Self::PushNotificationConfigGet),
140            "tasks/pushNotificationConfig/list" => Some(Self::PushNotificationConfigList),
141            "tasks/pushNotificationConfig/delete" => Some(Self::PushNotificationConfigDelete),
142            _ => None,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn parse_jsonrpc_request_from_json() {
153        let json = r#"{
154            "jsonrpc": "2.0",
155            "method": "message/send",
156            "params": {"message": {"messageId": "m1", "role": "user", "parts": []}},
157            "id": 42
158        }"#;
159        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
160        assert_eq!(req.jsonrpc, "2.0");
161        assert_eq!(req.method, "message/send");
162        assert_eq!(req.id, Some(JsonRpcId::Number(42)));
163        assert!(req.params.is_object());
164    }
165
166    #[test]
167    fn parse_jsonrpc_request_string_id() {
168        let json = r#"{"jsonrpc":"2.0","method":"tasks/get","params":{},"id":"abc-123"}"#;
169        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
170        assert_eq!(req.id, Some(JsonRpcId::String("abc-123".to_string())));
171    }
172
173    #[test]
174    fn parse_jsonrpc_request_null_id() {
175        // With serde(untagged) on JsonRpcId and Option<JsonRpcId>, "id": null deserializes as None
176        let json = r#"{"jsonrpc":"2.0","method":"tasks/get","params":{},"id":null}"#;
177        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
178        assert_eq!(req.id, None);
179    }
180
181    #[test]
182    fn parse_jsonrpc_request_no_id() {
183        // Notifications have no id field
184        let json = r#"{"jsonrpc":"2.0","method":"tasks/get","params":{}}"#;
185        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
186        assert_eq!(req.id, None);
187    }
188
189    #[test]
190    fn parse_jsonrpc_request_default_params() {
191        let json = r#"{"jsonrpc":"2.0","method":"tasks/get"}"#;
192        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
193        assert!(req.params.is_null());
194    }
195
196    #[test]
197    fn jsonrpc_response_success() {
198        let id = Some(JsonRpcId::Number(1));
199        let resp = JsonRpcResponse::success(id, serde_json::json!({"status": "ok"}));
200        assert_eq!(resp.jsonrpc, "2.0");
201        assert!(resp.result.is_some());
202        assert!(resp.error.is_none());
203        assert_eq!(resp.result.unwrap()["status"], "ok");
204    }
205
206    #[test]
207    fn jsonrpc_response_error() {
208        let id = Some(JsonRpcId::String("req-1".to_string()));
209        let err = JsonRpcError::from_code(TASK_NOT_FOUND);
210        let resp = JsonRpcResponse::error(id, err);
211        assert_eq!(resp.jsonrpc, "2.0");
212        assert!(resp.result.is_none());
213        assert!(resp.error.is_some());
214        let e = resp.error.unwrap();
215        assert_eq!(e.code, TASK_NOT_FOUND);
216        assert_eq!(e.message, "Task not found");
217    }
218
219    #[test]
220    fn jsonrpc_response_round_trip() {
221        let resp =
222            JsonRpcResponse::success(Some(JsonRpcId::Number(7)), serde_json::json!({"data": 42}));
223        let json = serde_json::to_string(&resp).unwrap();
224        let parsed: JsonRpcResponse = serde_json::from_str(&json).unwrap();
225        assert_eq!(parsed.jsonrpc, "2.0");
226        assert_eq!(parsed.result.unwrap()["data"], 42);
227        assert!(parsed.error.is_none());
228    }
229
230    #[test]
231    fn jsonrpc_error_from_code_all_standard() {
232        let cases = [
233            (PARSE_ERROR, "Parse error"),
234            (INVALID_REQUEST, "Invalid Request"),
235            (METHOD_NOT_FOUND, "Method not found"),
236            (INVALID_PARAMS, "Invalid params"),
237            (INTERNAL_ERROR, "Internal error"),
238            (TASK_NOT_FOUND, "Task not found"),
239            (TASK_NOT_CANCELABLE, "Task not cancelable"),
240            (
241                PUSH_NOTIFICATION_NOT_SUPPORTED,
242                "Push notification not supported",
243            ),
244            (UNSUPPORTED_OPERATION, "Unsupported operation"),
245            (CONTENT_TYPE_NOT_SUPPORTED, "Incompatible content types"),
246            (INVALID_AGENT_RESPONSE, "Invalid agent response"),
247        ];
248        for (code, expected_msg) in &cases {
249            let err = JsonRpcError::from_code(*code);
250            assert_eq!(err.code, *code);
251            assert_eq!(err.message, *expected_msg, "code {code}");
252            assert!(err.data.is_none());
253        }
254    }
255
256    #[test]
257    fn jsonrpc_error_from_code_unknown() {
258        let err = JsonRpcError::from_code(-99999);
259        assert_eq!(err.code, -99999);
260        assert_eq!(err.message, "Unknown error");
261    }
262
263    #[test]
264    fn jsonrpc_error_with_message() {
265        let err = JsonRpcError::with_message(INTERNAL_ERROR, "something broke");
266        assert_eq!(err.code, INTERNAL_ERROR);
267        assert_eq!(err.message, "something broke");
268    }
269
270    #[test]
271    fn a2a_method_parse_all() {
272        let cases = [
273            ("message/send", A2aMethod::MessageSend),
274            ("message/stream", A2aMethod::MessageStream),
275            ("tasks/get", A2aMethod::TasksGet),
276            ("tasks/cancel", A2aMethod::TasksCancel),
277            ("tasks/resubscribe", A2aMethod::TasksResubscribe),
278            (
279                "tasks/pushNotificationConfig/set",
280                A2aMethod::PushNotificationConfigSet,
281            ),
282            (
283                "tasks/pushNotificationConfig/get",
284                A2aMethod::PushNotificationConfigGet,
285            ),
286            (
287                "tasks/pushNotificationConfig/list",
288                A2aMethod::PushNotificationConfigList,
289            ),
290            (
291                "tasks/pushNotificationConfig/delete",
292                A2aMethod::PushNotificationConfigDelete,
293            ),
294        ];
295        for (method_str, expected) in &cases {
296            let parsed = A2aMethod::parse(method_str);
297            assert_eq!(parsed, Some(expected.clone()), "parse({method_str})");
298        }
299    }
300
301    #[test]
302    fn a2a_method_parse_unknown() {
303        assert_eq!(A2aMethod::parse("unknown/method"), None);
304        assert_eq!(A2aMethod::parse(""), None);
305        assert_eq!(A2aMethod::parse("message/send/extra"), None);
306    }
307}