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