zeph-a2a 0.15.0

A2A protocol client and server with agent discovery for Zeph
Documentation
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::jsonrpc::JsonRpcError;

#[derive(Debug, thiserror::Error)]
pub enum A2aError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    #[error("JSON serialization/deserialization failed: {0}")]
    Json(#[from] serde_json::Error),

    #[error("JSON-RPC error {code}: {message}")]
    JsonRpc { code: i32, message: String },

    #[error("agent discovery failed for {url}: {reason}")]
    Discovery { url: String, reason: String },

    #[error("SSE stream error: {0}")]
    Stream(String),

    #[error("server error: {0}")]
    Server(String),

    #[error("security policy violation: {0}")]
    Security(String),
}

impl From<JsonRpcError> for A2aError {
    fn from(e: JsonRpcError) -> Self {
        Self::JsonRpc {
            code: e.code,
            message: e.message,
        }
    }
}

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

    #[test]
    fn from_jsonrpc_error() {
        let rpc_err = JsonRpcError {
            code: -32001,
            message: "task not found".into(),
            data: None,
        };
        let err: A2aError = rpc_err.into();
        match err {
            A2aError::JsonRpc { code, message } => {
                assert_eq!(code, -32001);
                assert_eq!(message, "task not found");
            }
            _ => panic!("expected JsonRpc variant"),
        }
    }

    #[test]
    fn error_display() {
        let err = A2aError::Discovery {
            url: "http://example.com".into(),
            reason: "connection refused".into(),
        };
        assert_eq!(
            err.to_string(),
            "agent discovery failed for http://example.com: connection refused"
        );

        let err = A2aError::Stream("unexpected EOF".into());
        assert_eq!(err.to_string(), "SSE stream error: unexpected EOF");
    }

    #[test]
    fn security_error_display() {
        let err = A2aError::Security("TLS required but endpoint uses HTTP".into());
        assert_eq!(
            err.to_string(),
            "security policy violation: TLS required but endpoint uses HTTP"
        );
    }

    #[test]
    fn from_serde_json_error() {
        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
        let err: A2aError = json_err.into();
        assert!(matches!(err, A2aError::Json(_)));
    }
}