Skip to main content

agent_client_protocol/jsonrpc/
raw_id.rs

1//! The protocol-version-neutral JSON-RPC ID used by the raw message layer.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::v1::schema as v1;
8#[cfg(feature = "unstable_protocol_v2")]
9use crate::v2::schema as v2;
10
11/// A JSON-RPC `id` exactly as it appears on the wire: an integer, a string or
12/// `null`.
13///
14/// This is the ID of [`RawJsonRpcMessage`](crate::RawJsonRpcMessage) and its
15/// requests and responses, whichever ACP version the connection speaks. The
16/// SDK's request handles return it from `raw_id()`; their `id()` returns the
17/// request ID type of their [`Protocol`](crate::Protocol) version. It keeps the wire form: `1`
18/// and `"1"` are different IDs, and integers are kept exactly across the
19/// whole 64-bit signed range, which is the range every ACP schema version
20/// accepts. Other numbers are not valid IDs.
21///
22/// `null` is allowed because JSON-RPC allows it, and protocol v1 accepts it
23/// on requests. Protocol v2 does not; the SDK rejects a v2 request with a
24/// `null` ID before dispatch. Error responses to messages whose ID cannot be
25/// read also carry `null`.
26///
27/// It converts losslessly to and from
28/// [`v1::schema::RequestId`](crate::v1::schema::RequestId), and from
29/// `v2::schema::RequestId`. Converting to `v2::schema::RequestId` fails only
30/// for `null`:
31///
32/// ```
33/// use agent_client_protocol::{RawJsonRpcId, v1::schema};
34///
35/// let id = RawJsonRpcId::from(schema::RequestId::Number(7));
36/// assert_eq!(id, RawJsonRpcId::Number(7));
37/// assert_eq!(schema::RequestId::from(id), schema::RequestId::Number(7));
38/// assert_eq!(serde_json::to_string(&RawJsonRpcId::from("7".to_string())).unwrap(), r#""7""#);
39/// ```
40#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42#[serde(untagged)]
43#[allow(
44    clippy::exhaustive_enums,
45    reason = "JSON-RPC defines exactly these ID types"
46)]
47pub enum RawJsonRpcId {
48    /// The JSON-RPC `null` ID.
49    Null,
50    /// An integer ID.
51    Number(i64),
52    /// A string ID.
53    Str(String),
54}
55
56impl fmt::Display for RawJsonRpcId {
57    /// Integers and strings are written without quotes, and `null` as `null`,
58    /// as [`v1::schema::RequestId`](v1::RequestId) does. Use `serde_json` for the wire form.
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Null => f.write_str("null"),
62            Self::Number(id) => write!(f, "{id}"),
63            Self::Str(id) => f.write_str(id),
64        }
65    }
66}
67
68impl From<i64> for RawJsonRpcId {
69    fn from(id: i64) -> Self {
70        Self::Number(id)
71    }
72}
73
74impl From<String> for RawJsonRpcId {
75    fn from(id: String) -> Self {
76        Self::Str(id)
77    }
78}
79
80impl From<v1::RequestId> for RawJsonRpcId {
81    fn from(id: v1::RequestId) -> Self {
82        match id {
83            v1::RequestId::Null => Self::Null,
84            v1::RequestId::Number(id) => Self::Number(id),
85            v1::RequestId::Str(id) => Self::Str(id),
86        }
87    }
88}
89
90impl From<RawJsonRpcId> for v1::RequestId {
91    fn from(id: RawJsonRpcId) -> Self {
92        match id {
93            RawJsonRpcId::Null => Self::Null,
94            RawJsonRpcId::Number(id) => Self::Number(id),
95            RawJsonRpcId::Str(id) => Self::Str(id),
96        }
97    }
98}
99
100#[cfg(feature = "unstable_protocol_v2")]
101impl From<v2::RequestId> for RawJsonRpcId {
102    fn from(id: v2::RequestId) -> Self {
103        match id {
104            v2::RequestId::Number(id) => Self::Number(id),
105            v2::RequestId::Str(id) => Self::Str(id),
106        }
107    }
108}
109
110#[cfg(feature = "unstable_protocol_v2")]
111impl TryFrom<RawJsonRpcId> for v2::RequestId {
112    type Error = crate::Error;
113
114    /// Fails for `null`, which protocol v2 does not allow, with the Invalid
115    /// Request error (`-32600`) the SDK sends for a v2 request with a `null`
116    /// ID.
117    fn try_from(id: RawJsonRpcId) -> Result<Self, Self::Error> {
118        match id {
119            RawJsonRpcId::Null => Err(null_v2_request_id()),
120            RawJsonRpcId::Number(id) => Ok(Self::Number(id)),
121            RawJsonRpcId::Str(id) => Ok(Self::Str(id)),
122        }
123    }
124}
125
126/// The Invalid Request error for a protocol-v2 request whose ID is `null`.
127pub(crate) fn null_v2_request_id() -> crate::Error {
128    crate::Error::invalid_request()
129        .data("ACP protocol v2 request ids must be a string or an integer, not null")
130}
131
132#[cfg(test)]
133mod tests {
134    use serde_json::{Value, json};
135
136    use super::*;
137
138    fn wire_ids() -> Vec<Value> {
139        vec![
140            Value::Null,
141            json!(0),
142            json!(1),
143            json!(-1),
144            json!(9_007_199_254_740_993_i64),
145            json!(i64::MAX),
146            json!(i64::MIN),
147            json!("1"),
148            json!(""),
149            json!("ünïcødé \"quoted\""),
150        ]
151    }
152
153    #[test]
154    fn wire_form_round_trips_exactly() {
155        for wire in wire_ids() {
156            let text = wire.to_string();
157            let id: RawJsonRpcId = serde_json::from_str(&text).unwrap();
158            assert_eq!(serde_json::to_string(&id).unwrap(), text);
159        }
160        assert_ne!(
161            serde_json::from_str::<RawJsonRpcId>("1").unwrap(),
162            serde_json::from_str::<RawJsonRpcId>(r#""1""#).unwrap()
163        );
164    }
165
166    #[test]
167    fn non_ids_are_rejected() {
168        for text in [
169            "9223372036854775808",
170            "-9223372036854775809",
171            "1.5",
172            "1e3",
173            "true",
174            "[]",
175            "{}",
176        ] {
177            assert!(
178                serde_json::from_str::<RawJsonRpcId>(text).is_err(),
179                "{text}"
180            );
181            assert!(
182                serde_json::from_str::<v1::RequestId>(text).is_err(),
183                "{text}"
184            );
185        }
186    }
187
188    #[test]
189    fn v1_conversions_are_lossless_and_match_the_wire() {
190        for wire in wire_ids() {
191            let v1_id: v1::RequestId = serde_json::from_value(wire.clone()).unwrap();
192            let id = RawJsonRpcId::from(v1_id.clone());
193            assert_eq!(serde_json::to_value(&id).unwrap(), wire);
194            assert_eq!(id.to_string(), v1_id.to_string());
195            assert_eq!(v1::RequestId::from(id), v1_id);
196        }
197    }
198
199    #[cfg(feature = "unstable_protocol_v2")]
200    #[test]
201    fn v2_conversions_are_lossless_and_reject_null() {
202        for wire in wire_ids() {
203            let id: RawJsonRpcId = serde_json::from_value(wire.clone()).unwrap();
204            if let Ok(v2_id) = serde_json::from_value::<v2::RequestId>(wire.clone()) {
205                assert_eq!(RawJsonRpcId::from(v2_id.clone()), id);
206                assert_eq!(v2::RequestId::try_from(id).unwrap(), v2_id);
207            } else {
208                assert_eq!(id, RawJsonRpcId::Null);
209                let error = v2::RequestId::try_from(id).unwrap_err();
210                assert_eq!(error.code, crate::ErrorCode::INVALID_REQUEST);
211            }
212        }
213    }
214}