1use super::validation::*;
14use crate::{Result, error::SubXError};
15
16pub fn normalize_ai_provider(value: &str) -> String {
53 let trimmed = value.trim().to_ascii_lowercase();
54 if trimmed == "ollama" {
55 "local".to_string()
56 } else {
57 trimmed
58 }
59}
60
61pub fn validate_field(key: &str, value: &str) -> Result<()> {
73 match key {
74 "ai.provider" => {
76 validate_non_empty_string(value, "AI provider")?;
77 validate_enum(
78 value,
79 &[
80 "openai",
81 "anthropic",
82 "local",
83 "ollama",
84 "openrouter",
85 "azure-openai",
86 ],
87 )?;
88 }
89 "ai.model" => validate_ai_model(value)?,
90 "ai.api_key" => {
91 if !value.is_empty() {
92 validate_api_key(value)?;
93 }
94 }
95 "ai.base_url" => validate_url_format(value)?,
96 "ai.temperature" => {
97 let temp: f32 = value
98 .parse()
99 .map_err(|_| SubXError::config("Temperature must be a number"))?;
100 validate_temperature(temp)?;
101 }
102 "ai.max_tokens" => {
103 let tokens: u32 = value
104 .parse()
105 .map_err(|_| SubXError::config("Max tokens must be a positive integer"))?;
106 validate_positive_number(tokens as f64)?;
107 }
108 "ai.max_sample_length" => {
109 let length: usize = value
110 .parse()
111 .map_err(|_| SubXError::config("Max sample length must be a positive integer"))?;
112 validate_range(length, 100, 10000)?;
113 }
114 "ai.retry_attempts" => {
115 let attempts: u32 = value
116 .parse()
117 .map_err(|_| SubXError::config("Retry attempts must be a positive integer"))?;
118 validate_range(attempts, 1, 10)?;
119 }
120 "ai.retry_delay_ms" => {
121 let delay: u64 = value
122 .parse()
123 .map_err(|_| SubXError::config("Retry delay must be a positive integer"))?;
124 validate_range(delay, 100, 30000)?;
125 }
126 "ai.request_timeout_seconds" => {
127 let timeout: u64 = value
128 .parse()
129 .map_err(|_| SubXError::config("Request timeout must be a positive integer"))?;
130 validate_range(timeout, 10, 600)?;
131 }
132
133 "ai.api_version" => {
135 validate_non_empty_string(value, "Azure OpenAI API version")?;
136 }
137
138 "sync.default_method" => {
140 validate_enum(value, &["auto", "vad", "manual"])?;
141 }
142 "sync.max_offset_seconds" => {
143 let offset: f32 = value
144 .parse()
145 .map_err(|_| SubXError::config("Max offset must be a number"))?;
146 validate_range(offset, 0.1, 3600.0)?;
147 }
148 "sync.vad.enabled" => {
149 parse_bool(value)?;
150 }
151 "sync.vad.sensitivity" => {
152 let sensitivity: f32 = value
153 .parse()
154 .map_err(|_| SubXError::config("VAD sensitivity must be a number"))?;
155 validate_range(sensitivity, 0.0, 1.0)?;
156 }
157 "sync.vad.padding_chunks" => {
158 let chunks: u32 = value
159 .parse()
160 .map_err(|_| SubXError::config("Padding chunks must be a non-negative integer"))?;
161 validate_range(chunks, 0, 10)?;
162 }
163 "sync.vad.min_speech_duration_ms" => {
164 let _duration: u32 = value.parse().map_err(|_| {
165 SubXError::config("Min speech duration must be a non-negative integer")
166 })?;
167 }
169
170 "formats.default_output" => {
172 validate_enum(value, &["srt", "ass", "vtt", "webvtt"])?;
173 }
174 "formats.preserve_styling" => {
175 parse_bool(value)?;
176 }
177 "formats.default_encoding" => {
178 validate_enum(value, &["utf-8", "gbk", "big5", "shift_jis"])?;
179 }
180 "formats.encoding_detection_confidence" => {
181 let confidence: f32 = value
182 .parse()
183 .map_err(|_| SubXError::config("Encoding detection confidence must be a number"))?;
184 validate_range(confidence, 0.0, 1.0)?;
185 }
186
187 "general.backup_enabled" => {
189 parse_bool(value)?;
190 }
191 "general.max_concurrent_jobs" => {
192 let jobs: usize = value
193 .parse()
194 .map_err(|_| SubXError::config("Max concurrent jobs must be a positive integer"))?;
195 validate_range(jobs, 1, 64)?;
196 }
197 "general.task_timeout_seconds" => {
198 let timeout: u64 = value
199 .parse()
200 .map_err(|_| SubXError::config("Task timeout must be a positive integer"))?;
201 validate_range(timeout, 30, 3600)?;
202 }
203 "general.enable_progress_bar" => {
204 parse_bool(value)?;
205 }
206 "general.worker_idle_timeout_seconds" => {
207 let timeout: u64 = value
208 .parse()
209 .map_err(|_| SubXError::config("Worker idle timeout must be a positive integer"))?;
210 validate_range(timeout, 10, 3600)?;
211 }
212 "general.max_subtitle_bytes" => {
213 let bytes: u64 = value
214 .parse()
215 .map_err(|_| SubXError::config("max_subtitle_bytes must be a positive integer"))?;
216 validate_range(bytes, 1024_u64, 1_073_741_824_u64)?;
217 }
218 "general.max_audio_bytes" => {
219 let bytes: u64 = value
220 .parse()
221 .map_err(|_| SubXError::config("max_audio_bytes must be a positive integer"))?;
222 validate_range(bytes, 1024_u64, 10_737_418_240_u64)?;
223 }
224
225 "parallel.max_workers" => {
227 let workers: usize = value
228 .parse()
229 .map_err(|_| SubXError::config("Max workers must be a positive integer"))?;
230 validate_range(workers, 1, 64)?;
231 }
232 "parallel.task_queue_size" => {
233 let size: usize = value
234 .parse()
235 .map_err(|_| SubXError::config("Task queue size must be a positive integer"))?;
236 validate_range(size, 100, 10000)?;
237 }
238 "parallel.enable_task_priorities" => {
239 parse_bool(value)?;
240 }
241 "parallel.auto_balance_workers" => {
242 parse_bool(value)?;
243 }
244 "parallel.overflow_strategy" => {
245 validate_enum(value, &["Block", "Drop", "Expand"])?;
246 }
247
248 "translation.batch_size" => {
250 let size: usize = value.parse().map_err(|_| {
251 SubXError::config("Translation batch size must be a positive integer")
252 })?;
253 validate_range(size, 1, 1000)?;
254 }
255 "translation.default_target_language" => {
256 if !value.is_empty() {
257 validate_non_empty_string(value, "translation.default_target_language")?;
258 }
259 }
260
261 _ => {
262 return Err(SubXError::config(format!(
263 "Unknown configuration key: {key}"
264 )));
265 }
266 }
267
268 Ok(())
269}
270
271pub fn validate_all_fields(config: &crate::config::Config) -> Result<()> {
285 let ai = &config.ai;
286 validate_field("ai.provider", &ai.provider)?;
287 validate_field("ai.model", &ai.model)?;
288 if let Some(ref key) = ai.api_key {
289 validate_field("ai.api_key", key)?;
290 }
291 validate_field("ai.base_url", &ai.base_url)?;
292 validate_field("ai.temperature", &ai.temperature.to_string())?;
293 validate_field("ai.max_tokens", &ai.max_tokens.to_string())?;
294 validate_field("ai.max_sample_length", &ai.max_sample_length.to_string())?;
295 validate_field("ai.retry_attempts", &ai.retry_attempts.to_string())?;
296 validate_field("ai.retry_delay_ms", &ai.retry_delay_ms.to_string())?;
297 validate_field(
298 "ai.request_timeout_seconds",
299 &ai.request_timeout_seconds.to_string(),
300 )?;
301 if let Some(ref v) = ai.api_version {
302 validate_field("ai.api_version", v)?;
303 }
304
305 let s = &config.sync;
306 validate_field("sync.default_method", &s.default_method)?;
307 validate_field("sync.max_offset_seconds", &s.max_offset_seconds.to_string())?;
308 validate_field("sync.vad.enabled", &s.vad.enabled.to_string())?;
309 validate_field("sync.vad.sensitivity", &s.vad.sensitivity.to_string())?;
310 validate_field("sync.vad.padding_chunks", &s.vad.padding_chunks.to_string())?;
311 validate_field(
312 "sync.vad.min_speech_duration_ms",
313 &s.vad.min_speech_duration_ms.to_string(),
314 )?;
315
316 let f = &config.formats;
317 validate_field("formats.default_output", &f.default_output)?;
318 validate_field("formats.preserve_styling", &f.preserve_styling.to_string())?;
319 validate_field("formats.default_encoding", &f.default_encoding)?;
320 validate_field(
321 "formats.encoding_detection_confidence",
322 &f.encoding_detection_confidence.to_string(),
323 )?;
324
325 let g = &config.general;
326 validate_field("general.backup_enabled", &g.backup_enabled.to_string())?;
327 validate_field(
328 "general.max_concurrent_jobs",
329 &g.max_concurrent_jobs.to_string(),
330 )?;
331 validate_field(
332 "general.task_timeout_seconds",
333 &g.task_timeout_seconds.to_string(),
334 )?;
335 validate_field(
336 "general.enable_progress_bar",
337 &g.enable_progress_bar.to_string(),
338 )?;
339 validate_field(
340 "general.worker_idle_timeout_seconds",
341 &g.worker_idle_timeout_seconds.to_string(),
342 )?;
343 validate_field(
344 "general.max_subtitle_bytes",
345 &g.max_subtitle_bytes.to_string(),
346 )?;
347 validate_field("general.max_audio_bytes", &g.max_audio_bytes.to_string())?;
348
349 let p = &config.parallel;
350 validate_field("parallel.max_workers", &p.max_workers.to_string())?;
351 validate_field("parallel.task_queue_size", &p.task_queue_size.to_string())?;
352 validate_field(
353 "parallel.enable_task_priorities",
354 &p.enable_task_priorities.to_string(),
355 )?;
356 validate_field(
357 "parallel.auto_balance_workers",
358 &p.auto_balance_workers.to_string(),
359 )?;
360 validate_field(
361 "parallel.overflow_strategy",
362 &format!("{:?}", p.overflow_strategy),
363 )?;
364
365 let t = &config.translation;
366 validate_field("translation.batch_size", &t.batch_size.to_string())?;
367 if let Some(ref lang) = t.default_target_language {
368 validate_field("translation.default_target_language", lang)?;
369 }
370
371 Ok(())
372}
373
374pub fn get_field_description(key: &str) -> &'static str {
376 match key {
377 "ai.provider" => {
378 "AI service provider ('openai', 'openrouter', 'azure-openai', or 'local'; 'ollama' is accepted as an alias for 'local')"
379 }
380 "ai.model" => "AI model name (e.g., 'gpt-4.1-mini')",
381 "ai.api_key" => "API key for the AI service",
382 "ai.base_url" => "Custom API endpoint URL (optional)",
383 "ai.temperature" => "AI response randomness (0.0-2.0)",
384 "ai.max_tokens" => "Maximum tokens in AI response",
385 "ai.max_sample_length" => "Maximum sample length for AI processing",
386 "ai.retry_attempts" => "Number of retry attempts for AI requests",
387 "ai.retry_delay_ms" => "Delay between retry attempts in milliseconds",
388 "ai.request_timeout_seconds" => "Request timeout in seconds",
389 "ai.api_version" => "Azure OpenAI API version (optional, defaults to latest)",
390
391 "sync.default_method" => "Synchronization method ('auto', 'vad', or 'manual')",
392 "sync.max_offset_seconds" => "Maximum allowed time offset in seconds",
393 "sync.vad.enabled" => "Enable voice activity detection",
394 "sync.vad.sensitivity" => "Voice activity detection threshold (0.0-1.0)",
395 "sync.vad.chunk_size" => "VAD processing chunk size (must be power of 2)",
396 "sync.vad.sample_rate" => "Audio sample rate for VAD processing",
397 "sync.vad.padding_chunks" => "Number of padding chunks for VAD",
398 "sync.vad.min_speech_duration_ms" => "Minimum speech duration in milliseconds",
399
400 "formats.default_output" => "Default output format for subtitles",
401 "formats.preserve_styling" => "Preserve subtitle styling information",
402 "formats.default_encoding" => "Default character encoding",
403 "formats.encoding_detection_confidence" => "Confidence threshold for encoding detection",
404
405 "general.backup_enabled" => "Enable automatic backup creation",
406 "general.max_concurrent_jobs" => "Maximum number of concurrent jobs",
407 "general.task_timeout_seconds" => "Task timeout in seconds",
408 "general.enable_progress_bar" => "Enable progress bar display",
409 "general.worker_idle_timeout_seconds" => "Worker idle timeout in seconds",
410 "general.max_subtitle_bytes" => "Maximum subtitle file size in bytes",
411 "general.max_audio_bytes" => "Maximum audio file size in bytes",
412
413 "parallel.max_workers" => "Maximum number of worker threads",
414 "parallel.task_queue_size" => "Size of the task queue",
415 "parallel.enable_task_priorities" => "Enable task priority system",
416 "parallel.auto_balance_workers" => "Enable automatic worker load balancing",
417 "parallel.overflow_strategy" => "Strategy for handling queue overflow",
418
419 "translation.batch_size" => "Maximum subtitle cues per AI translation request (1-1000)",
420 "translation.default_target_language" => {
421 "Default target language used when --target-language is omitted"
422 }
423
424 _ => "Configuration field",
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
435 fn test_ai_provider_valid_all_enum_values() {
436 for v in &[
437 "openai",
438 "anthropic",
439 "local",
440 "ollama",
441 "openrouter",
442 "azure-openai",
443 ] {
444 assert!(validate_field("ai.provider", v).is_ok(), "provider={v}");
445 }
446 }
447
448 #[test]
451 fn test_normalize_ai_provider_ollama_alias() {
452 assert_eq!(normalize_ai_provider("ollama"), "local");
453 assert_eq!(normalize_ai_provider("OLLAMA"), "local");
454 assert_eq!(normalize_ai_provider(" ollama "), "local");
455 assert_eq!(normalize_ai_provider("\tOllama\n"), "local");
456 }
457
458 #[test]
459 fn test_normalize_ai_provider_canonical_pass_through() {
460 for v in &["openai", "openrouter", "azure-openai", "local"] {
461 assert_eq!(normalize_ai_provider(v), *v, "canonical input {v}");
462 }
463 }
464
465 #[test]
466 fn test_normalize_ai_provider_unknown_pass_through_lowercased() {
467 assert_eq!(normalize_ai_provider("grok"), "grok");
470 assert_eq!(normalize_ai_provider("GROK"), "grok");
471 assert_eq!(
472 normalize_ai_provider(" unknown-provider "),
473 "unknown-provider"
474 );
475 }
476
477 #[test]
478 fn test_ai_provider_invalid_value() {
479 assert!(validate_field("ai.provider", "grok").is_err());
480 assert!(validate_field("ai.provider", "OPENAI").is_err()); }
482
483 #[test]
484 fn test_ai_provider_empty_value() {
485 assert!(validate_field("ai.provider", "").is_err());
486 }
487
488 #[test]
491 fn test_ai_model_valid() {
492 assert!(validate_field("ai.model", "gpt-4.1-mini").is_ok());
493 assert!(validate_field("ai.model", &"a".repeat(100)).is_ok()); }
495
496 #[test]
497 fn test_ai_model_empty() {
498 assert!(validate_field("ai.model", "").is_err());
499 }
500
501 #[test]
502 fn test_ai_model_too_long() {
503 assert!(validate_field("ai.model", &"a".repeat(101)).is_err());
504 }
505
506 #[test]
509 fn test_ai_api_key_empty_is_ok() {
510 assert!(validate_field("ai.api_key", "").is_ok());
512 }
513
514 #[test]
515 fn test_ai_api_key_valid() {
516 assert!(validate_field("ai.api_key", "sk-abcdefghij1234").is_ok());
517 }
518
519 #[test]
520 fn test_ai_api_key_too_short() {
521 assert!(validate_field("ai.api_key", "short").is_err()); }
523
524 #[test]
527 fn test_ai_base_url_valid() {
528 assert!(validate_field("ai.base_url", "https://api.openai.com/v1").is_ok());
529 assert!(validate_field("ai.base_url", "http://localhost:8080").is_ok());
530 }
531
532 #[test]
533 fn test_ai_base_url_empty_is_ok() {
534 assert!(validate_field("ai.base_url", "").is_ok());
535 assert!(validate_field("ai.base_url", " ").is_ok());
536 }
537
538 #[test]
539 fn test_ai_base_url_invalid() {
540 assert!(validate_field("ai.base_url", "not-a-url").is_err());
541 assert!(validate_field("ai.base_url", "://missing-scheme").is_err());
542 }
543
544 #[test]
547 fn test_ai_temperature_valid_boundaries() {
548 assert!(validate_field("ai.temperature", "0.0").is_ok());
549 assert!(validate_field("ai.temperature", "2.0").is_ok());
550 assert!(validate_field("ai.temperature", "1.0").is_ok());
551 }
552
553 #[test]
554 fn test_ai_temperature_out_of_range() {
555 assert!(validate_field("ai.temperature", "-0.1").is_err());
556 assert!(validate_field("ai.temperature", "2.1").is_err());
557 }
558
559 #[test]
560 fn test_ai_temperature_parse_error() {
561 assert!(validate_field("ai.temperature", "hot").is_err());
562 assert!(validate_field("ai.temperature", "").is_err());
563 }
564
565 #[test]
568 fn test_ai_max_tokens_valid() {
569 assert!(validate_field("ai.max_tokens", "1").is_ok());
570 assert!(validate_field("ai.max_tokens", "4096").is_ok());
571 }
572
573 #[test]
574 fn test_ai_max_tokens_zero_is_err() {
575 assert!(validate_field("ai.max_tokens", "0").is_err());
576 }
577
578 #[test]
579 fn test_ai_max_tokens_parse_error() {
580 assert!(validate_field("ai.max_tokens", "lots").is_err());
581 assert!(validate_field("ai.max_tokens", "-1").is_err());
582 }
583
584 #[test]
587 fn test_ai_max_sample_length_valid_boundaries() {
588 assert!(validate_field("ai.max_sample_length", "100").is_ok());
589 assert!(validate_field("ai.max_sample_length", "10000").is_ok());
590 assert!(validate_field("ai.max_sample_length", "5000").is_ok());
591 }
592
593 #[test]
594 fn test_ai_max_sample_length_out_of_range() {
595 assert!(validate_field("ai.max_sample_length", "99").is_err());
596 assert!(validate_field("ai.max_sample_length", "10001").is_err());
597 }
598
599 #[test]
600 fn test_ai_max_sample_length_parse_error() {
601 assert!(validate_field("ai.max_sample_length", "big").is_err());
602 }
603
604 #[test]
607 fn test_ai_retry_attempts_valid_boundaries() {
608 assert!(validate_field("ai.retry_attempts", "1").is_ok());
609 assert!(validate_field("ai.retry_attempts", "10").is_ok());
610 assert!(validate_field("ai.retry_attempts", "3").is_ok());
611 }
612
613 #[test]
614 fn test_ai_retry_attempts_out_of_range() {
615 assert!(validate_field("ai.retry_attempts", "0").is_err());
616 assert!(validate_field("ai.retry_attempts", "11").is_err());
617 }
618
619 #[test]
620 fn test_ai_retry_attempts_parse_error() {
621 assert!(validate_field("ai.retry_attempts", "many").is_err());
622 }
623
624 #[test]
627 fn test_ai_retry_delay_ms_valid_boundaries() {
628 assert!(validate_field("ai.retry_delay_ms", "100").is_ok());
629 assert!(validate_field("ai.retry_delay_ms", "30000").is_ok());
630 assert!(validate_field("ai.retry_delay_ms", "1000").is_ok());
631 }
632
633 #[test]
634 fn test_ai_retry_delay_ms_out_of_range() {
635 assert!(validate_field("ai.retry_delay_ms", "99").is_err());
636 assert!(validate_field("ai.retry_delay_ms", "30001").is_err());
637 }
638
639 #[test]
640 fn test_ai_retry_delay_ms_parse_error() {
641 assert!(validate_field("ai.retry_delay_ms", "fast").is_err());
642 }
643
644 #[test]
647 fn test_ai_request_timeout_seconds_valid_boundaries() {
648 assert!(validate_field("ai.request_timeout_seconds", "10").is_ok());
649 assert!(validate_field("ai.request_timeout_seconds", "600").is_ok());
650 assert!(validate_field("ai.request_timeout_seconds", "60").is_ok());
651 }
652
653 #[test]
654 fn test_ai_request_timeout_seconds_out_of_range() {
655 assert!(validate_field("ai.request_timeout_seconds", "9").is_err());
656 assert!(validate_field("ai.request_timeout_seconds", "601").is_err());
657 }
658
659 #[test]
660 fn test_ai_request_timeout_seconds_parse_error() {
661 assert!(validate_field("ai.request_timeout_seconds", "now").is_err());
662 }
663
664 #[test]
667 fn test_ai_api_version_valid() {
668 assert!(validate_field("ai.api_version", "2024-02-15-preview").is_ok());
669 assert!(validate_field("ai.api_version", "v1").is_ok());
670 }
671
672 #[test]
673 fn test_ai_api_version_empty_is_err() {
674 assert!(validate_field("ai.api_version", "").is_err());
675 assert!(validate_field("ai.api_version", " ").is_err());
676 }
677
678 #[test]
681 fn test_sync_default_method_valid_all_values() {
682 assert!(validate_field("sync.default_method", "auto").is_ok());
683 assert!(validate_field("sync.default_method", "vad").is_ok());
684 assert!(validate_field("sync.default_method", "manual").is_ok());
685 }
686
687 #[test]
688 fn test_sync_default_method_invalid() {
689 assert!(validate_field("sync.default_method", "force").is_err());
690 assert!(validate_field("sync.default_method", "").is_err());
691 }
692
693 #[test]
696 fn test_sync_max_offset_seconds_valid_boundaries() {
697 assert!(validate_field("sync.max_offset_seconds", "0.1").is_ok());
698 assert!(validate_field("sync.max_offset_seconds", "3600.0").is_ok());
699 assert!(validate_field("sync.max_offset_seconds", "10.0").is_ok());
700 }
701
702 #[test]
703 fn test_sync_max_offset_seconds_out_of_range() {
704 assert!(validate_field("sync.max_offset_seconds", "0.0").is_err());
705 assert!(validate_field("sync.max_offset_seconds", "3601.0").is_err());
706 }
707
708 #[test]
709 fn test_sync_max_offset_seconds_parse_error() {
710 assert!(validate_field("sync.max_offset_seconds", "abit").is_err());
711 }
712
713 #[test]
716 fn test_sync_vad_enabled_valid() {
717 assert!(validate_field("sync.vad.enabled", "true").is_ok());
718 assert!(validate_field("sync.vad.enabled", "false").is_ok());
719 assert!(validate_field("sync.vad.enabled", "1").is_ok());
720 assert!(validate_field("sync.vad.enabled", "0").is_ok());
721 }
722
723 #[test]
724 fn test_sync_vad_enabled_invalid() {
725 assert!(validate_field("sync.vad.enabled", "maybe").is_err());
726 }
727
728 #[test]
731 fn test_sync_vad_sensitivity_valid_boundaries() {
732 assert!(validate_field("sync.vad.sensitivity", "0.0").is_ok());
733 assert!(validate_field("sync.vad.sensitivity", "1.0").is_ok());
734 assert!(validate_field("sync.vad.sensitivity", "0.5").is_ok());
735 }
736
737 #[test]
738 fn test_sync_vad_sensitivity_out_of_range() {
739 assert!(validate_field("sync.vad.sensitivity", "-0.1").is_err());
740 assert!(validate_field("sync.vad.sensitivity", "1.1").is_err());
741 }
742
743 #[test]
744 fn test_sync_vad_sensitivity_parse_error() {
745 assert!(validate_field("sync.vad.sensitivity", "high").is_err());
746 }
747
748 #[test]
751 fn test_sync_vad_padding_chunks_valid_boundaries() {
752 assert!(validate_field("sync.vad.padding_chunks", "0").is_ok());
753 assert!(validate_field("sync.vad.padding_chunks", "10").is_ok());
754 assert!(validate_field("sync.vad.padding_chunks", "5").is_ok());
755 }
756
757 #[test]
758 fn test_sync_vad_padding_chunks_out_of_range() {
759 assert!(validate_field("sync.vad.padding_chunks", "11").is_err());
760 }
761
762 #[test]
763 fn test_sync_vad_padding_chunks_parse_error() {
764 assert!(validate_field("sync.vad.padding_chunks", "some").is_err());
765 assert!(validate_field("sync.vad.padding_chunks", "-1").is_err());
766 }
767
768 #[test]
771 fn test_sync_vad_min_speech_duration_ms_valid() {
772 assert!(validate_field("sync.vad.min_speech_duration_ms", "0").is_ok());
773 assert!(validate_field("sync.vad.min_speech_duration_ms", "250").is_ok());
774 assert!(validate_field("sync.vad.min_speech_duration_ms", "4294967295").is_ok()); }
776
777 #[test]
778 fn test_sync_vad_min_speech_duration_ms_parse_error() {
779 assert!(validate_field("sync.vad.min_speech_duration_ms", "long").is_err());
780 assert!(validate_field("sync.vad.min_speech_duration_ms", "-1").is_err());
781 }
782
783 #[test]
786 fn test_formats_default_output_valid_all_values() {
787 for v in &["srt", "ass", "vtt", "webvtt"] {
788 assert!(
789 validate_field("formats.default_output", v).is_ok(),
790 "format={v}"
791 );
792 }
793 }
794
795 #[test]
796 fn test_formats_default_output_invalid() {
797 assert!(validate_field("formats.default_output", "txt").is_err());
798 assert!(validate_field("formats.default_output", "SRT").is_err());
799 }
800
801 #[test]
804 fn test_formats_preserve_styling_valid() {
805 assert!(validate_field("formats.preserve_styling", "true").is_ok());
806 assert!(validate_field("formats.preserve_styling", "false").is_ok());
807 assert!(validate_field("formats.preserve_styling", "yes").is_ok());
808 assert!(validate_field("formats.preserve_styling", "no").is_ok());
809 }
810
811 #[test]
812 fn test_formats_preserve_styling_invalid() {
813 assert!(validate_field("formats.preserve_styling", "maybe").is_err());
814 }
815
816 #[test]
819 fn test_formats_default_encoding_valid_all_values() {
820 for v in &["utf-8", "gbk", "big5", "shift_jis"] {
821 assert!(
822 validate_field("formats.default_encoding", v).is_ok(),
823 "enc={v}"
824 );
825 }
826 }
827
828 #[test]
829 fn test_formats_default_encoding_invalid() {
830 assert!(validate_field("formats.default_encoding", "latin1").is_err());
831 assert!(validate_field("formats.default_encoding", "UTF-8").is_err()); }
833
834 #[test]
837 fn test_formats_encoding_detection_confidence_valid_boundaries() {
838 assert!(validate_field("formats.encoding_detection_confidence", "0.0").is_ok());
839 assert!(validate_field("formats.encoding_detection_confidence", "1.0").is_ok());
840 assert!(validate_field("formats.encoding_detection_confidence", "0.75").is_ok());
841 }
842
843 #[test]
844 fn test_formats_encoding_detection_confidence_out_of_range() {
845 assert!(validate_field("formats.encoding_detection_confidence", "-0.1").is_err());
846 assert!(validate_field("formats.encoding_detection_confidence", "1.1").is_err());
847 }
848
849 #[test]
850 fn test_formats_encoding_detection_confidence_parse_error() {
851 assert!(validate_field("formats.encoding_detection_confidence", "high").is_err());
852 }
853
854 #[test]
857 fn test_general_backup_enabled_valid() {
858 assert!(validate_field("general.backup_enabled", "true").is_ok());
859 assert!(validate_field("general.backup_enabled", "false").is_ok());
860 assert!(validate_field("general.backup_enabled", "on").is_ok());
861 assert!(validate_field("general.backup_enabled", "off").is_ok());
862 }
863
864 #[test]
865 fn test_general_backup_enabled_invalid() {
866 assert!(validate_field("general.backup_enabled", "yeah").is_err());
867 }
868
869 #[test]
872 fn test_general_max_concurrent_jobs_valid_boundaries() {
873 assert!(validate_field("general.max_concurrent_jobs", "1").is_ok());
874 assert!(validate_field("general.max_concurrent_jobs", "64").is_ok());
875 assert!(validate_field("general.max_concurrent_jobs", "8").is_ok());
876 }
877
878 #[test]
879 fn test_general_max_concurrent_jobs_out_of_range() {
880 assert!(validate_field("general.max_concurrent_jobs", "0").is_err());
881 assert!(validate_field("general.max_concurrent_jobs", "65").is_err());
882 }
883
884 #[test]
885 fn test_general_max_concurrent_jobs_parse_error() {
886 assert!(validate_field("general.max_concurrent_jobs", "many").is_err());
887 }
888
889 #[test]
892 fn test_general_task_timeout_seconds_valid_boundaries() {
893 assert!(validate_field("general.task_timeout_seconds", "30").is_ok());
894 assert!(validate_field("general.task_timeout_seconds", "3600").is_ok());
895 assert!(validate_field("general.task_timeout_seconds", "300").is_ok());
896 }
897
898 #[test]
899 fn test_general_task_timeout_seconds_out_of_range() {
900 assert!(validate_field("general.task_timeout_seconds", "29").is_err());
901 assert!(validate_field("general.task_timeout_seconds", "3601").is_err());
902 }
903
904 #[test]
905 fn test_general_task_timeout_seconds_parse_error() {
906 assert!(validate_field("general.task_timeout_seconds", "forever").is_err());
907 }
908
909 #[test]
912 fn test_general_enable_progress_bar_valid() {
913 assert!(validate_field("general.enable_progress_bar", "true").is_ok());
914 assert!(validate_field("general.enable_progress_bar", "false").is_ok());
915 }
916
917 #[test]
918 fn test_general_enable_progress_bar_invalid() {
919 assert!(validate_field("general.enable_progress_bar", "show").is_err());
920 }
921
922 #[test]
925 fn test_general_worker_idle_timeout_seconds_valid_boundaries() {
926 assert!(validate_field("general.worker_idle_timeout_seconds", "10").is_ok());
927 assert!(validate_field("general.worker_idle_timeout_seconds", "3600").is_ok());
928 assert!(validate_field("general.worker_idle_timeout_seconds", "60").is_ok());
929 }
930
931 #[test]
932 fn test_general_worker_idle_timeout_seconds_out_of_range() {
933 assert!(validate_field("general.worker_idle_timeout_seconds", "9").is_err());
934 assert!(validate_field("general.worker_idle_timeout_seconds", "3601").is_err());
935 }
936
937 #[test]
938 fn test_general_worker_idle_timeout_seconds_parse_error() {
939 assert!(validate_field("general.worker_idle_timeout_seconds", "soon").is_err());
940 }
941
942 #[test]
945 fn test_general_max_subtitle_bytes_valid_boundaries() {
946 assert!(validate_field("general.max_subtitle_bytes", "1024").is_ok());
947 assert!(validate_field("general.max_subtitle_bytes", "1073741824").is_ok());
948 assert!(validate_field("general.max_subtitle_bytes", "10485760").is_ok());
949 }
950
951 #[test]
952 fn test_general_max_subtitle_bytes_out_of_range() {
953 assert!(validate_field("general.max_subtitle_bytes", "1023").is_err());
954 assert!(validate_field("general.max_subtitle_bytes", "1073741825").is_err());
955 }
956
957 #[test]
958 fn test_general_max_subtitle_bytes_parse_error() {
959 assert!(validate_field("general.max_subtitle_bytes", "huge").is_err());
960 }
961
962 #[test]
965 fn test_general_max_audio_bytes_valid_boundaries() {
966 assert!(validate_field("general.max_audio_bytes", "1024").is_ok());
967 assert!(validate_field("general.max_audio_bytes", "10737418240").is_ok());
968 assert!(validate_field("general.max_audio_bytes", "104857600").is_ok());
969 }
970
971 #[test]
972 fn test_general_max_audio_bytes_out_of_range() {
973 assert!(validate_field("general.max_audio_bytes", "1023").is_err());
974 assert!(validate_field("general.max_audio_bytes", "10737418241").is_err());
975 }
976
977 #[test]
978 fn test_general_max_audio_bytes_parse_error() {
979 assert!(validate_field("general.max_audio_bytes", "big").is_err());
980 }
981
982 #[test]
985 fn test_parallel_max_workers_valid_boundaries() {
986 assert!(validate_field("parallel.max_workers", "1").is_ok());
987 assert!(validate_field("parallel.max_workers", "64").is_ok());
988 assert!(validate_field("parallel.max_workers", "4").is_ok());
989 }
990
991 #[test]
992 fn test_parallel_max_workers_out_of_range() {
993 assert!(validate_field("parallel.max_workers", "0").is_err());
994 assert!(validate_field("parallel.max_workers", "65").is_err());
995 }
996
997 #[test]
998 fn test_parallel_max_workers_parse_error() {
999 assert!(validate_field("parallel.max_workers", "auto").is_err());
1000 }
1001
1002 #[test]
1005 fn test_parallel_task_queue_size_valid_boundaries() {
1006 assert!(validate_field("parallel.task_queue_size", "100").is_ok());
1007 assert!(validate_field("parallel.task_queue_size", "10000").is_ok());
1008 assert!(validate_field("parallel.task_queue_size", "1000").is_ok());
1009 }
1010
1011 #[test]
1012 fn test_parallel_task_queue_size_out_of_range() {
1013 assert!(validate_field("parallel.task_queue_size", "99").is_err());
1014 assert!(validate_field("parallel.task_queue_size", "10001").is_err());
1015 }
1016
1017 #[test]
1018 fn test_parallel_task_queue_size_parse_error() {
1019 assert!(validate_field("parallel.task_queue_size", "large").is_err());
1020 }
1021
1022 #[test]
1025 fn test_parallel_enable_task_priorities_valid() {
1026 assert!(validate_field("parallel.enable_task_priorities", "true").is_ok());
1027 assert!(validate_field("parallel.enable_task_priorities", "false").is_ok());
1028 }
1029
1030 #[test]
1031 fn test_parallel_enable_task_priorities_invalid() {
1032 assert!(validate_field("parallel.enable_task_priorities", "yes_please").is_err());
1033 }
1034
1035 #[test]
1038 fn test_parallel_auto_balance_workers_valid() {
1039 assert!(validate_field("parallel.auto_balance_workers", "true").is_ok());
1040 assert!(validate_field("parallel.auto_balance_workers", "false").is_ok());
1041 }
1042
1043 #[test]
1044 fn test_parallel_auto_balance_workers_invalid() {
1045 assert!(validate_field("parallel.auto_balance_workers", "auto").is_err());
1046 }
1047
1048 #[test]
1051 fn test_parallel_overflow_strategy_valid_all_values() {
1052 for v in &["Block", "Drop", "Expand"] {
1053 assert!(
1054 validate_field("parallel.overflow_strategy", v).is_ok(),
1055 "strategy={v}"
1056 );
1057 }
1058 }
1059
1060 #[test]
1061 fn test_parallel_overflow_strategy_invalid() {
1062 assert!(validate_field("parallel.overflow_strategy", "block").is_err()); assert!(validate_field("parallel.overflow_strategy", "Ignore").is_err());
1064 }
1065
1066 #[test]
1069 fn test_validate_unknown_field_returns_error() {
1070 assert!(validate_field("unknown.field", "value").is_err());
1071 assert!(validate_field("ai", "openai").is_err());
1072 assert!(validate_field("", "value").is_err());
1073 }
1074
1075 #[test]
1078 fn test_get_field_description_all_known_keys() {
1079 let known_keys = [
1080 "ai.provider",
1081 "ai.model",
1082 "ai.api_key",
1083 "ai.base_url",
1084 "ai.temperature",
1085 "ai.max_tokens",
1086 "ai.max_sample_length",
1087 "ai.retry_attempts",
1088 "ai.retry_delay_ms",
1089 "ai.request_timeout_seconds",
1090 "ai.api_version",
1091 "sync.default_method",
1092 "sync.max_offset_seconds",
1093 "sync.vad.enabled",
1094 "sync.vad.sensitivity",
1095 "sync.vad.chunk_size",
1096 "sync.vad.sample_rate",
1097 "sync.vad.padding_chunks",
1098 "sync.vad.min_speech_duration_ms",
1099 "formats.default_output",
1100 "formats.preserve_styling",
1101 "formats.default_encoding",
1102 "formats.encoding_detection_confidence",
1103 "general.backup_enabled",
1104 "general.max_concurrent_jobs",
1105 "general.task_timeout_seconds",
1106 "general.enable_progress_bar",
1107 "general.worker_idle_timeout_seconds",
1108 "general.max_subtitle_bytes",
1109 "general.max_audio_bytes",
1110 "parallel.max_workers",
1111 "parallel.task_queue_size",
1112 "parallel.enable_task_priorities",
1113 "parallel.auto_balance_workers",
1114 "parallel.overflow_strategy",
1115 ];
1116 for key in &known_keys {
1117 let desc = get_field_description(key);
1118 assert!(!desc.is_empty(), "empty description for {key}");
1119 assert_ne!(desc, "Configuration field", "generic fallback for {key}");
1120 }
1121 }
1122
1123 #[test]
1124 fn test_get_field_description_unknown_key_returns_fallback() {
1125 assert_eq!(
1126 get_field_description("unknown.field"),
1127 "Configuration field"
1128 );
1129 assert_eq!(get_field_description(""), "Configuration field");
1130 }
1131
1132 #[test]
1135 fn validate_all_fields_accepts_default_config() {
1136 let cfg = crate::config::Config::default();
1137 assert!(validate_all_fields(&cfg).is_ok());
1138 }
1139
1140 #[test]
1141 fn validate_all_fields_rejects_out_of_range_max_sample_length() {
1142 let mut cfg = crate::config::Config::default();
1143 cfg.ai.max_sample_length = 10;
1144 assert!(validate_all_fields(&cfg).is_err());
1145 }
1146
1147 #[test]
1148 fn validate_all_fields_rejects_out_of_range_retry_delay_ms() {
1149 let mut cfg = crate::config::Config::default();
1150 cfg.ai.retry_delay_ms = 50;
1151 assert!(validate_all_fields(&cfg).is_err());
1152 }
1153
1154 #[test]
1155 fn validate_all_fields_rejects_out_of_range_max_subtitle_bytes() {
1156 let mut cfg = crate::config::Config::default();
1157 cfg.general.max_subtitle_bytes = 10;
1158 assert!(validate_all_fields(&cfg).is_err());
1159 }
1160
1161 #[test]
1162 fn validate_all_fields_rejects_oversized_task_queue_size() {
1163 let mut cfg = crate::config::Config::default();
1164 cfg.parallel.task_queue_size = 999_999;
1165 assert!(validate_all_fields(&cfg).is_err());
1166 }
1167}