use a2a_lf::{AgentCard, AgentInterface, TRANSPORT_PROTOCOL_HTTP_JSON, TRANSPORT_PROTOCOL_JSONRPC};
use reqwest::Method;
use reqwest::header::HeaderValue;
use serde::Serialize;
use serde_json::Value;
use crate::a2a::error::A2aError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum A2aBinding {
JsonRpc,
HttpJson,
}
impl A2aBinding {
pub fn as_str(self) -> &'static str {
match self {
Self::JsonRpc => TRANSPORT_PROTOCOL_JSONRPC,
Self::HttpJson => TRANSPORT_PROTOCOL_HTTP_JSON,
}
}
fn from_declared(declared: &str) -> Option<Self> {
let declared = declared.trim();
[Self::JsonRpc, Self::HttpJson]
.into_iter()
.find(|binding| declared.eq_ignore_ascii_case(binding.as_str()))
}
}
impl std::fmt::Display for A2aBinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub(super) fn select_interface(card: &AgentCard) -> Option<(A2aBinding, &AgentInterface)> {
card.supported_interfaces.iter().find_map(|interface| {
let binding = A2aBinding::from_declared(&interface.protocol_binding)?;
speaks_version(&interface.protocol_version).then_some((binding, interface))
})
}
pub(super) fn speaks_version(declared: &str) -> bool {
let mut parts = declared.trim().split('.');
let major = parts.next().and_then(|part| part.parse::<u32>().ok());
let rest_numeric = parts.all(|part| part.parse::<u32>().is_ok());
major == Some(1) && rest_numeric
}
pub(super) fn is_dot_segment(segment: &str) -> bool {
matches!(segment, "" | "." | "..")
}
pub(super) fn checked_id<'a>(method: &'static str, id: &'a str) -> Result<&'a str, A2aError> {
if is_dot_segment(id) {
return Err(A2aError::InvalidId { method });
}
Ok(id)
}
#[derive(Debug)]
pub(super) struct Operation {
pub(super) method: &'static str,
pub(super) params: Value,
pub(super) verb: Method,
pub(super) path: Vec<String>,
pub(super) query: Vec<(&'static str, String)>,
pub(super) extensions: Option<HeaderValue>,
}
impl Operation {
pub(super) fn new<P: Serialize>(
method: &'static str,
params: &P,
verb: Method,
path: impl IntoIterator<Item = String>,
) -> Result<Self, A2aError> {
Ok(Self {
method,
params: serde_json::to_value(params).map_err(|_| A2aError::Decode { method })?,
verb,
path: path.into_iter().collect(),
query: Vec::new(),
extensions: None,
})
}
pub(super) fn query(mut self, key: &'static str, value: Option<impl ToString>) -> Self {
if let Some(value) = value {
self.query.push((key, value.to_string()));
}
self
}
pub(super) fn activating(mut self, extensions: &[String]) -> Result<Self, A2aError> {
if extensions.is_empty() {
return Ok(self);
}
let well_formed = |uri: &String| {
!uri.is_empty() && !uri.contains(',') && !uri.chars().any(char::is_whitespace)
};
if !extensions.iter().all(well_formed) {
return Err(A2aError::InvalidExtensionUri);
}
let value = HeaderValue::try_from(extensions.join(","))
.map_err(|_| A2aError::InvalidExtensionUri)?;
self.extensions = Some(value);
Ok(self)
}
pub(super) fn has_body(&self) -> bool {
self.verb == Method::POST
}
}