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 ai_config = AIConfig {
328 provider: "openai".to_string(),
329 api_key: Some("sk-test123456789".to_string()),
330 temperature: 0.8,
331 ..Default::default()
332 };
333 assert!(validate_ai_config(&ai_config).is_ok());
334
335 let ai_config = AIConfig {
337 provider: "openrouter".to_string(),
338 api_key: Some("test-openrouter-key".to_string()),
339 model: "deepseek/deepseek-r1-0528:free".to_string(),
340 ..Default::default()
341 };
342 assert!(validate_ai_config(&ai_config).is_ok());
343
344 let ai_config = AIConfig {
346 provider: "azure-openai".to_string(),
347 api_key: Some("azure-key-123".to_string()),
348 model: "dep123".to_string(),
349 api_version: Some("2025-04-01-preview".to_string()),
350 ..Default::default()
351 };
352 assert!(validate_ai_config(&ai_config).is_ok());
353 }
354
355 #[test]
356 fn test_validate_ai_config_invalid_provider() {
357 let ai_config = AIConfig {
358 provider: "invalid".to_string(),
359 ..Default::default()
360 };
361 let err = validate_ai_config(&ai_config).unwrap_err();
362 assert!(err.to_string().contains(
363 "Unsupported AI provider: invalid. Supported providers: openai, openrouter, anthropic, azure-openai"
364 ));
365 }
366
367 #[test]
368 fn test_validate_ai_config_invalid_temperature() {
369 let ai_config = AIConfig {
370 provider: "openai".to_string(),
371 temperature: 3.0, ..Default::default()
373 };
374 assert!(validate_ai_config(&ai_config).is_err());
375 }
376
377 #[test]
378 fn test_validate_ai_config_invalid_openai_key() {
379 let ai_config = AIConfig {
380 provider: "openai".to_string(),
381 api_key: Some("invalid-key".to_string()),
382 ..Default::default()
383 };
384 assert!(validate_ai_config(&ai_config).is_err());
385 }
386
387 #[test]
388 fn test_validate_sync_config_valid() {
389 let sync_config = SyncConfig::default();
390 assert!(validate_sync_config(&sync_config).is_ok());
391 }
392
393 #[test]
394 fn test_validate_vad_config_invalid_sensitivity() {
395 let vad_config = VadConfig {
396 sensitivity: 1.5, ..Default::default()
398 };
399 assert!(vad_config.validate().is_err());
400 }
401
402 #[test]
403 fn test_validate_config_consistency() {
404 let mut config = Config::default();
405 config.ai.provider = "openai".to_string();
406 config.ai.api_key = Some("".to_string()); assert!(validate_config(&config).is_err());
408
409 config.ai.api_key = Some("sk-valid123".to_string());
411 assert!(validate_config(&config).is_ok());
412
413 config.ai.api_key = None;
415 assert!(validate_config(&config).is_ok());
416 }
417}