1use crate::error::ConfigError;
7use serde::{Deserialize, Serialize};
8use std::fmt::Debug;
9use std::time::Duration;
10
11pub trait ProviderConfig: Clone + Debug + Send + Sync {
16 type Provider;
18
19 fn build(self) -> Result<Self::Provider, ConfigError>;
24
25 fn validate(&self) -> Result<(), ConfigError>;
30}
31
32#[derive(Clone, Serialize, Deserialize)]
37pub struct SecretString(String);
38
39impl SecretString {
40 pub fn new(value: impl Into<String>) -> Self {
42 Self(value.into())
43 }
44
45 pub fn expose_secret(&self) -> &str {
51 &self.0
52 }
53
54 pub fn is_empty(&self) -> bool {
56 self.0.is_empty()
57 }
58
59 pub fn len(&self) -> usize {
61 self.0.len()
62 }
63}
64
65impl From<String> for SecretString {
66 fn from(value: String) -> Self {
67 Self(value)
68 }
69}
70
71impl From<&str> for SecretString {
72 fn from(value: &str) -> Self {
73 Self(value.to_string())
74 }
75}
76
77impl Debug for SecretString {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str("[REDACTED]")
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct HttpConfig {
86 pub timeout: Duration,
88
89 pub max_retries: u32,
91
92 pub retry_delay: Duration,
94
95 pub max_retry_delay: Duration,
97
98 pub user_agent: Option<String>,
100
101 pub headers: std::collections::HashMap<String, String>,
103
104 pub compression: bool,
106
107 pub pool: PoolConfig,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PoolConfig {
114 pub max_connections_per_host: usize,
116
117 pub max_idle_connections: usize,
119
120 pub idle_timeout: Duration,
122
123 pub connect_timeout: Duration,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RateLimitConfig {
130 pub requests_per_second: f64,
132
133 pub burst_capacity: u32,
135
136 pub enabled: bool,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct RetryConfig {
143 pub max_attempts: u32,
145
146 pub base_delay: Duration,
148
149 pub max_delay: Duration,
151
152 pub backoff_multiplier: f64,
154
155 pub jitter: bool,
157
158 pub enabled: bool,
160}
161
162impl Default for HttpConfig {
163 fn default() -> Self {
164 Self {
165 timeout: Duration::from_secs(30),
166 max_retries: 3,
167 retry_delay: Duration::from_millis(100),
168 max_retry_delay: Duration::from_secs(60),
169 user_agent: Some("ferrous-llm-core/2.0".to_string()),
170 headers: std::collections::HashMap::new(),
171 compression: true,
172 pool: PoolConfig::default(),
173 }
174 }
175}
176
177impl Default for PoolConfig {
178 fn default() -> Self {
179 Self {
180 max_connections_per_host: 100,
181 max_idle_connections: 10,
182 idle_timeout: Duration::from_secs(90),
183 connect_timeout: Duration::from_secs(10),
184 }
185 }
186}
187
188impl Default for RateLimitConfig {
189 fn default() -> Self {
190 Self {
191 requests_per_second: 10.0,
192 burst_capacity: 20,
193 enabled: true,
194 }
195 }
196}
197
198impl Default for RetryConfig {
199 fn default() -> Self {
200 Self {
201 max_attempts: 3,
202 base_delay: Duration::from_millis(100),
203 max_delay: Duration::from_secs(60),
204 backoff_multiplier: 2.0,
205 jitter: true,
206 enabled: true,
207 }
208 }
209}
210
211pub mod validation {
213 use super::*;
214 use url::Url;
215
216 pub fn validate_non_empty(value: &str, field_name: &str) -> Result<(), ConfigError> {
218 if value.trim().is_empty() {
219 Err(ConfigError::missing_field(field_name))
220 } else {
221 Ok(())
222 }
223 }
224
225 pub fn validate_secret_non_empty(
227 value: &SecretString,
228 field_name: &str,
229 ) -> Result<(), ConfigError> {
230 if value.is_empty() {
231 Err(ConfigError::missing_field(field_name))
232 } else {
233 Ok(())
234 }
235 }
236
237 pub fn validate_url(url: &str, field_name: &str) -> Result<Url, ConfigError> {
239 Url::parse(url)
240 .map_err(|_| ConfigError::invalid_value(field_name, format!("Invalid URL: {url}")))
241 }
242
243 pub fn validate_https_url(url: &Url, field_name: &str) -> Result<(), ConfigError> {
245 if url.scheme() != "https" {
246 Err(ConfigError::invalid_value(
247 field_name,
248 "URL must use HTTPS scheme",
249 ))
250 } else {
251 Ok(())
252 }
253 }
254
255 pub fn validate_range<T>(value: T, min: T, max: T, field_name: &str) -> Result<(), ConfigError>
257 where
258 T: PartialOrd + std::fmt::Display,
259 {
260 if value < min || value > max {
261 Err(ConfigError::invalid_value(
262 field_name,
263 format!("Value {value} must be between {min} and {max}"),
264 ))
265 } else {
266 Ok(())
267 }
268 }
269
270 pub fn validate_positive_duration(
272 duration: Duration,
273 field_name: &str,
274 ) -> Result<(), ConfigError> {
275 if duration.is_zero() {
276 Err(ConfigError::invalid_value(
277 field_name,
278 "Duration must be positive",
279 ))
280 } else {
281 Ok(())
282 }
283 }
284
285 pub fn validate_api_key(api_key: &SecretString, field_name: &str) -> Result<(), ConfigError> {
287 let key = api_key.expose_secret();
288
289 if key.is_empty() {
291 return Err(ConfigError::missing_field(field_name));
292 }
293
294 if key.len() < 10 {
295 return Err(ConfigError::invalid_value(
296 field_name,
297 "API key appears to be too short",
298 ));
299 }
300
301 let placeholder_patterns = ["your_api_key", "api_key_here", "replace_me", "xxx"];
303 for pattern in &placeholder_patterns {
304 if key.to_lowercase().contains(pattern) {
305 return Err(ConfigError::invalid_value(
306 field_name,
307 "API key appears to be a placeholder",
308 ));
309 }
310 }
311
312 Ok(())
313 }
314
315 pub fn validate_model_name(model: &str, field_name: &str) -> Result<(), ConfigError> {
317 validate_non_empty(model, field_name)?;
318
319 if model.contains(char::is_whitespace) {
321 return Err(ConfigError::invalid_value(
322 field_name,
323 "Model name cannot contain whitespace",
324 ));
325 }
326
327 if model.len() > 100 {
328 return Err(ConfigError::invalid_value(
329 field_name,
330 "Model name is too long",
331 ));
332 }
333
334 Ok(())
335 }
336}
337
338pub trait ConfigBuilder<T> {
343 fn build(self) -> T;
345}
346
347pub mod env {
349 use super::*;
350 use std::env;
351
352 pub fn required(key: &str) -> Result<String, ConfigError> {
354 env::var(key).map_err(|_| ConfigError::missing_field(key))
355 }
356
357 pub fn optional(key: &str) -> Option<String> {
359 env::var(key).ok()
360 }
361
362 pub fn required_secret(key: &str) -> Result<SecretString, ConfigError> {
364 required(key).map(SecretString::new)
365 }
366
367 pub fn optional_secret(key: &str) -> Option<SecretString> {
369 optional(key).map(SecretString::new)
370 }
371
372 pub fn with_default(key: &str, default: &str) -> String {
374 optional(key).unwrap_or_else(|| default.to_string())
375 }
376
377 pub fn parse<T>(key: &str) -> Result<T, ConfigError>
379 where
380 T: std::str::FromStr,
381 T::Err: std::fmt::Display,
382 {
383 let value = required(key)?;
384 value
385 .parse()
386 .map_err(|e| ConfigError::invalid_value(key, format!("Failed to parse: {e}")))
387 }
388
389 pub fn parse_optional<T>(key: &str) -> Result<Option<T>, ConfigError>
391 where
392 T: std::str::FromStr,
393 T::Err: std::fmt::Display,
394 {
395 match optional(key) {
396 Some(value) => value
397 .parse()
398 .map(Some)
399 .map_err(|e| ConfigError::invalid_value(key, format!("Failed to parse: {e}"))),
400 None => Ok(None),
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_secret_string_debug() {
411 let secret = SecretString::new("super_secret_key");
412 let debug_output = format!("{:?}", secret);
413 assert_eq!(debug_output, "[REDACTED]");
414 assert!(!debug_output.contains("super_secret_key"));
415 }
416
417 #[test]
418 fn test_secret_string_expose() {
419 let secret = SecretString::new("my_secret");
420 assert_eq!(secret.expose_secret(), "my_secret");
421 }
422
423 #[test]
424 fn test_validation_non_empty() {
425 use validation::*;
426
427 assert!(validate_non_empty("valid", "test").is_ok());
428 assert!(validate_non_empty("", "test").is_err());
429 assert!(validate_non_empty(" ", "test").is_err());
430 }
431
432 #[test]
433 fn test_validation_url() {
434 use validation::*;
435
436 assert!(validate_url("https://api.example.com", "url").is_ok());
437 assert!(validate_url("not_a_url", "url").is_err());
438 }
439
440 #[test]
441 fn test_validation_range() {
442 use validation::*;
443
444 assert!(validate_range(5, 1, 10, "value").is_ok());
445 assert!(validate_range(0, 1, 10, "value").is_err());
446 assert!(validate_range(15, 1, 10, "value").is_err());
447 }
448}