1use a2a::*;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use crate::client::A2AClient;
8use crate::jsonrpc::JsonRpcTransportFactory;
9use crate::middleware::CallInterceptor;
10use crate::rest::RestTransportFactory;
11use crate::transport::TransportFactory;
12
13#[derive(Debug, Clone, Hash, PartialEq, Eq)]
15struct TransportKey {
16 protocol: String,
17 major_version: u64,
18}
19
20impl TransportKey {
21 fn from_interface(iface: &AgentInterface) -> Self {
22 let major = iface
23 .protocol_version
24 .split('.')
25 .next()
26 .and_then(|s| s.parse::<u64>().ok())
27 .unwrap_or(1);
28 TransportKey {
29 protocol: iface.protocol_binding.clone(),
30 major_version: major,
31 }
32 }
33
34 fn from_protocol(protocol: &str, version: &str) -> Self {
35 let major = version
36 .split('.')
37 .next()
38 .and_then(|s| s.parse::<u64>().ok())
39 .unwrap_or(1);
40 TransportKey {
41 protocol: protocol.to_string(),
42 major_version: major,
43 }
44 }
45}
46
47pub struct A2AClientFactory {
54 factories: HashMap<TransportKey, Arc<dyn TransportFactory>>,
55 preferred_bindings: Vec<String>,
56 interceptors: Vec<Arc<dyn CallInterceptor>>,
57}
58
59impl A2AClientFactory {
60 pub fn builder() -> A2AClientFactoryBuilder {
62 A2AClientFactoryBuilder::new()
63 }
64
65 pub async fn create_from_card(
73 &self,
74 card: &AgentCard,
75 ) -> Result<A2AClient<Box<dyn crate::Transport>>, A2AError> {
76 self.create_from_card_selecting(card)
77 .await
78 .map(|(client, _)| client)
79 }
80
81 pub async fn create_from_card_with_interface(
87 &self,
88 card: &AgentCard,
89 ) -> Result<(A2AClient<Box<dyn crate::Transport>>, AgentInterface), A2AError> {
90 self.create_from_card_selecting(card).await
91 }
92
93 async fn create_from_card_selecting(
94 &self,
95 card: &AgentCard,
96 ) -> Result<(A2AClient<Box<dyn crate::Transport>>, AgentInterface), A2AError> {
97 let mut candidates: Vec<(usize, &AgentInterface, &Arc<dyn TransportFactory>)> = Vec::new();
98
99 for iface in &card.supported_interfaces {
100 let key = TransportKey::from_interface(iface);
101 if let Some(factory) = self.factories.get(&key) {
102 let priority = self
103 .preferred_bindings
104 .iter()
105 .position(|b| b == &iface.protocol_binding)
106 .unwrap_or(usize::MAX);
107 candidates.push((priority, iface, factory));
108 }
109 }
110
111 if candidates.is_empty() {
112 return Err(A2AError::unsupported_operation(
113 "no compatible transport found for agent card interfaces",
114 ));
115 }
116
117 candidates.sort_by_key(|(prio, _, _)| *prio);
119
120 let mut last_err = None;
121 for (_prio, iface, factory) in &candidates {
122 match factory.create(card, iface).await {
123 Ok(transport) => {
124 let mut client =
125 A2AClient::new(transport).with_interceptors(self.interceptors.clone());
126 if let Some(tenant) = iface.tenant.as_deref().filter(|t| !t.is_empty()) {
131 client = client.with_tenant(tenant);
132 }
133 return Ok((client, (*iface).clone()));
134 }
135 Err(e) => {
136 tracing::debug!(
137 protocol = %iface.protocol_binding,
138 url = %iface.url,
139 error = %e,
140 "transport creation failed, trying next"
141 );
142 last_err = Some(e);
143 }
144 }
145 }
146
147 Err(last_err.unwrap_or_else(|| A2AError::internal("failed to create transport")))
148 }
149}
150
151pub struct A2AClientFactoryBuilder {
153 factories: HashMap<TransportKey, Arc<dyn TransportFactory>>,
154 preferred_bindings: Vec<String>,
155 interceptors: Vec<Arc<dyn CallInterceptor>>,
156 include_defaults: bool,
157}
158
159impl A2AClientFactoryBuilder {
160 fn new() -> Self {
161 A2AClientFactoryBuilder {
162 factories: HashMap::new(),
163 preferred_bindings: vec![
164 TRANSPORT_PROTOCOL_JSONRPC.to_string(),
165 TRANSPORT_PROTOCOL_HTTP_JSON.to_string(),
166 ],
167 interceptors: Vec::new(),
168 include_defaults: true,
169 }
170 }
171
172 pub fn register(mut self, factory: Arc<dyn TransportFactory>) -> Self {
174 let key = TransportKey::from_protocol(factory.protocol(), VERSION);
175 self.factories.insert(key, factory);
176 self
177 }
178
179 pub fn preferred_bindings(mut self, bindings: Vec<String>) -> Self {
181 self.preferred_bindings = bindings;
182 self
183 }
184
185 pub fn with_interceptor(mut self, interceptor: Arc<dyn CallInterceptor>) -> Self {
187 self.interceptors.push(interceptor);
188 self
189 }
190
191 pub fn no_defaults(mut self) -> Self {
193 self.include_defaults = false;
194 self
195 }
196
197 pub fn build(mut self) -> A2AClientFactory {
199 if self.include_defaults {
200 let jsonrpc_key = TransportKey::from_protocol(TRANSPORT_PROTOCOL_JSONRPC, VERSION);
201 self.factories
202 .entry(jsonrpc_key)
203 .or_insert_with(|| Arc::new(JsonRpcTransportFactory::new(None)));
204
205 let rest_key = TransportKey::from_protocol(TRANSPORT_PROTOCOL_HTTP_JSON, VERSION);
206 self.factories
207 .entry(rest_key)
208 .or_insert_with(|| Arc::new(RestTransportFactory::new(None)));
209 }
210
211 A2AClientFactory {
212 factories: self.factories,
213 preferred_bindings: self.preferred_bindings,
214 interceptors: self.interceptors,
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_transport_key_from_interface() {
225 let iface = AgentInterface::new("http://localhost", "jsonrpc");
226 let key = TransportKey::from_interface(&iface);
227 assert_eq!(key.protocol, "jsonrpc");
228 }
229
230 #[test]
231 fn test_transport_key_from_interface_no_version() {
232 let mut iface = AgentInterface::new("http://localhost", "rest");
233 iface.protocol_version = "bad".to_string();
234 let key = TransportKey::from_interface(&iface);
235 assert_eq!(key.major_version, 1); }
237
238 #[test]
239 fn test_transport_key_from_protocol() {
240 let key = TransportKey::from_protocol("jsonrpc", "2.3.4");
241 assert_eq!(key.protocol, "jsonrpc");
242 assert_eq!(key.major_version, 2);
243 }
244
245 #[test]
246 fn test_builder_defaults() {
247 let factory = A2AClientFactory::builder().build();
248 assert_eq!(factory.factories.len(), 2); assert_eq!(factory.preferred_bindings.len(), 2);
250 }
251
252 #[test]
253 fn test_builder_no_defaults() {
254 let factory = A2AClientFactory::builder().no_defaults().build();
255 assert!(factory.factories.is_empty());
256 }
257
258 #[test]
259 fn test_builder_preferred_bindings() {
260 let factory = A2AClientFactory::builder()
261 .preferred_bindings(vec!["grpc".to_string()])
262 .build();
263 assert_eq!(factory.preferred_bindings, vec!["grpc"]);
264 }
265
266 #[test]
267 fn test_builder_with_interceptor() {
268 use crate::middleware::LoggingInterceptor;
269 let factory = A2AClientFactory::builder()
270 .with_interceptor(Arc::new(LoggingInterceptor))
271 .build();
272 assert_eq!(factory.interceptors.len(), 1);
273 }
274
275 #[tokio::test]
276 async fn test_create_from_card_no_matching_transport() {
277 let factory = A2AClientFactory::builder().no_defaults().build();
278 let card = AgentCard {
279 name: "test".into(),
280 description: "test agent".into(),
281 version: "1.0".into(),
282 supported_interfaces: vec![AgentInterface::new("http://localhost", "unknown")],
283 capabilities: AgentCapabilities::default(),
284 default_input_modes: vec!["text".into()],
285 default_output_modes: vec!["text".into()],
286 skills: vec![],
287 provider: None,
288 documentation_url: None,
289 icon_url: None,
290 security_schemes: None,
291 security_requirements: None,
292 signatures: None,
293 };
294 let result = factory.create_from_card(&card).await;
295 assert!(result.is_err());
296 }
297
298 #[tokio::test]
299 async fn test_create_from_card_with_interface_returns_selected_interface() {
300 let factory = A2AClientFactory::builder().build();
301 let mut iface = AgentInterface::new("http://localhost/jsonrpc", TRANSPORT_PROTOCOL_JSONRPC);
302 iface.tenant = Some("tenant-42".to_string());
303 let card = AgentCard {
304 name: "test".into(),
305 description: "test agent".into(),
306 version: "1.0".into(),
307 supported_interfaces: vec![iface],
308 capabilities: AgentCapabilities::default(),
309 default_input_modes: vec!["text".into()],
310 default_output_modes: vec!["text".into()],
311 skills: vec![],
312 provider: None,
313 documentation_url: None,
314 icon_url: None,
315 security_schemes: None,
316 security_requirements: None,
317 signatures: None,
318 };
319
320 let (_client, selected) = factory
321 .create_from_card_with_interface(&card)
322 .await
323 .unwrap();
324 assert_eq!(selected.protocol_binding, TRANSPORT_PROTOCOL_JSONRPC);
325 assert_eq!(selected.tenant.as_deref(), Some("tenant-42"));
326 }
327
328 fn card_with_tenant(tenant: Option<&str>) -> AgentCard {
329 let mut iface = AgentInterface::new("http://localhost/jsonrpc", TRANSPORT_PROTOCOL_JSONRPC);
330 iface.tenant = tenant.map(str::to_string);
331 AgentCard {
332 name: "test".into(),
333 description: "test agent".into(),
334 version: "1.0".into(),
335 supported_interfaces: vec![iface],
336 capabilities: AgentCapabilities::default(),
337 default_input_modes: vec!["text".into()],
338 default_output_modes: vec!["text".into()],
339 skills: vec![],
340 provider: None,
341 documentation_url: None,
342 icon_url: None,
343 security_schemes: None,
344 security_requirements: None,
345 signatures: None,
346 }
347 }
348
349 #[tokio::test]
352 async fn test_create_from_card_applies_the_declared_tenant() {
353 let factory = A2AClientFactory::builder().build();
354 let client = factory
355 .create_from_card(&card_with_tenant(Some("tenant-42")))
356 .await
357 .unwrap();
358 assert_eq!(client.tenant(), Some("tenant-42"));
359 }
360
361 #[tokio::test]
364 async fn test_create_from_card_without_a_declared_tenant() {
365 let factory = A2AClientFactory::builder().build();
366 let client = factory
367 .create_from_card(&card_with_tenant(None))
368 .await
369 .unwrap();
370 assert_eq!(client.tenant(), None);
371 }
372
373 #[tokio::test]
376 async fn test_create_from_card_treats_an_empty_tenant_as_undeclared() {
377 let factory = A2AClientFactory::builder().build();
378 let client = factory
379 .create_from_card(&card_with_tenant(Some("")))
380 .await
381 .unwrap();
382 assert_eq!(client.tenant(), None);
383 }
384}