subx_cli/config/
validator.rs

1//! Configuration validation module providing validation rules and constraints.
2//!
3//! This module provides comprehensive validation functionality for configuration
4//! values, ensuring that all settings meet business requirements and system
5//! constraints before being used by the application.
6
7use crate::Result;
8use crate::config::Config;
9use crate::config::{SyncConfig, VadConfig};
10use crate::error::SubXError;
11
12/// Trait defining the validation interface for configuration sections.
13pub trait ConfigValidator {
14    /// Validate the configuration and return any errors found.
15    fn validate(&self, config: &Config) -> Result<()>;
16}
17
18/// AI configuration validator.
19pub struct AIValidator;
20
21impl ConfigValidator for AIValidator {
22    fn validate(&self, config: &Config) -> Result<()> {
23        // Check AI provider
24        match config.ai.provider.as_str() {
25            "openai" | "anthropic" => {}
26            _ => {
27                return Err(SubXError::config(format!(
28                    "Unsupported AI provider: {}",
29                    config.ai.provider
30                )));
31            }
32        }
33
34        // Check API key format for OpenAI
35        if config.ai.provider == "openai" {
36            if let Some(api_key) = config.ai.api_key.as_deref() {
37                if !api_key.starts_with("sk-") && !api_key.is_empty() {
38                    return Err(SubXError::config("OpenAI API key must start with 'sk-'"));
39                }
40            }
41        }
42
43        // Check temperature range
44        if config.ai.temperature < 0.0 || config.ai.temperature > 2.0 {
45            return Err(SubXError::config(
46                "Temperature value must be between 0.0 and 2.0",
47            ));
48        }
49
50        // Check retry attempts
51        if config.ai.retry_attempts > 10 {
52            return Err(SubXError::config("Retry count cannot exceed 10 times"));
53        }
54
55        // Check request timeout
56        if config.ai.request_timeout_seconds < 10 {
57            return Err(SubXError::config(
58                "Request timeout must be at least 10 seconds to allow for network latency",
59            ));
60        }
61
62        if config.ai.request_timeout_seconds > 600 {
63            return Err(SubXError::config(
64                "Request timeout should not exceed 600 seconds (10 minutes) to avoid excessive waiting",
65            ));
66        }
67
68        Ok(())
69    }
70}
71
72/// Sync configuration validator.
73pub struct SyncValidator;
74
75impl ConfigValidator for SyncValidator {
76    fn validate(&self, config: &Config) -> Result<()> {
77        // Delegate to SyncConfig's own validation logic
78        config.sync.validate()
79    }
80}
81
82impl SyncConfig {
83    /// Validate the sync configuration for correctness.
84    ///
85    /// Checks all sync-related configuration parameters to ensure they
86    /// are within valid ranges and have acceptable values.
87    ///
88    /// # Returns
89    ///
90    /// Returns `Ok(())` if validation passes, or an error describing
91    /// the validation failure.
92    ///
93    /// # Errors
94    ///
95    /// This function returns an error if:
96    /// - `default_method` is not one of the supported methods
97    /// - `max_offset_seconds` is outside the valid range
98    /// - VAD configuration validation fails
99    pub fn validate(&self) -> Result<()> {
100        // Validate default_method
101        match self.default_method.as_str() {
102            "vad" | "auto" | "manual" => {}
103            _ => {
104                return Err(SubXError::config(
105                    "sync.default_method must be one of: vad, auto, manual",
106                ));
107            }
108        }
109
110        // Validate max_offset_seconds
111        if self.max_offset_seconds <= 0.0 {
112            return Err(SubXError::config(
113                "sync.max_offset_seconds must be greater than 0. Recommended range: 30.0 to 300.0 seconds.",
114            ));
115        }
116
117        if self.max_offset_seconds > 3600.0 {
118            return Err(SubXError::config(
119                "sync.max_offset_seconds should not exceed 3600 seconds (1 hour). If a larger value is needed, please verify the sync requirements are reasonable.",
120            ));
121        }
122
123        // Provide recommendations for common use cases
124        if self.max_offset_seconds < 5.0 {
125            log::warn!(
126                "sync.max_offset_seconds is set to {:.1}s which may be too small. Consider using 30.0-60.0 seconds.",
127                self.max_offset_seconds
128            );
129        } else if self.max_offset_seconds > 600.0 && self.max_offset_seconds <= 3600.0 {
130            log::warn!(
131                "sync.max_offset_seconds is set to {:.1}s which is quite large. Please confirm this meets your requirements.",
132                self.max_offset_seconds
133            );
134        }
135
136        // Validate sub-configurations
137        self.vad.validate()?;
138
139        Ok(())
140    }
141}
142
143impl VadConfig {
144    /// Validate the local VAD configuration for correctness.
145    ///
146    /// Ensures that all VAD-related parameters are within acceptable
147    /// ranges and have valid values for audio processing.
148    ///
149    /// # Returns
150    ///
151    /// Returns `Ok(())` if validation passes, or an error describing
152    /// the validation failure.
153    ///
154    /// # Errors
155    ///
156    /// This function returns an error if:
157    /// - `sensitivity` is outside the valid range (0.0-1.0)
158    /// - `chunk_size` is not a power of 2 or outside valid range
159    /// - `sample_rate` is not one of the supported rates
160    pub fn validate(&self) -> Result<()> {
161        // Validate sensitivity
162        if self.sensitivity < 0.0 || self.sensitivity > 1.0 {
163            return Err(SubXError::config(
164                "sync.vad.sensitivity must be between 0.0 and 1.0",
165            ));
166        }
167
168        // Validate chunk_size (must be power of 2 and reasonable size)
169        if self.chunk_size < 256 || self.chunk_size > 2048 || !self.chunk_size.is_power_of_two() {
170            return Err(SubXError::config(
171                "sync.vad.chunk_size must be a power of 2 between 256 and 2048",
172            ));
173        }
174
175        // Validate sample_rate
176        match self.sample_rate {
177            8000 | 16000 | 22050 | 32000 | 44100 | 48000 => {}
178            _ => {
179                return Err(SubXError::config(
180                    "sync.vad.sample_rate must be one of: 8000, 16000, 22050, 32000, 44100, 48000",
181                ));
182            }
183        }
184
185        Ok(())
186    }
187}
188
189/// Formats configuration validator.
190pub struct FormatsValidator;
191
192impl ConfigValidator for FormatsValidator {
193    fn validate(&self, config: &Config) -> Result<()> {
194        // Check default output format
195        if config.formats.default_output.is_empty() {
196            return Err(SubXError::config("Default output format cannot be empty"));
197        }
198
199        // Check default encoding
200        if config.formats.default_encoding.is_empty() {
201            return Err(SubXError::config("Default encoding cannot be empty"));
202        }
203
204        // Check encoding detection confidence
205        if config.formats.encoding_detection_confidence < 0.0
206            || config.formats.encoding_detection_confidence > 1.0
207        {
208            return Err(SubXError::config(
209                "Encoding detection confidence must be between 0.0 and 1.0",
210            ));
211        }
212
213        Ok(())
214    }
215}
216
217/// Parallel configuration validator.
218pub struct ParallelValidator;
219
220impl ConfigValidator for ParallelValidator {
221    fn validate(&self, config: &Config) -> Result<()> {
222        // Check max workers
223        if config.parallel.max_workers == 0 {
224            return Err(SubXError::config(
225                "Maximum concurrent workers must be greater than 0",
226            ));
227        }
228
229        Ok(())
230    }
231}
232
233/// Validate the complete configuration.
234///
235/// This function runs all configuration validators and returns the first
236/// error encountered, or Ok(()) if all validation passes.
237pub fn validate_config(config: &Config) -> Result<()> {
238    AIValidator.validate(config)?;
239    SyncValidator.validate(config)?;
240    FormatsValidator.validate(config)?;
241    ParallelValidator.validate(config)?;
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::config::Config;
249
250    #[test]
251    fn test_validate_default_config() {
252        let config = Config::default();
253        assert!(validate_config(&config).is_ok());
254    }
255
256    #[test]
257    fn test_invalid_ai_provider() {
258        let mut config = Config::default();
259        config.ai.provider = "invalid".to_string();
260        assert!(validate_config(&config).is_err());
261    }
262
263    #[test]
264    fn test_invalid_temperature() {
265        let mut config = Config::default();
266        config.ai.temperature = 3.0; // Too high
267        assert!(validate_config(&config).is_err());
268    }
269
270    #[test]
271    fn test_invalid_vad_sensitivity() {
272        let mut config = Config::default();
273        config.sync.vad.sensitivity = 1.5; // Too high (should be 0.0-1.0)
274        assert!(validate_config(&config).is_err());
275    }
276}