subx_cli/config/
validator.rs

1//! High-level configuration validation for configuration sections.
2//!
3//! This module provides validation for complete configuration sections and
4//! the entire configuration structure. It builds upon the low-level validation
5//! functions from the [`crate::config::validation`] module.
6//!
7//! # Architecture
8//!
9//! - [`crate::config::validation`] - Low-level validation functions for individual values
10//! - [`crate::config::validator`] (this module) - High-level configuration section validators
11//! - [`crate::config::field_validator`] - Key-value validation for configuration service
12
13use super::validation::*;
14use crate::Result;
15use crate::config::Config;
16use crate::config::{
17    AIConfig, FormatsConfig, GeneralConfig, ParallelConfig, SyncConfig, VadConfig,
18};
19use crate::error::SubXError;
20
21/// Validate the complete configuration.
22///
23/// This function validates all configuration sections and their
24/// interdependencies.
25///
26/// # Arguments
27/// * `config` - The configuration to validate
28///
29/// # Errors
30/// Returns the first validation error encountered.
31pub fn validate_config(config: &Config) -> Result<()> {
32    validate_ai_config(&config.ai)?;
33    validate_sync_config(&config.sync)?;
34    validate_general_config(&config.general)?;
35    validate_formats_config(&config.formats)?;
36    validate_parallel_config(&config.parallel)?;
37
38    // Cross-section validation
39    validate_config_consistency(config)?;
40
41    Ok(())
42}
43
44/// Validate AI configuration section.
45pub fn validate_ai_config(ai_config: &AIConfig) -> Result<()> {
46    validate_non_empty_string(&ai_config.provider, "AI provider")?;
47
48    // Validate provider-specific settings
49    match ai_config.provider.as_str() {
50        "openai" => {
51            if let Some(api_key) = &ai_config.api_key {
52                if !api_key.is_empty() {
53                    validate_api_key(api_key)?;
54                    if !api_key.starts_with("sk-") {
55                        return Err(SubXError::config("OpenAI API key must start with 'sk-'"));
56                    }
57                }
58            }
59            validate_ai_model(&ai_config.model)?;
60            validate_temperature(ai_config.temperature)?;
61            validate_positive_number(ai_config.max_tokens as f64)?;
62
63            if !ai_config.base_url.is_empty() {
64                validate_url_format(&ai_config.base_url)?;
65            }
66        }
67        "anthropic" => {
68            if let Some(api_key) = &ai_config.api_key {
69                if !api_key.is_empty() {
70                    validate_api_key(api_key)?;
71                }
72            }
73            validate_ai_model(&ai_config.model)?;
74            validate_temperature(ai_config.temperature)?;
75        }
76        _ => {
77            return Err(SubXError::config(format!(
78                "Unsupported AI provider: {}. Supported providers: openai, anthropic",
79                ai_config.provider
80            )));
81        }
82    }
83
84    // Validate retry settings
85    validate_positive_number(ai_config.retry_attempts as f64)?;
86    if ai_config.retry_attempts > 10 {
87        return Err(SubXError::config("Retry count cannot exceed 10 times"));
88    }
89
90    // Validate timeout settings
91    validate_range(ai_config.request_timeout_seconds as f64, 10.0, 600.0)
92        .map_err(|_| SubXError::config("Request timeout must be between 10 and 600 seconds"))?;
93
94    Ok(())
95}
96
97/// Validate sync configuration section.
98pub fn validate_sync_config(sync_config: &SyncConfig) -> Result<()> {
99    // Delegate to SyncConfig's validation with enhancements
100    sync_config.validate()
101}
102
103/// Validate general configuration section.
104pub fn validate_general_config(general_config: &GeneralConfig) -> Result<()> {
105    // Validate concurrent jobs
106    validate_positive_number(general_config.max_concurrent_jobs as f64)?;
107    if general_config.max_concurrent_jobs > 64 {
108        return Err(SubXError::config(
109            "Maximum concurrent jobs should not exceed 64",
110        ));
111    }
112
113    // Validate timeout settings
114    validate_range(general_config.task_timeout_seconds as f64, 30.0, 3600.0)
115        .map_err(|_| SubXError::config("Task timeout must be between 30 and 3600 seconds"))?;
116
117    validate_range(
118        general_config.worker_idle_timeout_seconds as f64,
119        10.0,
120        3600.0,
121    )
122    .map_err(|_| SubXError::config("Worker idle timeout must be between 10 and 3600 seconds"))?;
123
124    Ok(())
125}
126
127/// Validate formats configuration section.
128pub fn validate_formats_config(formats_config: &FormatsConfig) -> Result<()> {
129    // Check default output format
130    validate_non_empty_string(&formats_config.default_output, "Default output format")?;
131    validate_enum(
132        &formats_config.default_output,
133        &["srt", "ass", "vtt", "webvtt"],
134    )?;
135
136    // Check default encoding
137    validate_non_empty_string(&formats_config.default_encoding, "Default encoding")?;
138    validate_enum(
139        &formats_config.default_encoding,
140        &["utf-8", "gbk", "big5", "shift_jis"],
141    )?;
142
143    // Check encoding detection confidence
144    validate_range(formats_config.encoding_detection_confidence, 0.0, 1.0).map_err(|_| {
145        SubXError::config("Encoding detection confidence must be between 0.0 and 1.0")
146    })?;
147
148    Ok(())
149}
150
151/// Validate parallel processing configuration.
152pub fn validate_parallel_config(parallel_config: &ParallelConfig) -> Result<()> {
153    // Check max workers
154    validate_positive_number(parallel_config.max_workers as f64)?;
155    if parallel_config.max_workers > 64 {
156        return Err(SubXError::config("Maximum workers should not exceed 64"));
157    }
158
159    // Check task queue size
160    validate_positive_number(parallel_config.task_queue_size as f64)?;
161    if parallel_config.task_queue_size < 100 {
162        return Err(SubXError::config("Task queue size should be at least 100"));
163    }
164
165    Ok(())
166}
167
168/// Validate configuration consistency across sections.
169fn validate_config_consistency(config: &Config) -> Result<()> {
170    // Example: Ensure AI is properly configured if using AI features
171    if config.ai.provider == "openai" {
172        if let Some(api_key) = &config.ai.api_key {
173            if api_key.is_empty() {
174                return Err(SubXError::config(
175                    "OpenAI provider is selected but API key is empty",
176                ));
177            }
178        }
179        // Note: We don't require API key for default config to allow basic operation
180    }
181
182    // Ensure reasonable resource allocation
183    if config.parallel.max_workers > config.general.max_concurrent_jobs {
184        log::warn!(
185            "Parallel max_workers ({}) exceeds general max_concurrent_jobs ({})",
186            config.parallel.max_workers,
187            config.general.max_concurrent_jobs
188        );
189    }
190
191    Ok(())
192}
193
194impl SyncConfig {
195    /// Validate the sync configuration for correctness.
196    ///
197    /// Checks all sync-related configuration parameters to ensure they
198    /// are within valid ranges and have acceptable values.
199    ///
200    /// # Returns
201    ///
202    /// Returns `Ok(())` if validation passes, or an error describing
203    /// the validation failure.
204    ///
205    /// # Errors
206    ///
207    /// This function returns an error if:
208    /// - `default_method` is not one of the supported methods
209    /// - `max_offset_seconds` is outside the valid range
210    /// - VAD configuration validation fails
211    pub fn validate(&self) -> Result<()> {
212        // Validate default_method
213        validate_enum(&self.default_method, &["vad", "auto", "manual"])?;
214
215        // Validate max_offset_seconds
216        validate_positive_number(self.max_offset_seconds)?;
217        if self.max_offset_seconds > 3600.0 {
218            return Err(SubXError::config(
219                "sync.max_offset_seconds should not exceed 3600 seconds (1 hour). If a larger value is needed, please verify the sync requirements are reasonable.",
220            ));
221        }
222
223        // Provide recommendations for common use cases
224        if self.max_offset_seconds < 5.0 {
225            log::warn!(
226                "sync.max_offset_seconds is set to {:.1}s which may be too small. Consider using 30.0-60.0 seconds.",
227                self.max_offset_seconds
228            );
229        } else if self.max_offset_seconds > 600.0 && self.max_offset_seconds <= 3600.0 {
230            log::warn!(
231                "sync.max_offset_seconds is set to {:.1}s which is quite large. Please confirm this meets your requirements.",
232                self.max_offset_seconds
233            );
234        }
235
236        // Validate sub-configurations
237        self.vad.validate()?;
238
239        Ok(())
240    }
241}
242
243impl VadConfig {
244    /// Validate the local VAD configuration for correctness.
245    ///
246    /// Ensures that all VAD-related parameters are within acceptable
247    /// ranges and have valid values for audio processing.
248    ///
249    /// # Returns
250    ///
251    /// Returns `Ok(())` if validation passes, or an error describing
252    /// the validation failure.
253    ///
254    /// # Errors
255    ///
256    /// This function returns an error if:
257    /// - `sensitivity` is outside the valid range (0.0-1.0)
258    pub fn validate(&self) -> Result<()> {
259        // Validate sensitivity range
260        if !(0.0..=1.0).contains(&self.sensitivity) {
261            return Err(SubXError::config(
262                "VAD sensitivity must be between 0.0 and 1.0",
263            ));
264        }
265        // Validate padding_chunks
266        if self.padding_chunks > 10 {
267            return Err(SubXError::config("VAD padding_chunks must not exceed 10"));
268        }
269        // Validate minimum speech duration
270        if self.min_speech_duration_ms > 5000 {
271            return Err(SubXError::config(
272                "VAD min_speech_duration_ms must not exceed 5000ms",
273            ));
274        }
275        Ok(())
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::config::{AIConfig, Config, SyncConfig, VadConfig};
283
284    #[test]
285    fn test_validate_default_config() {
286        let config = Config::default();
287        assert!(validate_config(&config).is_ok());
288    }
289
290    #[test]
291    fn test_validate_ai_config_valid() {
292        let mut ai_config = AIConfig::default();
293        ai_config.provider = "openai".to_string();
294        ai_config.api_key = Some("sk-test123456789".to_string());
295        ai_config.temperature = 0.8;
296        assert!(validate_ai_config(&ai_config).is_ok());
297    }
298
299    #[test]
300    fn test_validate_ai_config_invalid_provider() {
301        let mut ai_config = AIConfig::default();
302        ai_config.provider = "invalid".to_string();
303        assert!(validate_ai_config(&ai_config).is_err());
304    }
305
306    #[test]
307    fn test_validate_ai_config_invalid_temperature() {
308        let mut ai_config = AIConfig::default();
309        ai_config.provider = "openai".to_string();
310        ai_config.temperature = 3.0; // Too high
311        assert!(validate_ai_config(&ai_config).is_err());
312    }
313
314    #[test]
315    fn test_validate_ai_config_invalid_openai_key() {
316        let mut ai_config = AIConfig::default();
317        ai_config.provider = "openai".to_string();
318        ai_config.api_key = Some("invalid-key".to_string());
319        assert!(validate_ai_config(&ai_config).is_err());
320    }
321
322    #[test]
323    fn test_validate_sync_config_valid() {
324        let sync_config = SyncConfig::default();
325        assert!(validate_sync_config(&sync_config).is_ok());
326    }
327
328    #[test]
329    fn test_validate_vad_config_invalid_sensitivity() {
330        let mut vad_config = VadConfig::default();
331        vad_config.sensitivity = 1.5; // Too high (should be 0.0-1.0)
332        assert!(vad_config.validate().is_err());
333    }
334
335    #[test]
336    fn test_validate_config_consistency() {
337        let mut config = Config::default();
338        config.ai.provider = "openai".to_string();
339        config.ai.api_key = Some("".to_string()); // Empty API key should fail
340        assert!(validate_config(&config).is_err());
341
342        // Valid case with proper API key
343        config.ai.api_key = Some("sk-valid123".to_string());
344        assert!(validate_config(&config).is_ok());
345
346        // Valid case with no API key (default state)
347        config.ai.api_key = None;
348        assert!(validate_config(&config).is_ok());
349    }
350}