perfgate_client/
config.rs1use std::path::PathBuf;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Default)]
11pub enum AuthMethod {
12 #[default]
14 None,
15 ApiKey(String),
17 Token(String),
19}
20
21impl AuthMethod {
22 pub fn header_value(&self) -> Option<String> {
24 match self {
25 AuthMethod::None => None,
26 AuthMethod::ApiKey(key) => Some(format!("Bearer {}", key)),
27 AuthMethod::Token(token) => Some(format!("Token {}", token)),
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct RetryConfig {
35 pub max_retries: u32,
37 pub base_delay: Duration,
39 pub max_delay: Duration,
41 pub retry_status_codes: Vec<u16>,
43}
44
45impl Default for RetryConfig {
46 fn default() -> Self {
47 Self {
48 max_retries: 3,
49 base_delay: Duration::from_millis(100),
50 max_delay: Duration::from_secs(5),
51 retry_status_codes: vec![429, 500, 502, 503, 504],
52 }
53 }
54}
55
56impl RetryConfig {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
64 self.max_retries = max_retries;
65 self
66 }
67
68 pub fn with_base_delay(mut self, base_delay: Duration) -> Self {
70 self.base_delay = base_delay;
71 self
72 }
73
74 pub fn with_max_delay(mut self, max_delay: Duration) -> Self {
76 self.max_delay = max_delay;
77 self
78 }
79
80 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
82 let multiplier = 2u32.pow(attempt);
83 let delay = self.base_delay.saturating_mul(multiplier);
84 delay.min(self.max_delay)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub enum FallbackStorage {
91 Local {
93 dir: PathBuf,
95 },
96}
97
98impl FallbackStorage {
99 pub fn local(dir: impl Into<PathBuf>) -> Self {
101 FallbackStorage::Local { dir: dir.into() }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct ClientConfig {
108 pub server_url: String,
110 pub auth: AuthMethod,
112 pub timeout: Duration,
114 pub retry: RetryConfig,
116 pub fallback: Option<FallbackStorage>,
118}
119
120impl Default for ClientConfig {
121 fn default() -> Self {
122 Self {
123 server_url: String::new(),
124 auth: AuthMethod::None,
125 timeout: Duration::from_secs(30),
126 retry: RetryConfig::default(),
127 fallback: None,
128 }
129 }
130}
131
132impl ClientConfig {
133 pub fn new(server_url: impl Into<String>) -> Self {
135 Self {
136 server_url: server_url.into(),
137 ..Self::default()
138 }
139 }
140
141 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
143 self.auth = AuthMethod::ApiKey(api_key.into());
144 self
145 }
146
147 pub fn with_token(mut self, token: impl Into<String>) -> Self {
149 self.auth = AuthMethod::Token(token.into());
150 self
151 }
152
153 pub fn with_timeout(mut self, timeout: Duration) -> Self {
155 self.timeout = timeout;
156 self
157 }
158
159 pub fn with_retry(mut self, retry: RetryConfig) -> Self {
161 self.retry = retry;
162 self
163 }
164
165 pub fn with_fallback(mut self, fallback: FallbackStorage) -> Self {
167 self.fallback = Some(fallback);
168 self
169 }
170
171 pub fn validate(&self) -> Result<(), String> {
173 if self.server_url.is_empty() {
174 return Err("server_url is required".to_string());
175 }
176
177 if let Err(e) = url::Url::parse(&self.server_url) {
179 return Err(format!("Invalid server_url: {}", e));
180 }
181
182 if self.timeout.is_zero() {
183 return Err("timeout must be greater than zero".to_string());
184 }
185
186 Ok(())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_auth_method_header_value() {
196 assert_eq!(AuthMethod::None.header_value(), None);
197 assert_eq!(
198 AuthMethod::ApiKey("secret".to_string()).header_value(),
199 Some("Bearer secret".to_string())
200 );
201 assert_eq!(
202 AuthMethod::Token("jwt-token".to_string()).header_value(),
203 Some("Token jwt-token".to_string())
204 );
205 }
206
207 #[test]
208 fn test_retry_config_delay() {
209 let config = RetryConfig {
210 max_retries: 3,
211 base_delay: Duration::from_millis(100),
212 max_delay: Duration::from_secs(5),
213 retry_status_codes: vec![],
214 };
215
216 assert_eq!(config.delay_for_attempt(0), Duration::from_millis(100));
218 assert_eq!(config.delay_for_attempt(1), Duration::from_millis(200));
219 assert_eq!(config.delay_for_attempt(2), Duration::from_millis(400));
220 }
221
222 #[test]
223 fn test_retry_config_delay_capped() {
224 let config = RetryConfig {
225 max_retries: 10,
226 base_delay: Duration::from_secs(1),
227 max_delay: Duration::from_secs(5),
228 retry_status_codes: vec![],
229 };
230
231 assert_eq!(config.delay_for_attempt(10), Duration::from_secs(5));
233 }
234
235 #[test]
236 fn test_client_config_validation() {
237 let config = ClientConfig::new("https://example.com/api/v1");
238 assert!(config.validate().is_ok());
239
240 let empty_config = ClientConfig {
241 server_url: String::new(),
242 ..Default::default()
243 };
244 assert!(empty_config.validate().is_err());
245
246 let invalid_url = ClientConfig::new("not a url");
247 assert!(invalid_url.validate().is_err());
248
249 let zero_timeout = ClientConfig {
250 server_url: "https://example.com".to_string(),
251 timeout: Duration::ZERO,
252 ..Default::default()
253 };
254 assert!(zero_timeout.validate().is_err());
255 }
256
257 #[test]
258 fn test_client_config_builder() {
259 let config = ClientConfig::new("https://example.com/api/v1")
260 .with_api_key("my-key")
261 .with_timeout(Duration::from_secs(60))
262 .with_fallback(FallbackStorage::local("/tmp/baselines"));
263
264 assert_eq!(config.server_url, "https://example.com/api/v1");
265 assert!(matches!(config.auth, AuthMethod::ApiKey(_)));
266 assert_eq!(config.timeout, Duration::from_secs(60));
267 assert!(config.fallback.is_some());
268 }
269}