a2a_protocol_client/builder/
transport_factory.rs1use crate::client::A2aClient;
13use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_JSONRPC, BINDING_REST};
14use crate::error::{ClientError, ClientResult};
15use crate::retry::RetryTransport;
16use crate::transport::{JsonRpcTransport, RestTransport, Transport};
17
18use super::ClientBuilder;
19
20impl ClientBuilder {
21 #[allow(clippy::too_many_lines)]
29 pub fn build(self) -> ClientResult<A2aClient> {
30 if self.config.request_timeout.is_zero() {
31 return Err(ClientError::Transport(
32 "request_timeout must be non-zero".into(),
33 ));
34 }
35 if self.config.stream_connect_timeout.is_zero() {
36 return Err(ClientError::Transport(
37 "stream_connect_timeout must be non-zero".into(),
38 ));
39 }
40 if self.config.connection_timeout.is_zero() {
41 return Err(ClientError::Transport(
42 "connection_timeout must be non-zero".into(),
43 ));
44 }
45
46 let transport: Box<dyn Transport> = if let Some(t) = self.transport_override {
47 t
48 } else {
49 let binding = self
50 .preferred_binding
51 .unwrap_or_else(|| BINDING_JSONRPC.into());
52
53 match binding.as_str() {
54 BINDING_JSONRPC => {
55 let t = JsonRpcTransport::with_all_timeouts(
56 &self.endpoint,
57 self.config.request_timeout,
58 self.config.stream_connect_timeout,
59 self.config.connection_timeout,
60 )?
61 .with_max_response_size(self.config.max_response_size);
62 Box::new(t)
63 }
64 BINDING_REST | BINDING_HTTP_JSON => {
69 let t = RestTransport::with_all_timeouts(
70 &self.endpoint,
71 self.config.request_timeout,
72 self.config.stream_connect_timeout,
73 self.config.connection_timeout,
74 )?
75 .with_max_response_size(self.config.max_response_size);
76 Box::new(t)
77 }
78 #[cfg(feature = "grpc")]
79 BINDING_GRPC => {
80 return Err(ClientError::Transport(
84 "gRPC transport requires async connect; \
85 use ClientBuilder::build_grpc() or \
86 with_custom_transport(GrpcTransport::connect(...))"
87 .into(),
88 ));
89 }
90 #[cfg(not(feature = "grpc"))]
91 BINDING_GRPC => {
92 return Err(ClientError::Transport(
93 "gRPC transport requires the `grpc` feature flag".into(),
94 ));
95 }
96 other => {
97 return Err(ClientError::Transport(format!(
98 "unknown protocol binding: {other}"
99 )));
100 }
101 }
102 };
103
104 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
106 Box::new(RetryTransport::new(transport, policy))
107 } else {
108 transport
109 };
110
111 Ok(A2aClient::new(transport, self.interceptors, self.config))
112 }
113
114 #[cfg(feature = "grpc")]
124 pub async fn build_grpc(self) -> ClientResult<A2aClient> {
125 use crate::transport::grpc::{GrpcTransport, GrpcTransportConfig};
126
127 if self.config.request_timeout.is_zero() {
128 return Err(ClientError::Transport(
129 "request_timeout must be non-zero".into(),
130 ));
131 }
132 if self.config.stream_connect_timeout.is_zero() {
137 return Err(ClientError::Transport(
138 "stream_connect_timeout must be non-zero".into(),
139 ));
140 }
141
142 let transport: Box<dyn Transport> = if let Some(t) = self.transport_override {
143 t
144 } else {
145 let grpc_config = GrpcTransportConfig::default()
146 .with_timeout(self.config.request_timeout)
147 .with_connect_timeout(self.config.connection_timeout)
148 .with_max_message_size(self.config.max_response_size)
152 .with_bare_address_scheme(self.grpc_bare_address_scheme);
153 #[cfg(feature = "grpc-tls")]
154 let grpc_config = match self.grpc_tls_config {
155 Some(tls) => grpc_config.with_tls_config(tls),
156 None => grpc_config,
157 };
158 let t = GrpcTransport::connect_with_config(&self.endpoint, grpc_config)
164 .await?
165 .with_stream_connect_timeout(self.config.stream_connect_timeout);
166 Box::new(t)
167 };
168
169 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
170 Box::new(RetryTransport::new(transport, policy))
171 } else {
172 transport
173 };
174
175 Ok(A2aClient::new(transport, self.interceptors, self.config))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::super::*;
182 use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_REST};
183 use std::time::Duration;
184
185 #[test]
186 fn builder_defaults_to_jsonrpc() {
187 let client = ClientBuilder::new("http://localhost:8080")
188 .build()
189 .expect("build");
190 let _ = client;
191 }
192
193 #[test]
194 fn builder_rest_transport() {
195 let client = ClientBuilder::new("http://localhost:8080")
196 .with_protocol_binding(BINDING_REST)
197 .build()
198 .expect("build");
199 let _ = client;
200 }
201
202 #[test]
203 fn builder_accepts_spec_http_json_binding() {
204 let client = ClientBuilder::new("http://localhost:8080")
207 .with_protocol_binding(BINDING_HTTP_JSON)
208 .build()
209 .expect("HTTP+JSON binding should resolve to the REST transport");
210 let _ = client;
211 }
212
213 #[test]
214 fn builder_grpc_sync_build_returns_error() {
215 let result = ClientBuilder::new("http://localhost:8080")
216 .with_protocol_binding(BINDING_GRPC)
217 .build();
218 assert!(result.is_err());
219 }
220
221 #[test]
222 fn builder_invalid_url_returns_error() {
223 let result = ClientBuilder::new("not-a-url").build();
224 assert!(result.is_err());
225 }
226
227 #[test]
228 fn builder_zero_request_timeout_errors() {
229 let result = ClientBuilder::new("http://localhost:8080")
230 .with_timeout(Duration::ZERO)
231 .build();
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn builder_zero_stream_timeout_errors() {
237 let result = ClientBuilder::new("http://localhost:8080")
238 .with_stream_connect_timeout(Duration::ZERO)
239 .build();
240 assert!(result.is_err());
241 }
242
243 #[cfg(feature = "grpc")]
258 #[tokio::test]
259 async fn build_grpc_zero_stream_timeout_errors() {
260 let result = ClientBuilder::new("http://127.0.0.1:1")
261 .with_stream_connect_timeout(Duration::ZERO)
262 .build_grpc()
263 .await;
264 let err = result.expect_err("a zero stream_connect_timeout is invalid");
265 assert!(
266 err.to_string().contains("stream_connect_timeout"),
267 "the error must name the knob that was wrong, not the dial that \
268 never should have been attempted: {err}"
269 );
270 }
271
272 #[test]
273 fn builder_zero_connection_timeout_errors() {
274 let result = ClientBuilder::new("http://localhost:8080")
275 .with_connection_timeout(Duration::ZERO)
276 .build();
277 assert!(result.is_err());
278 }
279
280 #[test]
281 fn builder_unknown_binding_errors() {
282 let result = ClientBuilder::new("http://localhost:8080")
283 .with_protocol_binding("UNKNOWN_PROTOCOL")
284 .build();
285 assert!(result.is_err());
286 }
287
288 #[test]
289 fn builder_rest_with_retry_policy() {
290 use crate::retry::RetryPolicy;
291
292 let client = ClientBuilder::new("http://localhost:8080")
294 .with_protocol_binding(BINDING_REST)
295 .with_retry_policy(RetryPolicy::default())
296 .build()
297 .expect("build");
298 let _ = client;
299 }
300
301 #[test]
302 fn builder_jsonrpc_with_retry_policy() {
303 use crate::retry::RetryPolicy;
304
305 let client = ClientBuilder::new("http://localhost:8080")
307 .with_retry_policy(RetryPolicy::default())
308 .build()
309 .expect("build");
310 let _ = client;
311 }
312
313 #[test]
314 fn builder_from_card_rejects_incompatible_binding() {
315 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
316
317 let card = AgentCard {
318 url: None,
319 name: "test".into(),
320 version: "1.0".into(),
321 description: "Test agent".into(),
322 supported_interfaces: vec![AgentInterface {
323 url: "http://localhost:9090".into(),
324 protocol_binding: "UNKNOWN".into(),
325 protocol_version: "1.0.0".into(),
326 tenant: None,
327 }],
328 provider: None,
329 icon_url: None,
330 documentation_url: None,
331 capabilities: AgentCapabilities::none(),
332 security_schemes: None,
333 security_requirements: None,
334 default_input_modes: vec![],
335 default_output_modes: vec![],
336 skills: vec![],
337 signatures: None,
338 };
339
340 let result = ClientBuilder::from_card(&card).unwrap().build();
341 assert!(result.is_err(), "unknown binding should fail");
342 }
343}