use a2a_protocol_types::{AgentCard, AgentInterface};
use super::SUPPORTED_PROTOCOL_MAJOR;
use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_JSONRPC, BINDING_REST};
use crate::error::{ClientError, ClientResult};
pub(super) fn protocol_major(version: &str) -> Option<u32> {
let rest = version.strip_prefix(['v', 'V']).unwrap_or(version);
let digits = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
rest[..digits].parse().ok()
}
pub(super) fn is_compatible(version: &str) -> bool {
version.is_empty() || protocol_major(version) == Some(SUPPORTED_PROTOCOL_MAJOR)
}
pub(super) fn canonical_binding(binding: &str) -> Option<&'static str> {
[BINDING_JSONRPC, BINDING_HTTP_JSON, BINDING_GRPC]
.into_iter()
.find(|b| b.eq_ignore_ascii_case(binding))
.or_else(|| {
BINDING_REST
.eq_ignore_ascii_case(binding)
.then_some(BINDING_HTTP_JSON)
})
}
pub(super) fn candidates(
card: &AgentCard,
preferences: &[String],
) -> ClientResult<Vec<AgentInterface>> {
if card.supported_interfaces.is_empty() {
return Err(ClientError::InvalidEndpoint(
"agent card has no supported interfaces".into(),
));
}
let rank = |i: &AgentInterface| {
preferences
.iter()
.position(|p| p.eq_ignore_ascii_case(&i.protocol_binding))
.unwrap_or(preferences.len())
};
let mut usable: Vec<AgentInterface> = card
.supported_interfaces
.iter()
.filter(|i| is_compatible(&i.protocol_version))
.cloned()
.collect();
if usable.is_empty() {
let offered: Vec<String> = card
.supported_interfaces
.iter()
.map(|i| format!("{} {} at {}", i.protocol_binding, i.protocol_version, i.url))
.collect();
return Err(ClientError::InvalidEndpoint(format!(
"agent card offers no interface for A2A protocol major {SUPPORTED_PROTOCOL_MAJOR}; \
it offers: {}",
offered.join(", ")
)));
}
usable.sort_by_key(rank);
Ok(usable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_major_reads_the_leading_number() {
assert_eq!(protocol_major("1.0"), Some(1));
assert_eq!(protocol_major("1"), Some(1));
assert_eq!(protocol_major("v1.0"), Some(1));
assert_eq!(protocol_major("V2"), Some(2));
assert_eq!(protocol_major("0.3.0"), Some(0));
assert_eq!(protocol_major("12.1"), Some(12));
assert_eq!(protocol_major("1-preview"), Some(1));
assert_eq!(protocol_major("latest"), None);
assert_eq!(protocol_major("v"), None);
assert_eq!(protocol_major(""), None);
}
#[test]
fn compatibility_is_major_one_or_unstated() {
assert!(is_compatible("1.0"));
assert!(is_compatible(""));
assert!(!is_compatible("0.3"));
assert!(!is_compatible("2.0"));
assert!(!is_compatible("11.0"), "11 is not 1");
assert!(!is_compatible("latest"));
}
#[test]
fn canonical_binding_ignores_case_and_maps_rest() {
assert_eq!(canonical_binding("jsonrpc"), Some(BINDING_JSONRPC));
assert_eq!(canonical_binding("Http+Json"), Some(BINDING_HTTP_JSON));
assert_eq!(canonical_binding("rest"), Some(BINDING_HTTP_JSON));
assert_eq!(canonical_binding("grpc"), Some(BINDING_GRPC));
assert_eq!(canonical_binding("JSON-RPC"), None);
assert_eq!(canonical_binding(""), None);
}
}