use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error;
use tokio::sync::RwLock;
use a2a_rs::domain::AgentCard;
#[derive(Debug, Error)]
#[error("could not read the agent card at {endpoint}: {reason}")]
pub struct CardSourceError {
pub endpoint: String,
pub reason: String,
}
#[async_trait]
pub trait CardSource: Send + Sync {
async fn fetch(&self, endpoint: &str) -> Result<AgentCard, CardSourceError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HttpCardSource;
impl HttpCardSource {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl CardSource for HttpCardSource {
async fn fetch(&self, endpoint: &str) -> Result<AgentCard, CardSourceError> {
a2a_rs::fetch_agent_card(endpoint)
.await
.map_err(|e| CardSourceError {
endpoint: endpoint.to_string(),
reason: e.to_string(),
})
}
}
#[derive(Clone, Default)]
pub struct InMemoryCardSource {
cards: Arc<RwLock<HashMap<String, AgentCard>>>,
}
impl InMemoryCardSource {
pub fn new() -> Self {
Self::default()
}
pub async fn insert(&self, endpoint: impl Into<String>, card: AgentCard) {
self.cards.write().await.insert(endpoint.into(), card);
}
}
#[async_trait]
impl CardSource for InMemoryCardSource {
async fn fetch(&self, endpoint: &str) -> Result<AgentCard, CardSourceError> {
self.cards
.read()
.await
.get(endpoint)
.cloned()
.ok_or_else(|| CardSourceError {
endpoint: endpoint.to_string(),
reason: "no card registered for this endpoint".to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn in_memory_source_answers_only_for_known_endpoints() {
let source = InMemoryCardSource::new();
let card = AgentCard {
name: "Weather Agent".to_string(),
..Default::default()
};
source.insert("http://127.0.0.1:8080", card).await;
assert_eq!(
source.fetch("http://127.0.0.1:8080").await.unwrap().name,
"Weather Agent"
);
let err = source.fetch("http://127.0.0.1:9999").await.unwrap_err();
assert_eq!(err.endpoint, "http://127.0.0.1:9999");
assert!(err.to_string().contains("http://127.0.0.1:9999"));
}
}