a2a_protocol_client/builder/
transport_factory.rs1use crate::client::A2aClient;
13use crate::config::{BINDING_GRPC, 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 Box::new(t)
62 }
63 BINDING_REST => {
64 let t = RestTransport::with_all_timeouts(
65 &self.endpoint,
66 self.config.request_timeout,
67 self.config.stream_connect_timeout,
68 self.config.connection_timeout,
69 )?;
70 Box::new(t)
71 }
72 #[cfg(feature = "grpc")]
73 BINDING_GRPC => {
74 return Err(ClientError::Transport(
78 "gRPC transport requires async connect; \
79 use ClientBuilder::build_grpc() or \
80 with_custom_transport(GrpcTransport::connect(...))"
81 .into(),
82 ));
83 }
84 #[cfg(not(feature = "grpc"))]
85 BINDING_GRPC => {
86 return Err(ClientError::Transport(
87 "gRPC transport requires the `grpc` feature flag".into(),
88 ));
89 }
90 other => {
91 return Err(ClientError::Transport(format!(
92 "unknown protocol binding: {other}"
93 )));
94 }
95 }
96 };
97
98 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
100 Box::new(RetryTransport::new(transport, policy))
101 } else {
102 transport
103 };
104
105 Ok(A2aClient::new(transport, self.interceptors, self.config))
106 }
107
108 #[cfg(feature = "grpc")]
118 pub async fn build_grpc(self) -> ClientResult<A2aClient> {
119 use crate::transport::grpc::{GrpcTransport, GrpcTransportConfig};
120
121 if self.config.request_timeout.is_zero() {
122 return Err(ClientError::Transport(
123 "request_timeout must be non-zero".into(),
124 ));
125 }
126
127 let transport: Box<dyn Transport> = if let Some(t) = self.transport_override {
128 t
129 } else {
130 let grpc_config = GrpcTransportConfig::default()
131 .with_timeout(self.config.request_timeout)
132 .with_connect_timeout(self.config.connection_timeout);
133 let t = GrpcTransport::connect_with_config(&self.endpoint, grpc_config).await?;
134 Box::new(t)
135 };
136
137 let transport: Box<dyn Transport> = if let Some(policy) = self.retry_policy {
138 Box::new(RetryTransport::new(transport, policy))
139 } else {
140 transport
141 };
142
143 Ok(A2aClient::new(transport, self.interceptors, self.config))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::super::*;
150 use crate::config::{BINDING_GRPC, BINDING_REST};
151 use std::time::Duration;
152
153 #[test]
154 fn builder_defaults_to_jsonrpc() {
155 let client = ClientBuilder::new("http://localhost:8080")
156 .build()
157 .expect("build");
158 let _ = client;
159 }
160
161 #[test]
162 fn builder_rest_transport() {
163 let client = ClientBuilder::new("http://localhost:8080")
164 .with_protocol_binding(BINDING_REST)
165 .build()
166 .expect("build");
167 let _ = client;
168 }
169
170 #[test]
171 fn builder_grpc_sync_build_returns_error() {
172 let result = ClientBuilder::new("http://localhost:8080")
173 .with_protocol_binding(BINDING_GRPC)
174 .build();
175 assert!(result.is_err());
176 }
177
178 #[test]
179 fn builder_invalid_url_returns_error() {
180 let result = ClientBuilder::new("not-a-url").build();
181 assert!(result.is_err());
182 }
183
184 #[test]
185 fn builder_zero_request_timeout_errors() {
186 let result = ClientBuilder::new("http://localhost:8080")
187 .with_timeout(Duration::ZERO)
188 .build();
189 assert!(result.is_err());
190 }
191
192 #[test]
193 fn builder_zero_stream_timeout_errors() {
194 let result = ClientBuilder::new("http://localhost:8080")
195 .with_stream_connect_timeout(Duration::ZERO)
196 .build();
197 assert!(result.is_err());
198 }
199
200 #[test]
201 fn builder_zero_connection_timeout_errors() {
202 let result = ClientBuilder::new("http://localhost:8080")
203 .with_connection_timeout(Duration::ZERO)
204 .build();
205 assert!(result.is_err());
206 }
207
208 #[test]
209 fn builder_unknown_binding_errors() {
210 let result = ClientBuilder::new("http://localhost:8080")
211 .with_protocol_binding("UNKNOWN_PROTOCOL")
212 .build();
213 assert!(result.is_err());
214 }
215
216 #[test]
217 fn builder_rest_with_retry_policy() {
218 use crate::retry::RetryPolicy;
219
220 let client = ClientBuilder::new("http://localhost:8080")
222 .with_protocol_binding(BINDING_REST)
223 .with_retry_policy(RetryPolicy::default())
224 .build()
225 .expect("build");
226 let _ = client;
227 }
228
229 #[test]
230 fn builder_jsonrpc_with_retry_policy() {
231 use crate::retry::RetryPolicy;
232
233 let client = ClientBuilder::new("http://localhost:8080")
235 .with_retry_policy(RetryPolicy::default())
236 .build()
237 .expect("build");
238 let _ = client;
239 }
240
241 #[test]
242 fn builder_from_card_rejects_incompatible_binding() {
243 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
244
245 let card = AgentCard {
246 url: None,
247 name: "test".into(),
248 version: "1.0".into(),
249 description: "Test agent".into(),
250 supported_interfaces: vec![AgentInterface {
251 url: "http://localhost:9090".into(),
252 protocol_binding: "UNKNOWN".into(),
253 protocol_version: "1.0.0".into(),
254 tenant: None,
255 }],
256 provider: None,
257 icon_url: None,
258 documentation_url: None,
259 capabilities: AgentCapabilities::none(),
260 security_schemes: None,
261 security_requirements: None,
262 default_input_modes: vec![],
263 default_output_modes: vec![],
264 skills: vec![],
265 signatures: None,
266 };
267
268 let result = ClientBuilder::from_card(&card).unwrap().build();
269 assert!(result.is_err(), "unknown binding should fail");
270 }
271}