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 let t = GrpcTransport::connect_with_config(&self.endpoint, grpc_config)
158 .await?
159 .with_stream_connect_timeout(self.config.stream_connect_timeout);
160 Box::new(t)
161 };
162
163 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
164 Box::new(RetryTransport::new(transport, policy))
165 } else {
166 transport
167 };
168
169 Ok(A2aClient::new(transport, self.interceptors, self.config))
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::super::*;
176 use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_REST};
177 use std::time::Duration;
178
179 #[test]
180 fn builder_defaults_to_jsonrpc() {
181 let client = ClientBuilder::new("http://localhost:8080")
182 .build()
183 .expect("build");
184 let _ = client;
185 }
186
187 #[test]
188 fn builder_rest_transport() {
189 let client = ClientBuilder::new("http://localhost:8080")
190 .with_protocol_binding(BINDING_REST)
191 .build()
192 .expect("build");
193 let _ = client;
194 }
195
196 #[test]
197 fn builder_accepts_spec_http_json_binding() {
198 let client = ClientBuilder::new("http://localhost:8080")
201 .with_protocol_binding(BINDING_HTTP_JSON)
202 .build()
203 .expect("HTTP+JSON binding should resolve to the REST transport");
204 let _ = client;
205 }
206
207 #[test]
208 fn builder_grpc_sync_build_returns_error() {
209 let result = ClientBuilder::new("http://localhost:8080")
210 .with_protocol_binding(BINDING_GRPC)
211 .build();
212 assert!(result.is_err());
213 }
214
215 #[test]
216 fn builder_invalid_url_returns_error() {
217 let result = ClientBuilder::new("not-a-url").build();
218 assert!(result.is_err());
219 }
220
221 #[test]
222 fn builder_zero_request_timeout_errors() {
223 let result = ClientBuilder::new("http://localhost:8080")
224 .with_timeout(Duration::ZERO)
225 .build();
226 assert!(result.is_err());
227 }
228
229 #[test]
230 fn builder_zero_stream_timeout_errors() {
231 let result = ClientBuilder::new("http://localhost:8080")
232 .with_stream_connect_timeout(Duration::ZERO)
233 .build();
234 assert!(result.is_err());
235 }
236
237 #[cfg(feature = "grpc")]
252 #[tokio::test]
253 async fn build_grpc_zero_stream_timeout_errors() {
254 let result = ClientBuilder::new("http://127.0.0.1:1")
255 .with_stream_connect_timeout(Duration::ZERO)
256 .build_grpc()
257 .await;
258 let err = result.expect_err("a zero stream_connect_timeout is invalid");
259 assert!(
260 err.to_string().contains("stream_connect_timeout"),
261 "the error must name the knob that was wrong, not the dial that \
262 never should have been attempted: {err}"
263 );
264 }
265
266 #[test]
267 fn builder_zero_connection_timeout_errors() {
268 let result = ClientBuilder::new("http://localhost:8080")
269 .with_connection_timeout(Duration::ZERO)
270 .build();
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn builder_unknown_binding_errors() {
276 let result = ClientBuilder::new("http://localhost:8080")
277 .with_protocol_binding("UNKNOWN_PROTOCOL")
278 .build();
279 assert!(result.is_err());
280 }
281
282 #[test]
283 fn builder_rest_with_retry_policy() {
284 use crate::retry::RetryPolicy;
285
286 let client = ClientBuilder::new("http://localhost:8080")
288 .with_protocol_binding(BINDING_REST)
289 .with_retry_policy(RetryPolicy::default())
290 .build()
291 .expect("build");
292 let _ = client;
293 }
294
295 #[test]
296 fn builder_jsonrpc_with_retry_policy() {
297 use crate::retry::RetryPolicy;
298
299 let client = ClientBuilder::new("http://localhost:8080")
301 .with_retry_policy(RetryPolicy::default())
302 .build()
303 .expect("build");
304 let _ = client;
305 }
306
307 #[test]
308 fn builder_from_card_rejects_incompatible_binding() {
309 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
310
311 let card = AgentCard {
312 url: None,
313 name: "test".into(),
314 version: "1.0".into(),
315 description: "Test agent".into(),
316 supported_interfaces: vec![AgentInterface {
317 url: "http://localhost:9090".into(),
318 protocol_binding: "UNKNOWN".into(),
319 protocol_version: "1.0.0".into(),
320 tenant: None,
321 }],
322 provider: None,
323 icon_url: None,
324 documentation_url: None,
325 capabilities: AgentCapabilities::none(),
326 security_schemes: None,
327 security_requirements: None,
328 default_input_modes: vec![],
329 default_output_modes: vec![],
330 skills: vec![],
331 signatures: None,
332 };
333
334 let result = ClientBuilder::from_card(&card).unwrap().build();
335 assert!(result.is_err(), "unknown binding should fail");
336 }
337}