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. Each side chooses IDs for its own
17/// requests, so the same ID can be in use in both directions. A sender MUST NOT reuse an ID while
18/// a request with it is still awaiting a response.
19#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
20#[derive(
21    Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize, PartialOrd, Ord, Display, From,
22)]
23#[serde(untagged)]
24#[allow(
25    clippy::exhaustive_enums,
26    reason = "Request IDs are limited to JSON-RPC's non-null ID types"
27)]
28#[from(String, i64)]
29pub enum RequestId {
30    /// A numeric request ID.
31    Number(i64),
32    /// A string request ID.
33    Str(String),
34}
35
36/// A JSON-RPC request object.
37#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
38#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
39#[allow(
40    clippy::exhaustive_structs,
41    reason = "This comes from the JSON-RPC specification itself"
42)]
43#[cfg_attr(feature = "schemars", schemars(rename = "{Params}", extend("x-docs-ignore" = true)))]
44#[skip_serializing_none]
45pub struct Request<Params> {
46    /// The request id used to correlate the matching response.
47    pub id: RequestId,
48    /// The method name to invoke.
49    pub method: Arc<str>,
50    /// Method-specific request parameters.
51    pub params: Option<Params>,
52}
53
54/// A JSON-RPC response object.
55#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
56#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
57#[allow(
58    clippy::exhaustive_enums,
59    reason = "This comes from the JSON-RPC specification itself"
60)]
61#[serde(untagged)]
62#[cfg_attr(feature = "schemars", schemars(rename = "{Result}", extend("x-docs-ignore" = true)))]
63pub enum Response<Result> {
64    /// A successful JSON-RPC response.
65    Result {
66        /// The id of the request this response answers.
67        id: RequestId,
68        /// Method-specific response data.
69        // Require the key even when the payload's deserializer accepts null.
70        #[serde(
71            deserialize_with = "Deserialize::deserialize",
72            bound(deserialize = "Result: Deserialize<'de>")
73        )]
74        result: Result,
75    },
76    /// A failed JSON-RPC response.
77    Error {
78        /// The id of the request this response answers.
79        ///
80        /// Required. `null` only when the request's id could not be determined, such as
81        /// for a parse error or an invalid request.
82        // Require the key; serde would otherwise treat a missing `Option` as `None`.
83        #[serde(deserialize_with = "Option::deserialize")]
84        #[cfg_attr(feature = "schemars", schemars(required, transform = nullable_request_id))]
85        id: Option<RequestId>,
86        /// Method-specific error data.
87        error: Error,
88    },
89}
90
91/// Adds `null` to the error response `id` schema. Schemars emits a required `Option` field as
92/// its inner type only.
93#[cfg(feature = "schemars")]
94fn nullable_request_id(schema: &mut schemars::Schema) {
95    let null = serde_json::json!({ "type": "null" });
96    if let Some(variants) = schema
97        .get_mut("anyOf")
98        .and_then(serde_json::Value::as_array_mut)
99        && !variants.contains(&null)
100    {
101        variants.push(null);
102    }
103}
104
105impl<R> Response<R> {
106    /// Creates a JSON-RPC response from a Rust [`Result`].
107    #[must_use]
108    pub fn new(id: impl Into<RequestId>, result: std::result::Result<R, Error>) -> Self {
109        match result {
110            Ok(result) => Self::Result {
111                id: id.into(),
112                result,
113            },
114            Err(error) => Self::Error {
115                id: Some(id.into()),
116                error,
117            },
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    use serde_json::{Value, json};
127
128    #[test]
129    fn request_id_rejects_null() {
130        assert!(serde_json::from_value::<RequestId>(Value::Null).is_err());
131        assert!(
132            serde_json::from_value::<Request<Value>>(json!({ "id": null, "method": "m" })).is_err()
133        );
134        assert_eq!(
135            serde_json::from_value::<RequestId>(json!(-1)).unwrap(),
136            RequestId::Number(-1)
137        );
138        assert_eq!(
139            serde_json::from_value::<RequestId>(json!("id")).unwrap(),
140            RequestId::Str("id".to_owned())
141        );
142    }
143
144    #[test]
145    fn error_response_id_is_required_and_nullable() {
146        let error = json!({ "code": -32700, "message": "Parse error" });
147
148        let response =
149            serde_json::from_value::<Response<Value>>(json!({ "id": null, "error": error }))
150                .unwrap();
151        assert!(matches!(response, Response::Error { id: None, .. }));
152        assert_eq!(
153            serde_json::to_value(&response).unwrap(),
154            json!({ "id": null, "error": error })
155        );
156
157        assert!(serde_json::from_value::<Response<Value>>(json!({ "error": error })).is_err());
158        assert!(
159            serde_json::from_value::<Response<Value>>(json!({ "id": null, "result": {} })).is_err()
160        );
161    }
162}