Skip to main content

a2a/
jsonrpc.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// Copyright A2A Contributors (https://github.com/a2aproject)
3// SPDX-License-Identifier: Apache-2.0
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6
7// ---------------------------------------------------------------------------
8// JSON-RPC 2.0 types
9// ---------------------------------------------------------------------------
10
11/// JSON-RPC 2.0 request.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct JsonRpcRequest {
14    pub jsonrpc: String,
15    pub id: JsonRpcId,
16    pub method: String,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub params: Option<Value>,
19}
20
21impl JsonRpcRequest {
22    pub fn new(id: JsonRpcId, method: impl Into<String>, params: Option<Value>) -> Self {
23        JsonRpcRequest {
24            jsonrpc: "2.0".to_string(),
25            id,
26            method: method.into(),
27            params,
28        }
29    }
30}
31
32/// JSON-RPC 2.0 response.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct JsonRpcResponse {
35    pub jsonrpc: String,
36    pub id: JsonRpcId,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub result: Option<Value>,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub error: Option<JsonRpcError>,
41}
42
43impl JsonRpcResponse {
44    pub fn success(id: JsonRpcId, result: Value) -> Self {
45        JsonRpcResponse {
46            jsonrpc: "2.0".to_string(),
47            id,
48            result: Some(result),
49            error: None,
50        }
51    }
52
53    pub fn error(id: JsonRpcId, error: JsonRpcError) -> Self {
54        JsonRpcResponse {
55            jsonrpc: "2.0".to_string(),
56            id,
57            result: None,
58            error: Some(error),
59        }
60    }
61}
62
63/// JSON-RPC 2.0 error object.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct JsonRpcError {
66    pub code: i32,
67    pub message: String,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub data: Option<Value>,
70}
71
72// ---------------------------------------------------------------------------
73// JsonRpcId — preserves string vs number
74// ---------------------------------------------------------------------------
75
76/// A JSON-RPC ID that can be a string, integer, or null.
77#[derive(Debug, Clone, PartialEq)]
78pub enum JsonRpcId {
79    String(String),
80    Number(i64),
81    Null,
82}
83
84impl Serialize for JsonRpcId {
85    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
86        match self {
87            JsonRpcId::String(s) => serializer.serialize_str(s),
88            JsonRpcId::Number(n) => serializer.serialize_i64(*n),
89            JsonRpcId::Null => serializer.serialize_none(),
90        }
91    }
92}
93
94impl<'de> Deserialize<'de> for JsonRpcId {
95    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
96        let v = Value::deserialize(deserializer)?;
97        match v {
98            Value::String(s) => Ok(JsonRpcId::String(s)),
99            Value::Number(n) => {
100                if let Some(i) = n.as_i64() {
101                    Ok(JsonRpcId::Number(i))
102                } else {
103                    Err(serde::de::Error::custom(
104                        "JSON-RPC id must be a non-fractional number",
105                    ))
106                }
107            }
108            Value::Null => Ok(JsonRpcId::Null),
109            _ => Err(serde::de::Error::custom(
110                "JSON-RPC id must be string, integer, or null",
111            )),
112        }
113    }
114}
115
116impl From<String> for JsonRpcId {
117    fn from(s: String) -> Self {
118        JsonRpcId::String(s)
119    }
120}
121
122impl From<&str> for JsonRpcId {
123    fn from(s: &str) -> Self {
124        JsonRpcId::String(s.to_string())
125    }
126}
127
128impl From<i64> for JsonRpcId {
129    fn from(n: i64) -> Self {
130        JsonRpcId::Number(n)
131    }
132}
133
134// ---------------------------------------------------------------------------
135// A2A JSON-RPC method names
136// ---------------------------------------------------------------------------
137
138pub mod methods {
139    pub const SEND_MESSAGE: &str = "SendMessage";
140    pub const SEND_STREAMING_MESSAGE: &str = "SendStreamingMessage";
141    pub const GET_TASK: &str = "GetTask";
142    pub const LIST_TASKS: &str = "ListTasks";
143    pub const CANCEL_TASK: &str = "CancelTask";
144    pub const SUBSCRIBE_TO_TASK: &str = "SubscribeToTask";
145    pub const CREATE_PUSH_CONFIG: &str = "CreateTaskPushNotificationConfig";
146    pub const GET_PUSH_CONFIG: &str = "GetTaskPushNotificationConfig";
147    pub const LIST_PUSH_CONFIGS: &str = "ListTaskPushNotificationConfigs";
148    pub const DELETE_PUSH_CONFIG: &str = "DeleteTaskPushNotificationConfig";
149    pub const GET_EXTENDED_AGENT_CARD: &str = "GetExtendedAgentCard";
150
151    pub fn is_streaming(method: &str) -> bool {
152        matches!(method, SEND_STREAMING_MESSAGE | SUBSCRIBE_TO_TASK)
153    }
154
155    pub fn is_valid(method: &str) -> bool {
156        matches!(
157            method,
158            SEND_MESSAGE
159                | SEND_STREAMING_MESSAGE
160                | GET_TASK
161                | LIST_TASKS
162                | CANCEL_TASK
163                | SUBSCRIBE_TO_TASK
164                | CREATE_PUSH_CONFIG
165                | GET_PUSH_CONFIG
166                | LIST_PUSH_CONFIGS
167                | DELETE_PUSH_CONFIG
168                | GET_EXTENDED_AGENT_CARD
169        )
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_jsonrpc_id_string() {
179        let id = JsonRpcId::String("abc".into());
180        let json = serde_json::to_string(&id).unwrap();
181        assert_eq!(json, r#""abc""#);
182        let back: JsonRpcId = serde_json::from_str(&json).unwrap();
183        assert_eq!(back, JsonRpcId::String("abc".into()));
184    }
185
186    #[test]
187    fn test_jsonrpc_id_number() {
188        let id = JsonRpcId::Number(42);
189        let json = serde_json::to_string(&id).unwrap();
190        assert_eq!(json, "42");
191        let back: JsonRpcId = serde_json::from_str(&json).unwrap();
192        assert_eq!(back, JsonRpcId::Number(42));
193    }
194
195    #[test]
196    fn test_jsonrpc_request_roundtrip() {
197        let req = JsonRpcRequest::new(
198            JsonRpcId::String("req-1".into()),
199            methods::SEND_MESSAGE,
200            Some(serde_json::json!({"key": "value"})),
201        );
202        let json = serde_json::to_string(&req).unwrap();
203        let back: JsonRpcRequest = serde_json::from_str(&json).unwrap();
204        assert_eq!(back.method, methods::SEND_MESSAGE);
205        assert_eq!(back.jsonrpc, "2.0");
206    }
207
208    #[test]
209    fn test_jsonrpc_id_null() {
210        let id = JsonRpcId::Null;
211        let json = serde_json::to_string(&id).unwrap();
212        assert_eq!(json, "null");
213        let back: JsonRpcId = serde_json::from_str(&json).unwrap();
214        assert_eq!(back, JsonRpcId::Null);
215    }
216
217    #[test]
218    fn test_jsonrpc_id_invalid_type() {
219        let result = serde_json::from_str::<JsonRpcId>(r#"[1,2]"#);
220        assert!(result.is_err());
221    }
222
223    #[test]
224    fn test_jsonrpc_id_fractional_number() {
225        let result = serde_json::from_str::<JsonRpcId>("3.14");
226        assert!(result.is_err());
227    }
228
229    #[test]
230    fn test_jsonrpc_id_from_string() {
231        let id: JsonRpcId = String::from("test").into();
232        assert_eq!(id, JsonRpcId::String("test".into()));
233    }
234
235    #[test]
236    fn test_jsonrpc_id_from_str() {
237        let id: JsonRpcId = "test".into();
238        assert_eq!(id, JsonRpcId::String("test".into()));
239    }
240
241    #[test]
242    fn test_jsonrpc_id_from_i64() {
243        let id: JsonRpcId = 42i64.into();
244        assert_eq!(id, JsonRpcId::Number(42));
245    }
246
247    #[test]
248    fn test_jsonrpc_response_success() {
249        let resp =
250            JsonRpcResponse::success(JsonRpcId::Number(1), serde_json::json!({"status": "ok"}));
251        assert_eq!(resp.jsonrpc, "2.0");
252        assert!(resp.result.is_some());
253        assert!(resp.error.is_none());
254        let json = serde_json::to_string(&resp).unwrap();
255        let back: JsonRpcResponse = serde_json::from_str(&json).unwrap();
256        assert_eq!(back.id, JsonRpcId::Number(1));
257    }
258
259    #[test]
260    fn test_jsonrpc_response_error() {
261        let err = JsonRpcError {
262            code: -32600,
263            message: "invalid".into(),
264            data: None,
265        };
266        let resp = JsonRpcResponse::error(JsonRpcId::String("e1".into()), err);
267        assert!(resp.result.is_none());
268        assert!(resp.error.is_some());
269        assert_eq!(resp.error.as_ref().unwrap().code, -32600);
270    }
271
272    #[test]
273    fn test_jsonrpc_error_with_data() {
274        let err = JsonRpcError {
275            code: -32000,
276            message: "custom".into(),
277            data: Some(serde_json::json!({"detail": "info"})),
278        };
279        let json = serde_json::to_string(&err).unwrap();
280        let back: JsonRpcError = serde_json::from_str(&json).unwrap();
281        assert!(back.data.is_some());
282    }
283
284    #[test]
285    fn test_jsonrpc_request_no_params() {
286        let req = JsonRpcRequest::new(JsonRpcId::Number(1), methods::GET_TASK, None);
287        let json = serde_json::to_string(&req).unwrap();
288        assert!(!json.contains("params"));
289    }
290
291    #[test]
292    fn test_methods_is_streaming() {
293        assert!(methods::is_streaming(methods::SEND_STREAMING_MESSAGE));
294        assert!(methods::is_streaming(methods::SUBSCRIBE_TO_TASK));
295        assert!(!methods::is_streaming("message.stream"));
296        assert!(!methods::is_streaming("tasks.resubscribe"));
297        assert!(!methods::is_streaming(methods::SEND_MESSAGE));
298        assert!(!methods::is_streaming(methods::GET_TASK));
299        assert!(!methods::is_streaming("unknown"));
300    }
301
302    #[test]
303    fn test_methods_is_valid() {
304        assert!(methods::is_valid(methods::SEND_MESSAGE));
305        assert!(methods::is_valid(methods::SEND_STREAMING_MESSAGE));
306        assert!(methods::is_valid(methods::GET_TASK));
307        assert!(methods::is_valid(methods::LIST_TASKS));
308        assert!(methods::is_valid(methods::CANCEL_TASK));
309        assert!(methods::is_valid(methods::SUBSCRIBE_TO_TASK));
310        assert!(methods::is_valid(methods::CREATE_PUSH_CONFIG));
311        assert!(methods::is_valid(methods::GET_PUSH_CONFIG));
312        assert!(methods::is_valid(methods::LIST_PUSH_CONFIGS));
313        assert!(methods::is_valid(methods::DELETE_PUSH_CONFIG));
314        assert!(methods::is_valid(methods::GET_EXTENDED_AGENT_CARD));
315        assert!(!methods::is_valid("message.send"));
316        assert!(!methods::is_valid("message.stream"));
317        assert!(!methods::is_valid("tasks.get"));
318        assert!(!methods::is_valid("tasks.list"));
319        assert!(!methods::is_valid("tasks.cancel"));
320        assert!(!methods::is_valid("tasks.resubscribe"));
321        assert!(!methods::is_valid("push-config.set"));
322        assert!(!methods::is_valid("push-config.get"));
323        assert!(!methods::is_valid("push-config.list"));
324        assert!(!methods::is_valid("push-config.delete"));
325        assert!(!methods::is_valid("agent-card.extended.get"));
326        assert!(!methods::is_valid("unknown.method"));
327    }
328}