Skip to main content

agent_client_protocol_schema/v2/
rpc.rs

1//! JSON-RPC request and response envelopes for protocol version 2.
2//!
3//! These differ from the shared JSON-RPC types only in their request IDs: v2
4//! request IDs are never `null`.
5
6use std::sync::Arc;
7
8use derive_more::{Display, From};
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12use super::Error;
13
14/// A JSON-RPC request ID: a string or an integer. Unlike base JSON-RPC, it MUST NOT be `null`.
15///
16/// The receiver MUST reply with the same value in the response.
17#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
18#[derive(
19    Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize, PartialOrd, Ord, Display, From,
20)]
21#[serde(untagged)]
22#[allow(
23    clippy::exhaustive_enums,
24    reason = "Request IDs are limited to JSON-RPC's non-null ID types"
25)]
26#[from(String, i64)]
27pub enum RequestId {
28    /// A numeric request ID.
29    Number(i64),
30    /// A string request ID.
31    Str(String),
32}
33
34/// A JSON-RPC request object.
35#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
36#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
37#[allow(
38    clippy::exhaustive_structs,
39    reason = "This comes from the JSON-RPC specification itself"
40)]
41#[cfg_attr(feature = "schemars", schemars(rename = "{Params}", extend("x-docs-ignore" = true)))]
42#[skip_serializing_none]
43pub struct Request<Params> {
44    /// The request id used to correlate the matching response.
45    pub id: RequestId,
46    /// The method name to invoke.
47    pub method: Arc<str>,
48    /// Method-specific request parameters.
49    pub params: Option<Params>,
50}
51
52/// A JSON-RPC response object.
53#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
54#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
55#[allow(
56    clippy::exhaustive_enums,
57    reason = "This comes from the JSON-RPC specification itself"
58)]
59#[serde(untagged)]
60#[cfg_attr(feature = "schemars", schemars(rename = "{Result}", extend("x-docs-ignore" = true)))]
61pub enum Response<Result> {
62    /// A successful JSON-RPC response.
63    Result {
64        /// The id of the request this response answers.
65        id: RequestId,
66        /// Method-specific response data.
67        // Require the key even when the payload's deserializer accepts null.
68        #[serde(
69            deserialize_with = "Deserialize::deserialize",
70            bound(deserialize = "Result: Deserialize<'de>")
71        )]
72        result: Result,
73    },
74    /// A failed JSON-RPC response.
75    Error {
76        /// The id of the request this response answers.
77        ///
78        /// Required. `null` only when the request's id could not be determined, such as
79        /// for a parse error or an invalid request.
80        // Require the key; serde would otherwise treat a missing `Option` as `None`.
81        #[serde(deserialize_with = "Option::deserialize")]
82        #[cfg_attr(feature = "schemars", schemars(required))]
83        id: Option<RequestId>,
84        /// Method-specific error data.
85        error: Error,
86    },
87}
88
89impl<R> Response<R> {
90    /// Creates a JSON-RPC response from a Rust [`Result`].
91    #[must_use]
92    pub fn new(id: impl Into<RequestId>, result: std::result::Result<R, Error>) -> Self {
93        match result {
94            Ok(result) => Self::Result {
95                id: id.into(),
96                result,
97            },
98            Err(error) => Self::Error {
99                id: Some(id.into()),
100                error,
101            },
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    use serde_json::{Value, json};
111
112    #[test]
113    fn request_id_rejects_null() {
114        assert!(serde_json::from_value::<RequestId>(Value::Null).is_err());
115        assert!(
116            serde_json::from_value::<Request<Value>>(json!({ "id": null, "method": "m" })).is_err()
117        );
118        assert_eq!(
119            serde_json::from_value::<RequestId>(json!(-1)).unwrap(),
120            RequestId::Number(-1)
121        );
122        assert_eq!(
123            serde_json::from_value::<RequestId>(json!("id")).unwrap(),
124            RequestId::Str("id".to_owned())
125        );
126    }
127
128    #[test]
129    fn error_response_id_is_required_and_nullable() {
130        let error = json!({ "code": -32700, "message": "Parse error" });
131
132        let response =
133            serde_json::from_value::<Response<Value>>(json!({ "id": null, "error": error }))
134                .unwrap();
135        assert!(matches!(response, Response::Error { id: None, .. }));
136        assert_eq!(
137            serde_json::to_value(&response).unwrap(),
138            json!({ "id": null, "error": error })
139        );
140
141        assert!(serde_json::from_value::<Response<Value>>(json!({ "error": error })).is_err());
142        assert!(
143            serde_json::from_value::<Response<Value>>(json!({ "id": null, "result": {} })).is_err()
144        );
145    }
146}