subx_cli/config/
validator.rs1use 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
21pub 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 validate_config_consistency(config)?;
40
41 Ok(())
42}
43
44pub fn validate_ai_config(ai_config: &AIConfig) -> Result<()> {
46 validate_non_empty_string(&ai_config.provider, "AI provider")?;
47
48 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_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_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
97pub fn validate_sync_config(sync_config: &SyncConfig) -> Result<()> {
99 sync_config.validate()
101}
102
103pub fn validate_general_config(general_config: &GeneralConfig) -> Result<()> {
105 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_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
127pub fn validate_formats_config(formats_config: &FormatsConfig) -> Result<()> {
129 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 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 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
151pub fn validate_parallel_config(parallel_config: &ParallelConfig) -> Result<()> {
153 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 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
168fn validate_config_consistency(config: &Config) -> Result<()> {
170 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 }
181
182 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 pub fn validate(&self) -> Result<()> {
212 validate_enum(&self.default_method, &["vad", "auto", "manual"])?;
214
215 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 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 self.vad.validate()?;
238
239 Ok(())
240 }
241}
242
243impl VadConfig {
244 pub fn validate(&self) -> Result<()> {
259 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 if self.padding_chunks > 10 {
267 return Err(SubXError::config("VAD padding_chunks must not exceed 10"));
268 }
269 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; 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; 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()); assert!(validate_config(&config).is_err());
341
342 config.ai.api_key = Some("sk-valid123".to_string());
344 assert!(validate_config(&config).is_ok());
345
346 config.ai.api_key = None;
348 assert!(validate_config(&config).is_ok());
349 }
350}