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
133 let transport: Box<dyn Transport> = if let Some(t) = self.transport_override {
134 t
135 } else {
136 let grpc_config = GrpcTransportConfig::default()
137 .with_timeout(self.config.request_timeout)
138 .with_connect_timeout(self.config.connection_timeout)
139 .with_max_message_size(self.config.max_response_size);
143 let t = GrpcTransport::connect_with_config(&self.endpoint, grpc_config).await?;
144 Box::new(t)
145 };
146
147 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
148 Box::new(RetryTransport::new(transport, policy))
149 } else {
150 transport
151 };
152
153 Ok(A2aClient::new(transport, self.interceptors, self.config))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::super::*;
160 use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_REST};
161 use std::time::Duration;
162
163 #[test]
164 fn builder_defaults_to_jsonrpc() {
165 let client = ClientBuilder::new("http://localhost:8080")
166 .build()
167 .expect("build");
168 let _ = client;
169 }
170
171 #[test]
172 fn builder_rest_transport() {
173 let client = ClientBuilder::new("http://localhost:8080")
174 .with_protocol_binding(BINDING_REST)
175 .build()
176 .expect("build");
177 let _ = client;
178 }
179
180 #[test]
181 fn builder_accepts_spec_http_json_binding() {
182 let client = ClientBuilder::new("http://localhost:8080")
185 .with_protocol_binding(BINDING_HTTP_JSON)
186 .build()
187 .expect("HTTP+JSON binding should resolve to the REST transport");
188 let _ = client;
189 }
190
191 #[test]
192 fn builder_grpc_sync_build_returns_error() {
193 let result = ClientBuilder::new("http://localhost:8080")
194 .with_protocol_binding(BINDING_GRPC)
195 .build();
196 assert!(result.is_err());
197 }
198
199 #[test]
200 fn builder_invalid_url_returns_error() {
201 let result = ClientBuilder::new("not-a-url").build();
202 assert!(result.is_err());
203 }
204
205 #[test]
206 fn builder_zero_request_timeout_errors() {
207 let result = ClientBuilder::new("http://localhost:8080")
208 .with_timeout(Duration::ZERO)
209 .build();
210 assert!(result.is_err());
211 }
212
213 #[test]
214 fn builder_zero_stream_timeout_errors() {
215 let result = ClientBuilder::new("http://localhost:8080")
216 .with_stream_connect_timeout(Duration::ZERO)
217 .build();
218 assert!(result.is_err());
219 }
220
221 #[test]
222 fn builder_zero_connection_timeout_errors() {
223 let result = ClientBuilder::new("http://localhost:8080")
224 .with_connection_timeout(Duration::ZERO)
225 .build();
226 assert!(result.is_err());
227 }
228
229 #[test]
230 fn builder_unknown_binding_errors() {
231 let result = ClientBuilder::new("http://localhost:8080")
232 .with_protocol_binding("UNKNOWN_PROTOCOL")
233 .build();
234 assert!(result.is_err());
235 }
236
237 #[test]
238 fn builder_rest_with_retry_policy() {
239 use crate::retry::RetryPolicy;
240
241 let client = ClientBuilder::new("http://localhost:8080")
243 .with_protocol_binding(BINDING_REST)
244 .with_retry_policy(RetryPolicy::default())
245 .build()
246 .expect("build");
247 let _ = client;
248 }
249
250 #[test]
251 fn builder_jsonrpc_with_retry_policy() {
252 use crate::retry::RetryPolicy;
253
254 let client = ClientBuilder::new("http://localhost:8080")
256 .with_retry_policy(RetryPolicy::default())
257 .build()
258 .expect("build");
259 let _ = client;
260 }
261
262 #[test]
263 fn builder_from_card_rejects_incompatible_binding() {
264 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
265
266 let card = AgentCard {
267 url: None,
268 name: "test".into(),
269 version: "1.0".into(),
270 description: "Test agent".into(),
271 supported_interfaces: vec![AgentInterface {
272 url: "http://localhost:9090".into(),
273 protocol_binding: "UNKNOWN".into(),
274 protocol_version: "1.0.0".into(),
275 tenant: None,
276 }],
277 provider: None,
278 icon_url: None,
279 documentation_url: None,
280 capabilities: AgentCapabilities::none(),
281 security_schemes: None,
282 security_requirements: None,
283 default_input_modes: vec![],
284 default_output_modes: vec![],
285 skills: vec![],
286 signatures: None,
287 };
288
289 let result = ClientBuilder::from_card(&card).unwrap().build();
290 assert!(result.is_err(), "unknown binding should fail");
291 }
292}