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(
107 RetryTransport::new(transport, policy)
108 .with_peer_honouring_idempotency(self.peer_honours_idempotency),
109 )
110 } else {
111 transport
112 };
113
114 Ok(A2aClient::new(transport, self.interceptors, self.config))
115 }
116
117 #[cfg(feature = "grpc")]
127 pub async fn build_grpc(self) -> ClientResult<A2aClient> {
128 use crate::transport::grpc::{GrpcTransport, GrpcTransportConfig};
129
130 if self.config.request_timeout.is_zero() {
131 return Err(ClientError::Transport(
132 "request_timeout must be non-zero".into(),
133 ));
134 }
135 if self.config.stream_connect_timeout.is_zero() {
140 return Err(ClientError::Transport(
141 "stream_connect_timeout must be non-zero".into(),
142 ));
143 }
144
145 let transport: Box<dyn Transport> = if let Some(t) = self.transport_override {
146 t
147 } else {
148 let grpc_config = GrpcTransportConfig::default()
149 .with_timeout(self.config.request_timeout)
150 .with_connect_timeout(self.config.connection_timeout)
151 .with_max_message_size(self.config.max_response_size)
155 .with_bare_address_scheme(self.grpc_bare_address_scheme);
156 #[cfg(feature = "grpc-tls")]
157 let grpc_config = match self.grpc_tls_config {
158 Some(tls) => grpc_config.with_tls_config(tls),
159 None => grpc_config,
160 };
161 let t = GrpcTransport::connect_with_config(&self.endpoint, grpc_config)
167 .await?
168 .with_stream_connect_timeout(self.config.stream_connect_timeout);
169 Box::new(t)
170 };
171
172 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
173 Box::new(
174 RetryTransport::new(transport, policy)
175 .with_peer_honouring_idempotency(self.peer_honours_idempotency),
176 )
177 } else {
178 transport
179 };
180
181 Ok(A2aClient::new(transport, self.interceptors, self.config))
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::super::*;
188 use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_REST};
189 use std::time::Duration;
190
191 #[test]
192 fn builder_defaults_to_jsonrpc() {
193 let client = ClientBuilder::new("http://localhost:8080")
194 .build()
195 .expect("build");
196 let _ = client;
197 }
198
199 #[test]
200 fn builder_rest_transport() {
201 let client = ClientBuilder::new("http://localhost:8080")
202 .with_protocol_binding(BINDING_REST)
203 .build()
204 .expect("build");
205 let _ = client;
206 }
207
208 #[test]
209 fn builder_accepts_spec_http_json_binding() {
210 let client = ClientBuilder::new("http://localhost:8080")
213 .with_protocol_binding(BINDING_HTTP_JSON)
214 .build()
215 .expect("HTTP+JSON binding should resolve to the REST transport");
216 let _ = client;
217 }
218
219 #[test]
220 fn builder_grpc_sync_build_returns_error() {
221 let result = ClientBuilder::new("http://localhost:8080")
222 .with_protocol_binding(BINDING_GRPC)
223 .build();
224 assert!(result.is_err());
225 }
226
227 #[test]
228 fn builder_invalid_url_returns_error() {
229 let result = ClientBuilder::new("not-a-url").build();
230 assert!(result.is_err());
231 }
232
233 #[test]
234 fn builder_zero_request_timeout_errors() {
235 let result = ClientBuilder::new("http://localhost:8080")
236 .with_timeout(Duration::ZERO)
237 .build();
238 assert!(result.is_err());
239 }
240
241 #[test]
242 fn builder_zero_stream_timeout_errors() {
243 let result = ClientBuilder::new("http://localhost:8080")
244 .with_stream_connect_timeout(Duration::ZERO)
245 .build();
246 assert!(result.is_err());
247 }
248
249 #[cfg(feature = "grpc")]
264 #[tokio::test]
265 async fn build_grpc_zero_stream_timeout_errors() {
266 let result = ClientBuilder::new("http://127.0.0.1:1")
267 .with_stream_connect_timeout(Duration::ZERO)
268 .build_grpc()
269 .await;
270 let err = result.expect_err("a zero stream_connect_timeout is invalid");
271 assert!(
272 err.to_string().contains("stream_connect_timeout"),
273 "the error must name the knob that was wrong, not the dial that \
274 never should have been attempted: {err}"
275 );
276 }
277
278 #[test]
279 fn builder_zero_connection_timeout_errors() {
280 let result = ClientBuilder::new("http://localhost:8080")
281 .with_connection_timeout(Duration::ZERO)
282 .build();
283 assert!(result.is_err());
284 }
285
286 #[test]
287 fn builder_unknown_binding_errors() {
288 let result = ClientBuilder::new("http://localhost:8080")
289 .with_protocol_binding("UNKNOWN_PROTOCOL")
290 .build();
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn builder_rest_with_retry_policy() {
296 use crate::retry::RetryPolicy;
297
298 let client = ClientBuilder::new("http://localhost:8080")
300 .with_protocol_binding(BINDING_REST)
301 .with_retry_policy(RetryPolicy::default())
302 .build()
303 .expect("build");
304 let _ = client;
305 }
306
307 #[test]
308 fn builder_jsonrpc_with_retry_policy() {
309 use crate::retry::RetryPolicy;
310
311 let client = ClientBuilder::new("http://localhost:8080")
313 .with_retry_policy(RetryPolicy::default())
314 .build()
315 .expect("build");
316 let _ = client;
317 }
318
319 #[test]
320 fn builder_from_card_rejects_incompatible_binding() {
321 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
322
323 let card = AgentCard {
324 url: None,
325 name: "test".into(),
326 version: "1.0".into(),
327 description: "Test agent".into(),
328 supported_interfaces: vec![AgentInterface {
329 url: "http://localhost:9090".into(),
330 protocol_binding: "UNKNOWN".into(),
331 protocol_version: "1.0.0".into(),
332 tenant: None,
333 }],
334 provider: None,
335 icon_url: None,
336 documentation_url: None,
337 capabilities: AgentCapabilities::none(),
338 security_schemes: None,
339 security_requirements: None,
340 default_input_modes: vec![],
341 default_output_modes: vec![],
342 skills: vec![],
343 signatures: None,
344 };
345
346 let result = ClientBuilder::from_card(&card).unwrap().build();
347 assert!(result.is_err(), "unknown binding should fail");
348 }
349}