use serde::{Deserialize, Serialize};
pub(crate) const PARSE_ERROR: i32 = -32700;
pub(crate) const INVALID_REQUEST: i32 = -32600;
pub(crate) const METHOD_NOT_FOUND: i32 = -32601;
pub(crate) const INVALID_PARAMS: i32 = -32602;
pub(crate) const INTERNAL_ERROR: i32 = -32603;
pub(crate) const TASK_NOT_FOUND: i32 = -32001;
pub(crate) const TASK_NOT_CANCELABLE: i32 = -32002;
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct JsonRpcRequest {
#[serde(default)]
pub(crate) jsonrpc: String,
#[serde(default)]
pub(crate) id: serde_json::Value,
pub(crate) method: String,
#[serde(default)]
pub(crate) params: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct JsonRpcResponse {
pub(crate) jsonrpc: String,
pub(crate) id: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub(crate) fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
Self {
jsonrpc: "2.0".to_owned(),
id,
result: Some(result),
error: None,
}
}
pub(crate) fn failure(id: serde_json::Value, error: JsonRpcError) -> Self {
Self {
jsonrpc: "2.0".to_owned(),
id,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct JsonRpcError {
pub(crate) code: i32,
pub(crate) message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) data: Option<serde_json::Value>,
}
impl JsonRpcError {
pub(crate) fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
}
pub(crate) fn method_not_found(method: &str) -> JsonRpcError {
JsonRpcError::new(METHOD_NOT_FOUND, format!("method not found: {method}"))
}
pub(crate) fn invalid_params(reason: impl Into<String>) -> JsonRpcError {
JsonRpcError::new(INVALID_PARAMS, reason)
}
pub(crate) fn parse_error() -> JsonRpcError {
JsonRpcError::new(PARSE_ERROR, "parse error")
}
pub(crate) fn invalid_request(reason: impl Into<String>) -> JsonRpcError {
JsonRpcError::new(INVALID_REQUEST, reason)
}
pub(crate) fn internal(reason: impl Into<String>) -> JsonRpcError {
JsonRpcError::new(INTERNAL_ERROR, reason)
}
pub(crate) fn facade_error_to_jsonrpc(
err: &crate::a2a::core::task_facade::FacadeError,
) -> JsonRpcError {
use crate::a2a::core::task_facade::FacadeError;
use crate::a2a::core::task_manager::TaskError;
match err {
FacadeError::Task(TaskError::TaskNotFound { .. }) => {
JsonRpcError::new(TASK_NOT_FOUND, err.to_string())
}
FacadeError::Task(
TaskError::TaskAlreadyTerminal { .. } | TaskError::TaskInvalidTransition { .. },
) => JsonRpcError::new(TASK_NOT_CANCELABLE, err.to_string()),
}
}
pub(crate) fn push_error_to_jsonrpc(
err: &crate::a2a::core::push_notifications::PushNotificationError,
) -> JsonRpcError {
use crate::a2a::core::push_notifications::PushNotificationError;
match err {
PushNotificationError::InvalidInput { reason } => invalid_params(reason.clone()),
}
}
pub(crate) fn convert_error_to_jsonrpc(
err: &crate::a2a::jsonrpc::convert::ConvertError,
) -> JsonRpcError {
invalid_params(err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn success_serializes_without_error_key() {
let resp = JsonRpcResponse::success(json!(1), json!({"ok": true}));
let value = serde_json::to_value(&resp).expect("serialize success");
assert_eq!(value["jsonrpc"], json!("2.0"));
assert_eq!(value["id"], json!(1));
assert_eq!(value["result"], json!({"ok": true}));
assert!(
value.get("error").is_none(),
"success response must omit the error key"
);
}
#[test]
fn failure_serializes_without_result_key() {
let resp = JsonRpcResponse::failure(json!("abc"), method_not_found("foo/bar"));
let value = serde_json::to_value(&resp).expect("serialize failure");
assert_eq!(value["jsonrpc"], json!("2.0"));
assert_eq!(value["id"], json!("abc"));
assert_eq!(value["error"]["code"], json!(METHOD_NOT_FOUND));
assert!(
value.get("result").is_none(),
"failure response must omit the result key"
);
assert!(
value["error"].get("data").is_none(),
"errors without data must omit the data key"
);
}
#[test]
fn numeric_id_round_trips_request_to_response() {
let req: JsonRpcRequest =
serde_json::from_value(json!({"jsonrpc": "2.0", "id": 42, "method": "tasks/get"}))
.expect("deserialize request");
assert_eq!(req.id, json!(42));
let resp = JsonRpcResponse::success(req.id.clone(), json!(null));
assert_eq!(resp.id, json!(42), "numeric id must echo back unchanged");
}
#[test]
fn string_id_round_trips_request_to_response() {
let req: JsonRpcRequest =
serde_json::from_value(json!({"jsonrpc": "2.0", "id": "req-7", "method": "tasks/get"}))
.expect("deserialize request");
assert_eq!(req.id, json!("req-7"));
let resp = JsonRpcResponse::failure(req.id.clone(), internal("boom"));
assert_eq!(
resp.id,
json!("req-7"),
"string id must echo back unchanged"
);
}
#[test]
fn facade_task_not_found_maps_to_minus_32001() {
use crate::a2a::core::task_facade::FacadeError;
use crate::a2a::core::task_manager::TaskError;
let err = FacadeError::Task(TaskError::TaskNotFound { id: "x".into() });
let jsonrpc = facade_error_to_jsonrpc(&err);
assert_eq!(jsonrpc.code, TASK_NOT_FOUND);
assert_eq!(jsonrpc.code, -32001);
}
#[test]
fn facade_already_terminal_maps_to_minus_32002() {
use crate::a2a::core::task_facade::FacadeError;
use crate::a2a::core::task_manager::TaskError;
let err = FacadeError::Task(TaskError::TaskAlreadyTerminal {
task_id: "x".into(),
state: "Completed".into(),
});
let jsonrpc = facade_error_to_jsonrpc(&err);
assert_eq!(jsonrpc.code, TASK_NOT_CANCELABLE);
assert_eq!(jsonrpc.code, -32002);
}
#[test]
fn facade_invalid_transition_maps_to_not_cancelable() {
use crate::a2a::core::task_facade::FacadeError;
use crate::a2a::core::task_manager::TaskError;
let err = FacadeError::Task(TaskError::TaskInvalidTransition {
task_id: "x".into(),
from: "Submitted".into(),
to: "Completed".into(),
});
assert_eq!(facade_error_to_jsonrpc(&err).code, TASK_NOT_CANCELABLE);
}
#[test]
fn method_not_found_has_minus_32601() {
assert_eq!(method_not_found("foo/bar").code, -32601);
}
#[test]
fn push_invalid_input_maps_to_invalid_params() {
use crate::a2a::core::push_notifications::PushNotificationError;
let err = PushNotificationError::InvalidInput {
reason: "bad url".into(),
};
let jsonrpc = push_error_to_jsonrpc(&err);
assert_eq!(jsonrpc.code, INVALID_PARAMS);
assert!(jsonrpc.message.contains("bad url"));
}
}