1use std::sync::Arc;
7
8use derive_more::{Display, From};
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22#[derive(
23 Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize, PartialOrd, Ord, Display, From,
24)]
25#[serde(untagged)]
26#[allow(
27 clippy::exhaustive_enums,
28 reason = "This comes from the JSON-RPC specification itself"
29)]
30#[from(String, i64)]
31pub enum RequestId {
32 #[display("null")]
34 Null,
35 Number(i64),
37 Str(String),
39}
40
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
44#[allow(
45 clippy::exhaustive_structs,
46 reason = "This comes from the JSON-RPC specification itself"
47)]
48#[cfg_attr(feature = "schemars", schemars(rename = "{Params}", extend("x-docs-ignore" = true)))]
49#[skip_serializing_none]
50pub struct Request<Params> {
51 pub id: RequestId,
53 pub method: Arc<str>,
55 pub params: Option<Params>,
57}
58
59#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
61#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
62#[allow(
63 clippy::exhaustive_enums,
64 reason = "This comes from the JSON-RPC specification itself"
65)]
66#[serde(untagged)]
67#[cfg_attr(feature = "schemars", schemars(rename = "{Result}", extend("x-docs-ignore" = true)))]
68pub enum Response<Result, Error> {
69 Result {
71 id: RequestId,
73 #[serde(
76 deserialize_with = "Deserialize::deserialize",
77 bound(deserialize = "Result: Deserialize<'de>")
78 )]
79 result: Result,
80 },
81 Error {
83 id: RequestId,
85 error: Error,
87 },
88}
89
90impl<R, E> Response<R, E> {
91 #[must_use]
93 pub fn new(id: impl Into<RequestId>, result: std::result::Result<R, E>) -> Self {
94 match result {
95 Ok(result) => Self::Result {
96 id: id.into(),
97 result,
98 },
99 Err(error) => Self::Error {
100 id: id.into(),
101 error,
102 },
103 }
104 }
105}
106
107#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
109#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
110#[allow(
111 clippy::exhaustive_structs,
112 reason = "This comes from the JSON-RPC specification itself"
113)]
114#[cfg_attr(feature = "schemars", schemars(rename = "{Params}", extend("x-docs-ignore" = true)))]
115#[skip_serializing_none]
116pub struct Notification<Params> {
117 pub method: Arc<str>,
119 pub params: Option<Params>,
121}
122
123#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[cfg_attr(feature = "schemars", schemars(inline))]
126enum JsonRpcVersion {
127 #[serde(rename = "2.0")]
128 V2,
129}
130
131#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[cfg_attr(feature = "schemars", schemars(inline))]
138pub struct JsonRpcMessage<M> {
139 jsonrpc: JsonRpcVersion,
140 #[serde(flatten)]
141 message: M,
142}
143
144impl<M> JsonRpcMessage<M> {
145 #[must_use]
147 pub fn wrap(message: M) -> Self {
148 Self {
149 jsonrpc: JsonRpcVersion::V2,
150 message,
151 }
152 }
153
154 #[must_use]
156 pub fn inner(&self) -> &M {
157 &self.message
158 }
159
160 #[must_use]
162 pub fn into_inner(self) -> M {
163 self.message
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
169#[display("JSON-RPC batch must contain at least one message")]
170#[non_exhaustive]
171pub struct EmptyJsonRpcBatch;
172
173impl std::error::Error for EmptyJsonRpcBatch {}
174
175#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
177#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
178#[cfg_attr(feature = "schemars", schemars(inline))]
179#[serde(transparent)]
180#[allow(
181 clippy::exhaustive_structs,
182 reason = "This comes from the JSON-RPC specification itself"
183)]
184pub struct JsonRpcBatch<M>(
185 #[cfg_attr(feature = "schemars", schemars(length(min = 1)))] Vec<JsonRpcMessage<M>>,
186);
187
188impl<M> JsonRpcBatch<M> {
189 pub fn new(messages: Vec<JsonRpcMessage<M>>) -> Result<Self, EmptyJsonRpcBatch> {
198 if messages.is_empty() {
199 Err(EmptyJsonRpcBatch)
200 } else {
201 Ok(Self(messages))
202 }
203 }
204
205 #[must_use]
207 pub fn as_slice(&self) -> &[JsonRpcMessage<M>] {
208 &self.0
209 }
210
211 #[must_use]
213 pub fn into_vec(self) -> Vec<JsonRpcMessage<M>> {
214 self.0
215 }
216}
217
218impl<M> TryFrom<Vec<JsonRpcMessage<M>>> for JsonRpcBatch<M> {
219 type Error = EmptyJsonRpcBatch;
220
221 fn try_from(messages: Vec<JsonRpcMessage<M>>) -> Result<Self, Self::Error> {
222 Self::new(messages)
223 }
224}
225
226impl<'de, M> Deserialize<'de> for JsonRpcBatch<M>
227where
228 M: Deserialize<'de>,
229{
230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231 where
232 D: serde::Deserializer<'de>,
233 {
234 let messages = Vec::<JsonRpcMessage<M>>::deserialize(deserializer)?;
235 Self::new(messages).map_err(serde::de::Error::custom)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 use crate::v1::{
244 AgentNotification, CancelNotification, ClientNotification, ContentBlock, ContentChunk,
245 SessionId, SessionNotification, SessionUpdate, TextContent,
246 };
247 use serde_json::{Number, Value, json};
248
249 #[test]
250 fn id_deserialization() {
251 let id = serde_json::from_value::<RequestId>(Value::Null).unwrap();
252 assert_eq!(id, RequestId::Null);
253
254 let id = serde_json::from_value::<RequestId>(Value::Number(Number::from_u128(1).unwrap()))
255 .unwrap();
256 assert_eq!(id, RequestId::Number(1));
257
258 let id = serde_json::from_value::<RequestId>(Value::Number(Number::from_i128(-1).unwrap()))
259 .unwrap();
260 assert_eq!(id, RequestId::Number(-1));
261
262 let id = serde_json::from_value::<RequestId>(Value::String("id".to_owned())).unwrap();
263 assert_eq!(id, RequestId::Str("id".to_owned()));
264 }
265
266 #[test]
267 fn id_serialization() {
268 let id = serde_json::to_value(RequestId::Null).unwrap();
269 assert_eq!(id, Value::Null);
270
271 let id = serde_json::to_value(RequestId::Number(1)).unwrap();
272 assert_eq!(id, Value::Number(Number::from_u128(1).unwrap()));
273
274 let id = serde_json::to_value(RequestId::Number(-1)).unwrap();
275 assert_eq!(id, Value::Number(Number::from_i128(-1).unwrap()));
276
277 let id = serde_json::to_value(RequestId::Str("id".to_owned())).unwrap();
278 assert_eq!(id, Value::String("id".to_owned()));
279 }
280
281 #[test]
282 fn id_display() {
283 let id = RequestId::Null;
284 assert_eq!(id.to_string(), "null");
285
286 let id = RequestId::Number(1);
287 assert_eq!(id.to_string(), "1");
288
289 let id = RequestId::Number(-1);
290 assert_eq!(id.to_string(), "-1");
291
292 let id = RequestId::Str("id".to_owned());
293 assert_eq!(id.to_string(), "id");
294 }
295
296 #[test]
297 fn batch_deserialization_requires_at_least_one_message() {
298 let err = serde_json::from_value::<JsonRpcBatch<Notification<ClientNotification>>>(
299 Value::Array(Vec::new()),
300 )
301 .unwrap_err();
302 assert!(err.to_string().contains("at least one message"));
303 }
304
305 #[test]
306 fn batch_serialization_round_trips_non_empty_messages() {
307 let notification = JsonRpcMessage::wrap(Notification {
308 method: "cancel".into(),
309 params: Some(ClientNotification::CancelNotification(CancelNotification {
310 session_id: SessionId("test-123".into()),
311 meta: None,
312 })),
313 });
314
315 let batch = JsonRpcBatch::new(vec![notification]).unwrap();
316 let serialized = serde_json::to_value(&batch).unwrap();
317 assert_eq!(
318 serialized,
319 json!([{
320 "jsonrpc": "2.0",
321 "method": "cancel",
322 "params": {
323 "sessionId": "test-123"
324 },
325 }])
326 );
327
328 let deserialized =
329 serde_json::from_value::<JsonRpcBatch<Notification<ClientNotification>>>(serialized)
330 .unwrap();
331 assert_eq!(deserialized.as_slice().len(), 1);
332 assert_eq!(deserialized.as_slice()[0].inner().method.as_ref(), "cancel");
333 }
334
335 #[test]
336 fn notification_wire_format() {
337 let outgoing_msg = JsonRpcMessage::wrap(Notification {
339 method: "cancel".into(),
340 params: Some(ClientNotification::CancelNotification(CancelNotification {
341 session_id: SessionId("test-123".into()),
342 meta: None,
343 })),
344 });
345
346 let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
347 assert_eq!(
348 serialized,
349 json!({
350 "jsonrpc": "2.0",
351 "method": "cancel",
352 "params": {
353 "sessionId": "test-123"
354 },
355 })
356 );
357
358 let outgoing_msg = JsonRpcMessage::wrap(Notification {
360 method: "sessionUpdate".into(),
361 params: Some(AgentNotification::SessionNotification(
362 SessionNotification {
363 session_id: SessionId("test-456".into()),
364 update: SessionUpdate::AgentMessageChunk(ContentChunk {
365 content: ContentBlock::Text(TextContent {
366 annotations: None,
367 text: "Hello".to_string(),
368 meta: None,
369 }),
370 message_id: None,
371 meta: None,
372 }),
373 meta: None,
374 },
375 )),
376 });
377
378 let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
379 assert_eq!(
380 serialized,
381 json!({
382 "jsonrpc": "2.0",
383 "method": "sessionUpdate",
384 "params": {
385 "sessionId": "test-456",
386 "update": {
387 "sessionUpdate": "agent_message_chunk",
388 "content": {
389 "type": "text",
390 "text": "Hello"
391 }
392 }
393 }
394 })
395 );
396 }
397}