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