klieo-a2a 0.7.0

Durable A2A v1.0 protocol layer atop klieo-bus traits.
Documentation
//! A2A error type with JSON-RPC code mapping.

use crate::envelope::{JsonRpcError, JsonRpcResponse};
use thiserror::Error;

/// Errors returned by the A2A server / handler.
#[derive(Debug, Error)]
pub enum A2aError {
    /// JSON-RPC method name not recognised. Maps to JSON-RPC -32601.
    #[error("method not found: {0}")]
    MethodNotFound(String),
    /// Params payload could not be decoded into the method's param type.
    /// Maps to JSON-RPC -32602.
    #[error("invalid params: {0}")]
    InvalidParams(String),
    /// JSON parse failure. Maps to JSON-RPC -32700.
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),
    /// Bus-layer failure (subscribe / publish / KV).
    #[error("bus error: {0}")]
    Bus(#[from] klieo_core::error::BusError),
    /// Task id not present in the store.
    #[error("task not found: {0}")]
    TaskNotFound(String),
    /// Catch-all server-side failure. Maps to JSON-RPC -32000.
    #[error("server error: {0}")]
    Server(String),
    /// Authentication or authorisation failure. Maps to A2A-specific -32001.
    #[error("unauthorized: {0}")]
    Unauthorized(String),
}

impl A2aError {
    /// Map this error onto a [`JsonRpcResponse`] carrying the matching
    /// JSON-RPC error code.
    pub fn to_json_rpc_error(&self, request_id: serde_json::Value) -> JsonRpcResponse {
        let (code, message) = match self {
            Self::MethodNotFound(_) => (-32601, self.to_string()),
            Self::InvalidParams(_) => (-32602, self.to_string()),
            Self::Json(_) => (-32700, self.to_string()),
            Self::Bus(_) | Self::TaskNotFound(_) | Self::Server(_) => (-32000, self.to_string()),
            Self::Unauthorized(_) => (-32001, self.to_string()),
        };
        JsonRpcResponse {
            jsonrpc: "2.0".to_string(),
            id: request_id,
            result: None,
            error: Some(JsonRpcError {
                code,
                message,
                data: None,
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn method_not_found_maps_to_minus_32601() {
        let resp = A2aError::MethodNotFound("Foo".into()).to_json_rpc_error(json!(7));
        assert_eq!(resp.error.unwrap().code, -32601);
    }

    #[test]
    fn invalid_params_maps_to_minus_32602() {
        let resp = A2aError::InvalidParams("missing field".into()).to_json_rpc_error(json!("a"));
        assert_eq!(resp.error.unwrap().code, -32602);
    }

    #[test]
    fn json_error_maps_to_minus_32700() {
        let json_err = serde_json::from_str::<u32>("not json").unwrap_err();
        let resp = A2aError::from(json_err).to_json_rpc_error(json!(null));
        assert_eq!(resp.error.unwrap().code, -32700);
    }

    #[test]
    fn unauthorized_maps_to_minus_32001() {
        let resp = A2aError::Unauthorized("no token".into()).to_json_rpc_error(json!(1));
        assert_eq!(resp.error.unwrap().code, -32001);
    }
}

// `error` is referenced by lib.rs; the module declaration in lib.rs must
// match the actual module we want exposed. T1's lib.rs declares
// `pub mod error;` so this file is the canonical location.