riglr_config/
app.rs

1//! Application-level configuration
2
3use crate::{ConfigError, ConfigResult};
4use serde::{Deserialize, Serialize};
5use validator::{Validate, ValidationError};
6
7/// Log level configuration
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
9#[serde(rename_all = "lowercase")]
10pub enum LogLevel {
11    /// Trace level - most verbose
12    Trace,
13    /// Debug level - detailed debugging
14    Debug,
15    /// Info level - general information
16    Info,
17    /// Warning level - warnings
18    Warn,
19    /// Error level - only errors
20    Error,
21}
22
23impl Default for LogLevel {
24    fn default() -> Self {
25        Self::Info
26    }
27}
28
29/// Application configuration
30#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
31pub struct AppConfig {
32    /// Server port
33    #[serde(default = "default_port")]
34    #[validate(range(min = 1, max = 65535))]
35    pub port: u16,
36
37    /// Application environment
38    #[serde(default)]
39    pub environment: Environment,
40
41    /// Log level
42    #[serde(default)]
43    pub log_level: LogLevel,
44
45    /// Whether to use testnet chains
46    #[serde(default)]
47    pub use_testnet: bool,
48
49    /// Transaction settings
50    #[serde(flatten)]
51    #[validate(nested)]
52    pub transaction: TransactionConfig,
53
54    /// Retry configuration
55    #[serde(flatten)]
56    #[validate(nested)]
57    pub retry: RetryConfig,
58}
59
60/// Application environment
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
62#[serde(rename_all = "lowercase")]
63pub enum Environment {
64    /// Development environment for local testing and debugging
65    Development,
66    /// Staging environment for pre-production testing
67    Staging,
68    /// Production environment for live deployment
69    Production,
70}
71
72impl Default for Environment {
73    fn default() -> Self {
74        Self::Development
75    }
76}
77
78/// Transaction configuration
79#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
80pub struct TransactionConfig {
81    /// Maximum gas price in gwei
82    #[serde(default = "default_max_gas_price")]
83    #[validate(range(min = 1))]
84    pub max_gas_price_gwei: u64,
85
86    /// Priority fee in gwei
87    #[serde(default = "default_priority_fee")]
88    pub priority_fee_gwei: u64,
89
90    /// Slippage tolerance as percentage (e.g., 0.5 = 0.5%)
91    #[serde(default = "default_slippage")]
92    #[validate(range(min = 0.0, max = 100.0))]
93    pub slippage_tolerance_percent: f64,
94
95    /// Transaction deadline in seconds
96    #[serde(default = "default_deadline")]
97    pub deadline_seconds: u64,
98}
99
100/// Retry configuration
101#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
102pub struct RetryConfig {
103    /// Maximum number of retry attempts
104    #[serde(default = "default_max_retries")]
105    #[validate(range(min = 1, max = 100))]
106    pub max_retry_attempts: u32,
107
108    /// Initial retry delay in milliseconds
109    #[serde(default = "default_retry_delay")]
110    pub retry_delay_ms: u64,
111
112    /// Backoff multiplier for exponential backoff
113    #[serde(default = "default_backoff_multiplier")]
114    #[validate(custom(function = "validate_backoff_multiplier"))]
115    pub retry_backoff_multiplier: f64,
116
117    /// Maximum retry delay in milliseconds
118    #[serde(default = "default_max_retry_delay")]
119    pub max_retry_delay_ms: u64,
120}
121
122/// Custom validator for backoff multiplier
123fn validate_backoff_multiplier(value: f64) -> Result<(), ValidationError> {
124    if value <= 1.0 {
125        return Err(ValidationError::new(
126            "retry_backoff_multiplier must be greater than 1.0",
127        ));
128    }
129    if value > 10.0 {
130        return Err(ValidationError::new(
131            "retry_backoff_multiplier must be at most 10.0",
132        ));
133    }
134    Ok(())
135}
136
137impl AppConfig {
138    /// Validates the application configuration for correctness
139    pub fn validate(&self) -> ConfigResult<()> {
140        // Use validator crate for field validation
141        Validate::validate(self)
142            .map_err(|e| ConfigError::validation(format!("Validation failed: {}", e)))?;
143
144        // Additional custom validation for retry config
145        if self.retry.max_retry_delay_ms < self.retry.retry_delay_ms {
146            return Err(ConfigError::validation(
147                "max_retry_delay_ms must be >= retry_delay_ms",
148            ));
149        }
150
151        Ok(())
152    }
153
154    /// Validates the application configuration
155    pub fn validate_config(&self) -> ConfigResult<()> {
156        Validate::validate(self)
157            .map_err(|e| ConfigError::validation(format!("Validation failed: {}", e)))
158    }
159}
160
161impl RetryConfig {
162    /// Validates the retry configuration
163    pub fn validate_config(&self) -> ConfigResult<()> {
164        Validate::validate(self)
165            .map_err(|e| ConfigError::validation(format!("Validation failed: {}", e)))
166    }
167}
168
169// RetryConfig validation is now handled by the Validate trait from validator crate
170
171// Default value functions
172fn default_port() -> u16 {
173    8080
174}
175fn default_max_gas_price() -> u64 {
176    100
177}
178fn default_priority_fee() -> u64 {
179    2
180}
181fn default_slippage() -> f64 {
182    0.5
183}
184fn default_deadline() -> u64 {
185    300
186} // 5 minutes
187fn default_max_retries() -> u32 {
188    3
189}
190fn default_retry_delay() -> u64 {
191    1000
192}
193fn default_backoff_multiplier() -> f64 {
194    2.0
195}
196fn default_max_retry_delay() -> u64 {
197    30000
198}
199
200impl Default for AppConfig {
201    fn default() -> Self {
202        Self {
203            port: default_port(),
204            environment: Environment::default(),
205            log_level: LogLevel::default(),
206            use_testnet: false,
207            transaction: TransactionConfig::default(),
208            retry: RetryConfig::default(),
209        }
210    }
211}
212
213impl Default for TransactionConfig {
214    fn default() -> Self {
215        Self {
216            max_gas_price_gwei: default_max_gas_price(),
217            priority_fee_gwei: default_priority_fee(),
218            slippage_tolerance_percent: default_slippage(),
219            deadline_seconds: default_deadline(),
220        }
221    }
222}
223
224impl Default for RetryConfig {
225    fn default() -> Self {
226        Self {
227            max_retry_attempts: default_max_retries(),
228            retry_delay_ms: default_retry_delay(),
229            retry_backoff_multiplier: default_backoff_multiplier(),
230            max_retry_delay_ms: default_max_retry_delay(),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_environment_default_should_return_development() {
241        assert_eq!(Environment::default(), Environment::Development);
242    }
243
244    #[test]
245    fn test_environment_serialization() {
246        // Test serde serialization/deserialization
247        let env = Environment::Production;
248        let json = serde_json::to_string(&env).unwrap();
249        assert_eq!(json, "\"production\"");
250
251        let deserialized: Environment = serde_json::from_str(&json).unwrap();
252        assert_eq!(deserialized, Environment::Production);
253    }
254
255    #[test]
256    fn test_default_functions() {
257        assert_eq!(default_port(), 8080);
258        assert_eq!(default_max_gas_price(), 100);
259        assert_eq!(default_priority_fee(), 2);
260        assert_eq!(default_slippage(), 0.5);
261        assert_eq!(default_deadline(), 300);
262        assert_eq!(default_max_retries(), 3);
263        assert_eq!(default_retry_delay(), 1000);
264        assert_eq!(default_backoff_multiplier(), 2.0);
265        assert_eq!(default_max_retry_delay(), 30000);
266    }
267
268    #[test]
269    fn test_app_config_default() {
270        let config = AppConfig::default();
271        assert_eq!(config.port, 8080);
272        assert_eq!(config.environment, Environment::Development);
273        assert_eq!(config.log_level, LogLevel::Info);
274        assert!(!config.use_testnet);
275        assert_eq!(config.transaction.max_gas_price_gwei, 100);
276        assert_eq!(config.retry.max_retry_attempts, 3);
277    }
278
279    #[test]
280    fn test_transaction_config_default() {
281        let config = TransactionConfig::default();
282        assert_eq!(config.max_gas_price_gwei, 100);
283        assert_eq!(config.priority_fee_gwei, 2);
284        assert_eq!(config.slippage_tolerance_percent, 0.5);
285        assert_eq!(config.deadline_seconds, 300);
286    }
287
288    #[test]
289    fn test_retry_config_default() {
290        let config = RetryConfig::default();
291        assert_eq!(config.max_retry_attempts, 3);
292        assert_eq!(config.retry_delay_ms, 1000);
293        assert_eq!(config.retry_backoff_multiplier, 2.0);
294        assert_eq!(config.max_retry_delay_ms, 30000);
295    }
296
297    #[test]
298    fn test_app_config_validate_when_valid_should_return_ok() {
299        let config = AppConfig::default();
300        assert!(config.validate().is_ok());
301    }
302
303    #[test]
304    fn test_log_level_enum_values() {
305        // Test that all log level enum values work correctly
306        let levels = [
307            LogLevel::Trace,
308            LogLevel::Debug,
309            LogLevel::Info,
310            LogLevel::Warn,
311            LogLevel::Error,
312        ];
313
314        for level in levels {
315            let mut config = AppConfig::default();
316            config.log_level = level;
317            assert!(
318                config.validate().is_ok(),
319                "Log level {:?} should be valid",
320                level
321            );
322        }
323    }
324
325    #[test]
326    fn test_app_config_validate_when_zero_gas_price_should_return_err() {
327        let mut config = AppConfig::default();
328        config.transaction.max_gas_price_gwei = 0;
329
330        let result = config.validate();
331        assert!(result.is_err());
332    }
333
334    #[test]
335    fn test_app_config_validate_when_negative_slippage_should_return_err() {
336        let mut config = AppConfig::default();
337        config.transaction.slippage_tolerance_percent = -1.0;
338
339        let result = config.validate();
340        assert!(result.is_err());
341    }
342
343    #[test]
344    fn test_app_config_validate_when_excessive_slippage_should_return_err() {
345        let mut config = AppConfig::default();
346        config.transaction.slippage_tolerance_percent = 101.0;
347
348        let result = config.validate();
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn test_app_config_validate_when_boundary_slippage_should_return_ok() {
354        // Test boundary values for slippage
355        let mut config = AppConfig::default();
356
357        // Test 0.0
358        config.transaction.slippage_tolerance_percent = 0.0;
359        assert!(config.validate().is_ok());
360
361        // Test 100.0
362        config.transaction.slippage_tolerance_percent = 100.0;
363        assert!(config.validate().is_ok());
364    }
365
366    #[test]
367    fn test_app_config_validate_when_retry_validation_fails_should_return_err() {
368        let mut config = AppConfig::default();
369        config.retry.max_retry_attempts = 0;
370
371        let result = config.validate();
372        assert!(result.is_err());
373    }
374
375    #[test]
376    fn test_retry_config_validate_when_valid_should_return_ok() {
377        let config = RetryConfig::default();
378        assert!(Validate::validate(&config).is_ok());
379    }
380
381    #[test]
382    fn test_retry_config_validate_when_zero_attempts_should_return_err() {
383        let mut config = RetryConfig::default();
384        config.max_retry_attempts = 0;
385
386        let result = Validate::validate(&config);
387        assert!(result.is_err());
388    }
389
390    #[test]
391    fn test_retry_config_validate_when_backoff_multiplier_too_low_should_return_err() {
392        let mut config = RetryConfig::default();
393        config.retry_backoff_multiplier = 1.0;
394
395        let result = Validate::validate(&config);
396        assert!(result.is_err());
397    }
398
399    #[test]
400    fn test_retry_config_validate_when_backoff_multiplier_negative_should_return_err() {
401        let mut config = RetryConfig::default();
402        config.retry_backoff_multiplier = 0.5;
403
404        let result = Validate::validate(&config);
405        assert!(result.is_err());
406    }
407
408    #[test]
409    fn test_retry_config_validate_when_max_delay_less_than_initial_should_return_err() {
410        let mut config = AppConfig::default();
411        config.retry.retry_delay_ms = 5000;
412        config.retry.max_retry_delay_ms = 3000;
413
414        let result = config.validate();
415        assert!(result.is_err());
416        let error = result.unwrap_err();
417        assert!(error
418            .to_string()
419            .contains("max_retry_delay_ms must be >= retry_delay_ms"));
420    }
421
422    #[test]
423    fn test_retry_config_validate_when_max_delay_equals_initial_should_return_ok() {
424        let mut config = AppConfig::default();
425        config.retry.retry_delay_ms = 5000;
426        config.retry.max_retry_delay_ms = 5000;
427
428        assert!(config.validate().is_ok());
429    }
430
431    #[test]
432    fn test_app_config_serialization() {
433        let config = AppConfig::default();
434
435        // Test serialization
436        let json = serde_json::to_string(&config).unwrap();
437        assert!(json.contains("\"port\":8080"));
438        assert!(json.contains("\"environment\":\"development\""));
439        assert!(json.contains("\"log_level\":\"info\""));
440
441        // Test deserialization
442        let deserialized: AppConfig = serde_json::from_str(&json).unwrap();
443        assert_eq!(deserialized.port, config.port);
444        assert_eq!(deserialized.environment, config.environment);
445        assert_eq!(deserialized.log_level, config.log_level);
446    }
447
448    #[test]
449    fn test_transaction_config_serialization() {
450        let config = TransactionConfig::default();
451
452        let json = serde_json::to_string(&config).unwrap();
453        assert!(json.contains("\"max_gas_price_gwei\":100"));
454        assert!(json.contains("\"priority_fee_gwei\":2"));
455        assert!(json.contains("\"slippage_tolerance_percent\":0.5"));
456        assert!(json.contains("\"deadline_seconds\":300"));
457
458        let deserialized: TransactionConfig = serde_json::from_str(&json).unwrap();
459        assert_eq!(deserialized.max_gas_price_gwei, config.max_gas_price_gwei);
460        assert_eq!(deserialized.priority_fee_gwei, config.priority_fee_gwei);
461        assert_eq!(
462            deserialized.slippage_tolerance_percent,
463            config.slippage_tolerance_percent
464        );
465        assert_eq!(deserialized.deadline_seconds, config.deadline_seconds);
466    }
467
468    #[test]
469    fn test_retry_config_serialization() {
470        let config = RetryConfig::default();
471
472        let json = serde_json::to_string(&config).unwrap();
473        assert!(json.contains("\"max_retry_attempts\":3"));
474        assert!(json.contains("\"retry_delay_ms\":1000"));
475        assert!(json.contains("\"retry_backoff_multiplier\":2.0"));
476        assert!(json.contains("\"max_retry_delay_ms\":30000"));
477
478        let deserialized: RetryConfig = serde_json::from_str(&json).unwrap();
479        assert_eq!(deserialized.max_retry_attempts, config.max_retry_attempts);
480        assert_eq!(deserialized.retry_delay_ms, config.retry_delay_ms);
481        assert_eq!(
482            deserialized.retry_backoff_multiplier,
483            config.retry_backoff_multiplier
484        );
485        assert_eq!(deserialized.max_retry_delay_ms, config.max_retry_delay_ms);
486    }
487
488    #[test]
489    fn test_environment_variants() {
490        // Test all environment variants
491        assert_eq!(format!("{:?}", Environment::Development), "Development");
492        assert_eq!(format!("{:?}", Environment::Staging), "Staging");
493        assert_eq!(format!("{:?}", Environment::Production), "Production");
494
495        // Test equality
496        assert_eq!(Environment::Development, Environment::Development);
497        assert_ne!(Environment::Development, Environment::Production);
498    }
499
500    #[test]
501    fn test_config_debug_implementation() {
502        let config = AppConfig::default();
503        let debug_str = format!("{:?}", config);
504        assert!(debug_str.contains("AppConfig"));
505        assert!(debug_str.contains("port: 8080"));
506        assert!(debug_str.contains("environment: Development"));
507    }
508
509    #[test]
510    fn test_config_clone_implementation() {
511        let config = AppConfig::default();
512        let cloned = config.clone();
513        assert_eq!(config.port, cloned.port);
514        assert_eq!(config.environment, cloned.environment);
515        assert_eq!(config.log_level, cloned.log_level);
516        assert_eq!(config.use_testnet, cloned.use_testnet);
517    }
518
519    #[test]
520    fn test_app_config_with_custom_values() {
521        let mut config = AppConfig::default();
522        config.port = 3000;
523        config.environment = Environment::Production;
524        config.log_level = LogLevel::Debug;
525        config.use_testnet = true;
526        config.transaction.max_gas_price_gwei = 200;
527        config.retry.max_retry_attempts = 5;
528
529        assert!(config.validate().is_ok());
530        assert_eq!(config.port, 3000);
531        assert_eq!(config.environment, Environment::Production);
532        assert_eq!(config.log_level, LogLevel::Debug);
533        assert!(config.use_testnet);
534        assert_eq!(config.transaction.max_gas_price_gwei, 200);
535        assert_eq!(config.retry.max_retry_attempts, 5);
536    }
537}