use async_trait::async_trait;
use crate::domain::{A2AError, AgentCard, AgentInterface};
use crate::port::Transport;
#[async_trait]
pub trait TransportFactory: Send + Sync {
fn protocol(&self) -> &str;
async fn create(
&self,
card: &AgentCard,
iface: &AgentInterface,
) -> Result<Box<dyn Transport>, A2AError>;
}
#[cfg(feature = "jsonrpc-client")]
pub struct JsonRpcTransportFactory;
#[cfg(feature = "jsonrpc-client")]
#[async_trait]
impl TransportFactory for JsonRpcTransportFactory {
fn protocol(&self) -> &str {
"JSONRPC"
}
async fn create(
&self,
_card: &AgentCard,
iface: &AgentInterface,
) -> Result<Box<dyn Transport>, A2AError> {
Ok(Box::new(super::jsonrpc_client::JsonRpcClient::new(
iface.url.clone(),
)))
}
}
#[cfg(feature = "http-client")]
pub struct ConnectRpcTransportFactory;
#[cfg(feature = "http-client")]
#[async_trait]
impl TransportFactory for ConnectRpcTransportFactory {
fn protocol(&self) -> &str {
"CONNECTRPC"
}
async fn create(
&self,
_card: &AgentCard,
iface: &AgentInterface,
) -> Result<Box<dyn Transport>, A2AError> {
iface.url.parse::<http::Uri>().map_err(|e| {
A2AError::InvalidParams(format!("invalid interface url {}: {e}", iface.url))
})?;
Ok(Box::new(super::http::HttpClient::new(iface.url.clone())))
}
}
#[derive(Default)]
pub struct TransportNegotiator {
factories: Vec<Box<dyn TransportFactory>>,
}
impl TransportNegotiator {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, factory: impl TransportFactory + 'static) -> Self {
self.factories.push(Box::new(factory));
self
}
pub fn supported(&self) -> impl Iterator<Item = &str> {
self.factories.iter().map(|f| f.protocol())
}
pub async fn negotiate(&self, card: &AgentCard) -> Result<Box<dyn Transport>, A2AError> {
for factory in &self.factories {
for iface in &card.supported_interfaces {
if iface.protocol_binding == factory.protocol()
&& version_compatible(&iface.protocol_version)
{
match factory.create(card, iface).await {
Ok(transport) => return Ok(transport),
Err(_err) => continue,
}
}
}
}
Err(A2AError::UnsupportedOperation(format!(
"no compatible transport: client supports [{}], card offers [{}]",
self.supported().collect::<Vec<_>>().join(", "),
card.supported_interfaces
.iter()
.map(|i| i.protocol_binding.as_str())
.collect::<Vec<_>>()
.join(", "),
)))
}
}
fn version_compatible(version: &str) -> bool {
version.is_empty() || version.split('.').next() == Some("1")
}
pub fn default_registry() -> TransportNegotiator {
#[allow(unused_mut)]
let mut negotiator = TransportNegotiator::new();
#[cfg(feature = "http-client")]
{
negotiator = negotiator.with(ConnectRpcTransportFactory);
}
#[cfg(feature = "jsonrpc-client")]
{
negotiator = negotiator.with(JsonRpcTransportFactory);
}
negotiator
}
#[cfg(any(feature = "http-client", feature = "jsonrpc-client"))]
pub async fn connect(
base_url: &str,
negotiator: &TransportNegotiator,
) -> Result<Box<dyn Transport>, A2AError> {
let card = fetch_agent_card(base_url).await?;
negotiator.negotiate(&card).await
}
#[cfg(any(feature = "http-client", feature = "jsonrpc-client"))]
pub async fn fetch_agent_card(base_url: &str) -> Result<AgentCard, A2AError> {
use crate::adapter::error::HttpClientError;
let client = reqwest::Client::new();
let base = base_url.trim_end_matches('/');
for path in [".well-known/agent-card.json", "agent-card"] {
let url = format!("{base}/{path}");
let resp = client
.get(&url)
.send()
.await
.map_err(HttpClientError::Reqwest)?;
if resp.status().is_success() {
return resp
.json::<AgentCard>()
.await
.map_err(|e| A2AError::Internal(format!("Failed to parse agent card JSON: {e}")));
}
}
Err(A2AError::Internal(format!(
"Agent card not found at {base_url}"
)))
}