use std::sync::Arc;
use axum::Router;
use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use crate::domain::a2a::AgentCard;
use crate::domain::a2a::well_known::AGENT_CARD_WELL_KNOWN_PATH;
pub fn serve_well_known(card: Arc<AgentCard>) -> Router {
Router::new()
.route(AGENT_CARD_WELL_KNOWN_PATH, get(get_card))
.with_state(card)
}
async fn get_card(State(card): State<Arc<AgentCard>>) -> Response {
let Ok(body) = serde_json::to_vec(&*card) else {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
};
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
body,
)
.into_response()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use super::serve_well_known;
use crate::domain::a2a::version::A2aVersion;
use crate::domain::a2a::well_known::AGENT_CARD_WELL_KNOWN_PATH;
use crate::domain::a2a::{AgentCard, AgentCardBuilder, AgentInterface, JSONRPC};
type TestError = Box<dyn std::error::Error>;
fn sample_card() -> Result<Arc<AgentCard>, TestError> {
let card = AgentCardBuilder::new()
.with_name("Test Agent")
.with_description("A test agent card for the well-known path.")
.with_version("1.0.0")
.with_interface(AgentInterface {
url: url::Url::parse("https://example.com/a2a/v1")?,
protocol_binding: JSONRPC.clone(),
protocol_version: A2aVersion::V1_0,
tenant: None,
})
.with_input_mode("text/plain")
.with_output_mode("text/plain")
.build()?;
Ok(Arc::new(card))
}
#[tokio::test]
async fn get_returns_200_with_the_card_json() -> Result<(), TestError> {
let card = sample_card()?;
let router = serve_well_known(card);
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri(AGENT_CARD_WELL_KNOWN_PATH)
.body(Body::empty())
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?,
)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert_eq!(response.status(), StatusCode::OK);
let content_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
assert_eq!(content_type.as_deref(), Some("application/json"));
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let obj = document
.as_object()
.ok_or("the response body must be a JSON object")?;
assert_eq!(
obj.get("name").and_then(serde_json::Value::as_str),
Some("Test Agent")
);
assert_eq!(
obj.get("description").and_then(serde_json::Value::as_str),
Some("A test agent card for the well-known path.")
);
Ok(())
}
#[tokio::test]
async fn post_returns_method_not_allowed() -> Result<(), TestError> {
let card = sample_card()?;
let router = serve_well_known(card);
let response = router
.oneshot(
Request::builder()
.method("POST")
.uri(AGENT_CARD_WELL_KNOWN_PATH)
.body(Body::from("{}"))
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?,
)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
Ok(())
}
#[tokio::test]
async fn hot_swapping_the_card_is_observable_on_the_next_request() -> Result<(), TestError> {
let _first = sample_card()?;
let second = AgentCardBuilder::new()
.with_name("Replaced Agent")
.with_description("The replacement card.")
.with_version("1.0.0")
.with_interface(AgentInterface {
url: url::Url::parse("https://example.com/a2a/v1")?,
protocol_binding: JSONRPC.clone(),
protocol_version: A2aVersion::V1_0,
tenant: None,
})
.with_input_mode("text/plain")
.with_output_mode("text/plain")
.build()?;
let second = Arc::new(second);
let router = serve_well_known(Arc::clone(&second));
let response = router
.oneshot(
Request::builder()
.method("GET")
.uri(AGENT_CARD_WELL_KNOWN_PATH)
.body(Body::empty())
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?,
)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let name = document
.as_object()
.and_then(|o| o.get("name"))
.and_then(serde_json::Value::as_str)
.map(str::to_owned);
assert_eq!(name.as_deref(), Some("Replaced Agent"));
Ok(())
}
}