use a2a_protocol_types::method::Method;
use crate::error::{ClientError, ClientResult};
fn returns_empty(method: &str) -> bool {
Method::from_wire_name(method).is_some_and(Method::returns_empty)
}
pub fn jsonrpc_empty_success(
method: &str,
body: &[u8],
request_id: &serde_json::Value,
) -> Option<ClientResult<serde_json::Value>> {
if !returns_empty(method) {
return None;
}
let value: serde_json::Value = serde_json::from_slice(body).ok()?;
let obj = value.as_object()?;
if obj.get("jsonrpc").and_then(serde_json::Value::as_str) != Some("2.0")
|| obj.contains_key("result")
|| obj.contains_key("error")
{
return None;
}
if obj.get("id") != Some(request_id) {
return Some(Err(ClientError::Transport(
"JSON-RPC response id does not match request id".into(),
)));
}
Some(Ok(serde_json::Value::Null))
}
pub fn rest_empty_success(method: &str, body: &[u8]) -> bool {
returns_empty(method) && body.iter().all(u8::is_ascii_whitespace)
}
#[cfg(test)]
mod tests {
use super::*;
const DELETE: &str = "DeleteTaskPushNotificationConfig";
fn id() -> serde_json::Value {
serde_json::json!("req-1")
}
#[test]
fn jsonrpc_result_less_envelope_is_success_only_for_empty_methods() {
let body = br#"{"jsonrpc":"2.0","id":"req-1"}"#;
assert!(matches!(
jsonrpc_empty_success(DELETE, body, &id()),
Some(Ok(serde_json::Value::Null))
));
assert!(jsonrpc_empty_success("GetTask", body, &id()).is_none());
assert!(jsonrpc_empty_success("NoSuchMethod", body, &id()).is_none());
}
#[test]
fn jsonrpc_envelopes_that_say_something_are_left_to_the_normal_parse() {
for body in [
&br#"{"jsonrpc":"2.0","id":"req-1","result":{}}"#[..],
br#"{"jsonrpc":"2.0","id":"req-1","error":{"code":1,"message":"m"}}"#,
br#"{"jsonrpc":"1.0","id":"req-1"}"#,
br#"{"id":"req-1"}"#,
b"[]",
b"not json",
] {
assert!(
jsonrpc_empty_success(DELETE, body, &id()).is_none(),
"{}",
String::from_utf8_lossy(body)
);
}
}
#[test]
fn jsonrpc_result_less_envelope_with_another_id_is_an_error() {
for body in [
&br#"{"jsonrpc":"2.0","id":"req-2"}"#[..],
br#"{"jsonrpc":"2.0"}"#,
] {
let got = jsonrpc_empty_success(DELETE, body, &id());
assert!(
matches!(got, Some(Err(ClientError::Transport(ref m))) if m.contains("id")),
"{got:?}"
);
}
}
#[test]
fn rest_empty_body_is_success_only_for_empty_methods() {
assert!(rest_empty_success(DELETE, b""));
assert!(rest_empty_success(DELETE, b" \r\n"));
assert!(!rest_empty_success(DELETE, b"{}"));
assert!(!rest_empty_success("GetTask", b""));
}
}