truthlinked_sdk/
builder.rs1use crate::error::{Result, TruthlinkedError};
2use crate::logging::LoggingConfig;
3use crate::retry::RetryConfig;
4use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
5use std::time::Duration;
6
7#[derive(Debug)]
29pub struct ClientBuilder {
30 base_url: String,
31 license_key: String,
32 timeout: Duration,
33 connect_timeout: Duration,
34 retry_config: RetryConfig,
35 logging_config: LoggingConfig,
36 custom_headers: HeaderMap,
37 user_agent: Option<String>,
38 proxy_url: Option<String>,
39 pool_max_idle_per_host: usize,
40 pool_idle_timeout: Duration,
41 enable_gzip: bool,
42 enable_brotli: bool,
43 certificate_pins: Vec<String>,
44 allow_http: bool, }
46
47impl ClientBuilder {
48 pub fn new(base_url: impl Into<String>, license_key: impl Into<String>) -> Self {
54 Self {
55 base_url: base_url.into(),
56 license_key: license_key.into(),
57 timeout: Duration::from_secs(30),
58 connect_timeout: Duration::from_secs(10),
59 retry_config: RetryConfig::production(),
60 logging_config: LoggingConfig::production(),
61 custom_headers: HeaderMap::new(),
62 user_agent: None,
63 proxy_url: None,
64 pool_max_idle_per_host: 10,
65 pool_idle_timeout: Duration::from_secs(90),
66 enable_gzip: true,
67 enable_brotli: true,
68 certificate_pins: Vec::new(),
69 allow_http: false,
70 }
71 }
72
73 pub fn timeout(mut self, timeout: Duration) -> Self {
75 self.timeout = timeout;
76 self
77 }
78
79 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
81 self.connect_timeout = timeout;
82 self
83 }
84
85 pub fn retry_config(mut self, config: RetryConfig) -> Self {
87 self.retry_config = config;
88 self
89 }
90
91 pub fn retries(mut self, max_attempts: u32) -> Self {
93 self.retry_config.max_attempts = max_attempts;
94 self
95 }
96
97 pub fn logging_config(mut self, config: LoggingConfig) -> Self {
99 self.logging_config = config;
100 self
101 }
102
103 pub fn enable_logging(mut self) -> Self {
105 self.logging_config = LoggingConfig::development();
106 self
107 }
108
109 pub fn disable_logging(mut self) -> Self {
111 self.logging_config = LoggingConfig::none();
112 self
113 }
114
115 pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
117 let header_name = HeaderName::from_bytes(name.as_ref().as_bytes())
118 .map_err(|_| TruthlinkedError::InvalidRequest("Invalid header name".to_string()))?;
119 let header_value = HeaderValue::from_str(value.as_ref())
120 .map_err(|_| TruthlinkedError::InvalidRequest("Invalid header value".to_string()))?;
121
122 self.custom_headers.insert(header_name, header_value);
123 Ok(self)
124 }
125
126 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
128 self.user_agent = Some(user_agent.into());
129 self
130 }
131
132 pub fn proxy(mut self, proxy_url: impl Into<String>) -> Self {
134 self.proxy_url = Some(proxy_url.into());
135 self
136 }
137
138 pub fn pool_config(mut self, max_idle_per_host: usize, idle_timeout: Duration) -> Self {
140 self.pool_max_idle_per_host = max_idle_per_host;
141 self.pool_idle_timeout = idle_timeout;
142 self
143 }
144
145 pub fn gzip(mut self, enable: bool) -> Self {
147 self.enable_gzip = enable;
148 self
149 }
150
151 pub fn brotli(mut self, enable: bool) -> Self {
153 self.enable_brotli = enable;
154 self
155 }
156
157 pub fn certificate_pin(mut self, pin: impl Into<String>) -> Self {
162 self.certificate_pins.push(pin.into());
163 self
164 }
165
166 pub fn build(self) -> Result<crate::client::Client> {
168 if !self.allow_http && !self.base_url.starts_with("https://") {
170 return Err(TruthlinkedError::InvalidRequest(
171 "Base URL must use HTTPS".to_string()
172 ));
173 }
174
175 let mut client_builder = reqwest::Client::builder()
177 .timeout(self.timeout)
178 .connect_timeout(self.connect_timeout)
179 .pool_idle_timeout(self.pool_idle_timeout)
180 .pool_max_idle_per_host(self.pool_max_idle_per_host)
181 .https_only(!self.allow_http) .default_headers(self.custom_headers);
183
184 if let Some(user_agent) = self.user_agent {
186 client_builder = client_builder.user_agent(user_agent);
187 } else {
188 client_builder = client_builder.user_agent(format!(
189 "truthlinked-sdk/{}",
190 env!("CARGO_PKG_VERSION")
191 ));
192 }
193
194 if let Some(proxy_url) = self.proxy_url {
196 let proxy = reqwest::Proxy::all(&proxy_url)
197 .map_err(|_| TruthlinkedError::InvalidRequest("Invalid proxy URL".to_string()))?;
198 client_builder = client_builder.proxy(proxy);
199 }
200
201 if !self.certificate_pins.is_empty() {
203 tracing::warn!("Certificate pinning not yet implemented in reqwest");
204 }
205
206 let http_client = client_builder.build()
207 .map_err(|_| TruthlinkedError::InvalidRequest("Failed to build HTTP client".to_string()))?;
208
209 crate::client::Client::with_config(
210 http_client,
211 self.base_url,
212 self.license_key,
213 self.retry_config,
214 self.logging_config,
215 )
216 }
217}
218
219impl ClientBuilder {
221 pub fn production(base_url: impl Into<String>, license_key: impl Into<String>) -> Self {
223 Self::new(base_url, license_key)
224 .timeout(Duration::from_secs(30))
225 .connect_timeout(Duration::from_secs(10))
226 .retry_config(RetryConfig::production())
227 .logging_config(LoggingConfig::production())
228 }
229
230 pub fn development(base_url: impl Into<String>, license_key: impl Into<String>) -> Self {
232 Self::new(base_url, license_key)
233 .timeout(Duration::from_secs(60))
234 .connect_timeout(Duration::from_secs(5))
235 .retry_config(RetryConfig::aggressive())
236 .logging_config(LoggingConfig::development())
237 }
238
239 pub fn testing(base_url: impl Into<String>, license_key: impl Into<String>) -> Self {
241 let mut builder = Self::new(base_url, license_key)
242 .timeout(Duration::from_secs(5))
243 .connect_timeout(Duration::from_secs(2))
244 .retry_config(RetryConfig::none())
245 .logging_config(LoggingConfig::none());
246 builder.allow_http = true; builder
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_builder_basic() {
257 let builder = ClientBuilder::new("https://api.example.com", "test_key");
258 assert_eq!(builder.base_url, "https://api.example.com");
259 assert_eq!(builder.license_key, "test_key");
260 }
261
262 #[test]
263 fn test_builder_fluent_interface() {
264 let builder = ClientBuilder::new("https://api.example.com", "test_key")
265 .timeout(Duration::from_secs(60))
266 .retries(5)
267 .user_agent("TestApp/1.0")
268 .enable_logging();
269
270 assert_eq!(builder.timeout, Duration::from_secs(60));
271 assert_eq!(builder.retry_config.max_attempts, 5);
272 assert_eq!(builder.user_agent, Some("TestApp/1.0".to_string()));
273 }
274
275 #[test]
276 fn test_builder_presets() {
277 let prod_builder = ClientBuilder::production("https://api.example.com", "key");
278 assert_eq!(prod_builder.timeout, Duration::from_secs(30));
279
280 let dev_builder = ClientBuilder::development("https://api.example.com", "key");
281 assert_eq!(dev_builder.timeout, Duration::from_secs(60));
282
283 let test_builder = ClientBuilder::testing("https://api.example.com", "key");
284 assert_eq!(test_builder.timeout, Duration::from_secs(5));
285 }
286}