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 "openrouter" => {
68 if let Some(api_key) = &ai_config.api_key {
69 if !api_key.is_empty() {
70 validate_api_key(api_key)?;
71 }
73 }
74 validate_ai_model(&ai_config.model)?;
75 validate_temperature(ai_config.temperature)?;
76 validate_positive_number(ai_config.max_tokens as f64)?;
77
78 if !ai_config.base_url.is_empty() {
79 validate_url_format(&ai_config.base_url)?;
80 }
81 }
82 "anthropic" => {
83 if let Some(api_key) = &ai_config.api_key {
84 if !api_key.is_empty() {
85 validate_api_key(api_key)?;
86 }
87 }
88 validate_ai_model(&ai_config.model)?;
89 validate_temperature(ai_config.temperature)?;
90 }
91 "azure-openai" => {
92 if let Some(api_key) = &ai_config.api_key {
93 if !api_key.is_empty() {
94 validate_api_key(api_key)?;
95 }
96 }
97 validate_ai_model(&ai_config.model)?;
98 validate_temperature(ai_config.temperature)?;
99 validate_positive_number(ai_config.max_tokens as f64)?;
100 if let Some(ver) = &ai_config.api_version {
101 if ver.trim().is_empty() {
102 return Err(SubXError::config(
103 "Azure OpenAI api_version must not be empty",
104 ));
105 }
106 }
107 if !ai_config.base_url.is_empty() {
108 validate_url_format(&ai_config.base_url)?;
109 }
110 }
111 _ => {
112 return Err(SubXError::config(format!(
113 "Unsupported AI provider: {}. Supported providers: openai, openrouter, anthropic, azure-openai",
114 ai_config.provider
115 )));
116 }
117 }
118
119 validate_positive_number(ai_config.retry_attempts as f64)?;
121 if ai_config.retry_attempts > 10 {
122 return Err(SubXError::config("Retry count cannot exceed 10 times"));
123 }
124
125 validate_range(ai_config.request_timeout_seconds as f64, 10.0, 600.0)
127 .map_err(|_| SubXError::config("Request timeout must be between 10 and 600 seconds"))?;
128
129 Ok(())
130}
131
132pub fn validate_sync_config(sync_config: &SyncConfig) -> Result<()> {
134 sync_config.validate()
136}
137
138pub fn validate_general_config(general_config: &GeneralConfig) -> Result<()> {
140 validate_positive_number(general_config.max_concurrent_jobs as f64)?;
142 if general_config.max_concurrent_jobs > 64 {
143 return Err(SubXError::config(
144 "Maximum concurrent jobs should not exceed 64",
145 ));
146 }
147
148 validate_range(general_config.task_timeout_seconds as f64, 30.0, 3600.0)
150 .map_err(|_| SubXError::config("Task timeout must be between 30 and 3600 seconds"))?;
151
152 validate_range(
153 general_config.worker_idle_timeout_seconds as f64,
154 10.0,
155 3600.0,
156 )
157 .map_err(|_| SubXError::config("Worker idle timeout must be between 10 and 3600 seconds"))?;
158
159 Ok(())
160}
161
162pub fn validate_formats_config(formats_config: &FormatsConfig) -> Result<()> {
164 validate_non_empty_string(&formats_config.default_output, "Default output format")?;
166 validate_enum(
167 &formats_config.default_output,
168 &["srt", "ass", "vtt", "webvtt"],
169 )?;
170
171 validate_non_empty_string(&formats_config.default_encoding, "Default encoding")?;
173 validate_enum(
174 &formats_config.default_encoding,
175 &["utf-8", "gbk", "big5", "shift_jis"],
176 )?;
177
178 validate_range(formats_config.encoding_detection_confidence, 0.0, 1.0).map_err(|_| {
180 SubXError::config("Encoding detection confidence must be between 0.0 and 1.0")
181 })?;
182
183 Ok(())
184}
185
186pub fn validate_parallel_config(parallel_config: &ParallelConfig) -> Result<()> {
188 validate_positive_number(parallel_config.max_workers as f64)?;
190 if parallel_config.max_workers > 64 {
191 return Err(SubXError::config("Maximum workers should not exceed 64"));
192 }
193
194 validate_positive_number(parallel_config.task_queue_size as f64)?;
196 if parallel_config.task_queue_size < 100 {
197 return Err(SubXError::config("Task queue size should be at least 100"));
198 }
199
200 Ok(())
201}
202
203fn validate_config_consistency(config: &Config) -> Result<()> {
205 if config.ai.provider == "openai" {
207 if let Some(api_key) = &config.ai.api_key {
208 if api_key.is_empty() {
209 return Err(SubXError::config(
210 "OpenAI provider is selected but API key is empty",
211 ));
212 }
213 }
214 }
216
217 if config.parallel.max_workers > config.general.max_concurrent_jobs {
219 log::warn!(
220 "Parallel max_workers ({}) exceeds general max_concurrent_jobs ({})",
221 config.parallel.max_workers,
222 config.general.max_concurrent_jobs
223 );
224 }
225
226 Ok(())
227}
228
229impl SyncConfig {
230 pub fn validate(&self) -> Result<()> {
247 validate_enum(&self.default_method, &["vad", "auto", "manual"])?;
249
250 validate_positive_number(self.max_offset_seconds)?;
252 if self.max_offset_seconds > 3600.0 {
253 return Err(SubXError::config(
254 "sync.max_offset_seconds should not exceed 3600 seconds (1 hour). If a larger value is needed, please verify the sync requirements are reasonable.",
255 ));
256 }
257
258 if self.max_offset_seconds < 5.0 {
260 log::warn!(
261 "sync.max_offset_seconds is set to {:.1}s which may be too small. Consider using 30.0-60.0 seconds.",
262 self.max_offset_seconds
263 );
264 } else if self.max_offset_seconds > 600.0 && self.max_offset_seconds <= 3600.0 {
265 log::warn!(
266 "sync.max_offset_seconds is set to {:.1}s which is quite large. Please confirm this meets your requirements.",
267 self.max_offset_seconds
268 );
269 }
270
271 self.vad.validate()?;
273
274 Ok(())
275 }
276}
277
278impl VadConfig {
279 pub fn validate(&self) -> Result<()> {
294 if !(0.0..=1.0).contains(&self.sensitivity) {
296 return Err(SubXError::config(
297 "VAD sensitivity must be between 0.0 and 1.0",
298 ));
299 }
300 if self.padding_chunks > 10 {
302 return Err(SubXError::config("VAD padding_chunks must not exceed 10"));
303 }
304 if self.min_speech_duration_ms > 5000 {
306 return Err(SubXError::config(
307 "VAD min_speech_duration_ms must not exceed 5000ms",
308 ));
309 }
310 Ok(())
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::config::{AIConfig, Config, SyncConfig, VadConfig};
318
319 #[test]
320 fn test_validate_default_config() {
321 let config = Config::default();
322 assert!(validate_config(&config).is_ok());
323 }
324
325 #[test]
326 fn test_validate_ai_config_valid() {
327 let mut ai_config = AIConfig::default();
328 ai_config.provider = "openai".to_string();
329 ai_config.api_key = Some("sk-test123456789".to_string());
330 ai_config.temperature = 0.8;
331 assert!(validate_ai_config(&ai_config).is_ok());
332
333 let mut ai_config = AIConfig::default();
335 ai_config.provider = "openrouter".to_string();
336 ai_config.api_key = Some("test-openrouter-key".to_string());
337 ai_config.model = "deepseek/deepseek-r1-0528:free".to_string();
338 assert!(validate_ai_config(&ai_config).is_ok());
339
340 let mut ai_config = AIConfig::default();
342 ai_config.provider = "azure-openai".to_string();
343 ai_config.api_key = Some("azure-key-123".to_string());
344 ai_config.model = "dep123".to_string();
345 ai_config.api_version = Some("2025-04-01-preview".to_string());
346 assert!(validate_ai_config(&ai_config).is_ok());
347 }
348
349 #[test]
350 fn test_validate_ai_config_invalid_provider() {
351 let mut ai_config = AIConfig::default();
352 ai_config.provider = "invalid".to_string();
353 let err = validate_ai_config(&ai_config).unwrap_err();
354 assert!(err.to_string().contains(
355 "Unsupported AI provider: invalid. Supported providers: openai, openrouter, anthropic, azure-openai"
356 ));
357 }
358
359 #[test]
360 fn test_validate_ai_config_invalid_temperature() {
361 let mut ai_config = AIConfig::default();
362 ai_config.provider = "openai".to_string();
363 ai_config.temperature = 3.0; assert!(validate_ai_config(&ai_config).is_err());
365 }
366
367 #[test]
368 fn test_validate_ai_config_invalid_openai_key() {
369 let mut ai_config = AIConfig::default();
370 ai_config.provider = "openai".to_string();
371 ai_config.api_key = Some("invalid-key".to_string());
372 assert!(validate_ai_config(&ai_config).is_err());
373 }
374
375 #[test]
376 fn test_validate_sync_config_valid() {
377 let sync_config = SyncConfig::default();
378 assert!(validate_sync_config(&sync_config).is_ok());
379 }
380
381 #[test]
382 fn test_validate_vad_config_invalid_sensitivity() {
383 let mut vad_config = VadConfig::default();
384 vad_config.sensitivity = 1.5; assert!(vad_config.validate().is_err());
386 }
387
388 #[test]
389 fn test_validate_config_consistency() {
390 let mut config = Config::default();
391 config.ai.provider = "openai".to_string();
392 config.ai.api_key = Some("".to_string()); assert!(validate_config(&config).is_err());
394
395 config.ai.api_key = Some("sk-valid123".to_string());
397 assert!(validate_config(&config).is_ok());
398
399 config.ai.api_key = None;
401 assert!(validate_config(&config).is_ok());
402 }
403}