ferrous_llm_core/
config.rs

1//! Configuration system for LLM providers.
2//!
3//! This module defines the configuration traits and utilities that providers
4//! use to manage their settings, validation, and initialization.
5
6use crate::error::ConfigError;
7use serde::{Deserialize, Serialize};
8use std::fmt::Debug;
9use std::time::Duration;
10
11/// Trait for provider configuration types.
12///
13/// All provider configurations must implement this trait to provide
14/// consistent validation and construction patterns.
15pub trait ProviderConfig: Clone + Debug + Send + Sync {
16    /// The provider type that this configuration creates
17    type Provider;
18
19    /// Build a provider instance from this configuration.
20    ///
21    /// This method should validate the configuration and create a new
22    /// provider instance. It should fail if the configuration is invalid.
23    fn build(self) -> Result<Self::Provider, ConfigError>;
24
25    /// Validate the configuration without building a provider.
26    ///
27    /// This method should check that all required fields are present
28    /// and that all values are valid.
29    fn validate(&self) -> Result<(), ConfigError>;
30}
31
32/// A secure string type for sensitive configuration values like API keys.
33///
34/// This type ensures that sensitive values are not accidentally logged
35/// or displayed in debug output.
36#[derive(Clone, Serialize, Deserialize)]
37pub struct SecretString(String);
38
39impl SecretString {
40    /// Create a new secret string
41    pub fn new(value: impl Into<String>) -> Self {
42        Self(value.into())
43    }
44
45    /// Get the secret value
46    ///
47    /// # Security Note
48    /// Be careful when using this method - the returned string
49    /// can be logged or displayed if not handled properly.
50    pub fn expose_secret(&self) -> &str {
51        &self.0
52    }
53
54    /// Check if the secret is empty
55    pub fn is_empty(&self) -> bool {
56        self.0.is_empty()
57    }
58
59    /// Get the length of the secret
60    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/// Common HTTP client configuration options.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct HttpConfig {
86    /// Request timeout duration
87    pub timeout: Duration,
88
89    /// Maximum number of retry attempts
90    pub max_retries: u32,
91
92    /// Base delay for exponential backoff
93    pub retry_delay: Duration,
94
95    /// Maximum delay for exponential backoff
96    pub max_retry_delay: Duration,
97
98    /// User agent string for requests
99    pub user_agent: Option<String>,
100
101    /// Additional HTTP headers to include in requests
102    pub headers: std::collections::HashMap<String, String>,
103
104    /// Whether to enable compression
105    pub compression: bool,
106
107    /// Connection pool settings
108    pub pool: PoolConfig,
109}
110
111/// Connection pool configuration.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PoolConfig {
114    /// Maximum number of connections per host
115    pub max_connections_per_host: usize,
116
117    /// Maximum number of idle connections to keep alive
118    pub max_idle_connections: usize,
119
120    /// How long to keep idle connections alive
121    pub idle_timeout: Duration,
122
123    /// Connection timeout
124    pub connect_timeout: Duration,
125}
126
127/// Rate limiting configuration.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RateLimitConfig {
130    /// Maximum requests per second
131    pub requests_per_second: f64,
132
133    /// Burst capacity (maximum requests that can be made at once)
134    pub burst_capacity: u32,
135
136    /// Whether rate limiting is enabled
137    pub enabled: bool,
138}
139
140/// Retry configuration for failed requests.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct RetryConfig {
143    /// Maximum number of retry attempts
144    pub max_attempts: u32,
145
146    /// Base delay between retries
147    pub base_delay: Duration,
148
149    /// Maximum delay between retries
150    pub max_delay: Duration,
151
152    /// Multiplier for exponential backoff
153    pub backoff_multiplier: f64,
154
155    /// Whether to add jitter to retry delays
156    pub jitter: bool,
157
158    /// Whether retries are enabled
159    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
211/// Validation utilities for configuration values.
212pub mod validation {
213    use super::*;
214    use url::Url;
215
216    /// Validate that a string is not empty
217    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    /// Validate that a secret string is not empty
226    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    /// Validate that a URL is well-formed
238    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    /// Validate that a URL is HTTPS
244    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    /// Validate that a numeric value is within a range
256    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    /// Validate that a duration is positive
271    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    /// Validate an API key format (basic checks)
286    pub fn validate_api_key(api_key: &SecretString, field_name: &str) -> Result<(), ConfigError> {
287        let key = api_key.expose_secret();
288
289        // Basic validation - not empty and reasonable length
290        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        // Check for common patterns that indicate a placeholder
302        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    /// Validate a model name
316    pub fn validate_model_name(model: &str, field_name: &str) -> Result<(), ConfigError> {
317        validate_non_empty(model, field_name)?;
318
319        // Basic validation - no whitespace, reasonable length
320        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
338/// Builder pattern helper for configuration types.
339///
340/// This trait provides a consistent interface for building configurations
341/// with method chaining.
342pub trait ConfigBuilder<T> {
343    /// Build the final configuration
344    fn build(self) -> T;
345}
346
347/// Environment variable helper for loading configuration.
348pub mod env {
349    use super::*;
350    use std::env;
351
352    /// Load a required environment variable
353    pub fn required(key: &str) -> Result<String, ConfigError> {
354        env::var(key).map_err(|_| ConfigError::missing_field(key))
355    }
356
357    /// Load an optional environment variable
358    pub fn optional(key: &str) -> Option<String> {
359        env::var(key).ok()
360    }
361
362    /// Load a required environment variable as a SecretString
363    pub fn required_secret(key: &str) -> Result<SecretString, ConfigError> {
364        required(key).map(SecretString::new)
365    }
366
367    /// Load an optional environment variable as a SecretString
368    pub fn optional_secret(key: &str) -> Option<SecretString> {
369        optional(key).map(SecretString::new)
370    }
371
372    /// Load an environment variable with a default value
373    pub fn with_default(key: &str, default: &str) -> String {
374        optional(key).unwrap_or_else(|| default.to_string())
375    }
376
377    /// Parse an environment variable as a specific type
378    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    /// Parse an optional environment variable as a specific type
390    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}