1use super::validation::*;
14use crate::{Result, error::SubXError};
15
16pub fn validate_field(key: &str, value: &str) -> Result<()> {
28 match key {
29 "ai.provider" => {
31 validate_non_empty_string(value, "AI provider")?;
32 validate_enum(
33 value,
34 &["openai", "anthropic", "local", "openrouter", "azure-openai"],
35 )?;
36 }
37 "ai.model" => validate_ai_model(value)?,
38 "ai.api_key" => {
39 if !value.is_empty() {
40 validate_api_key(value)?;
41 }
42 }
43 "ai.base_url" => validate_url_format(value)?,
44 "ai.temperature" => {
45 let temp: f32 = value
46 .parse()
47 .map_err(|_| SubXError::config("Temperature must be a number"))?;
48 validate_temperature(temp)?;
49 }
50 "ai.max_tokens" => {
51 let tokens: u32 = value
52 .parse()
53 .map_err(|_| SubXError::config("Max tokens must be a positive integer"))?;
54 validate_positive_number(tokens as f64)?;
55 }
56 "ai.max_sample_length" => {
57 let length: usize = value
58 .parse()
59 .map_err(|_| SubXError::config("Max sample length must be a positive integer"))?;
60 validate_range(length, 100, 10000)?;
61 }
62 "ai.retry_attempts" => {
63 let attempts: u32 = value
64 .parse()
65 .map_err(|_| SubXError::config("Retry attempts must be a positive integer"))?;
66 validate_range(attempts, 1, 10)?;
67 }
68 "ai.retry_delay_ms" => {
69 let delay: u64 = value
70 .parse()
71 .map_err(|_| SubXError::config("Retry delay must be a positive integer"))?;
72 validate_range(delay, 100, 30000)?;
73 }
74 "ai.request_timeout_seconds" => {
75 let timeout: u64 = value
76 .parse()
77 .map_err(|_| SubXError::config("Request timeout must be a positive integer"))?;
78 validate_range(timeout, 10, 600)?;
79 }
80
81 "ai.api_version" => {
83 validate_non_empty_string(value, "Azure OpenAI API version")?;
84 }
85
86 "sync.default_method" => {
88 validate_enum(value, &["auto", "vad", "manual"])?;
89 }
90 "sync.max_offset_seconds" => {
91 let offset: f32 = value
92 .parse()
93 .map_err(|_| SubXError::config("Max offset must be a number"))?;
94 validate_range(offset, 0.1, 3600.0)?;
95 }
96 "sync.vad.enabled" => {
97 parse_bool(value)?;
98 }
99 "sync.vad.sensitivity" => {
100 let sensitivity: f32 = value
101 .parse()
102 .map_err(|_| SubXError::config("VAD sensitivity must be a number"))?;
103 validate_range(sensitivity, 0.0, 1.0)?;
104 }
105 "sync.vad.padding_chunks" => {
106 let chunks: u32 = value
107 .parse()
108 .map_err(|_| SubXError::config("Padding chunks must be a non-negative integer"))?;
109 validate_range(chunks, 0, 10)?;
110 }
111 "sync.vad.min_speech_duration_ms" => {
112 let _duration: u32 = value.parse().map_err(|_| {
113 SubXError::config("Min speech duration must be a non-negative integer")
114 })?;
115 }
117
118 "formats.default_output" => {
120 validate_enum(value, &["srt", "ass", "vtt", "webvtt"])?;
121 }
122 "formats.preserve_styling" => {
123 parse_bool(value)?;
124 }
125 "formats.default_encoding" => {
126 validate_enum(value, &["utf-8", "gbk", "big5", "shift_jis"])?;
127 }
128 "formats.encoding_detection_confidence" => {
129 let confidence: f32 = value
130 .parse()
131 .map_err(|_| SubXError::config("Encoding detection confidence must be a number"))?;
132 validate_range(confidence, 0.0, 1.0)?;
133 }
134
135 "general.backup_enabled" => {
137 parse_bool(value)?;
138 }
139 "general.max_concurrent_jobs" => {
140 let jobs: usize = value
141 .parse()
142 .map_err(|_| SubXError::config("Max concurrent jobs must be a positive integer"))?;
143 validate_range(jobs, 1, 64)?;
144 }
145 "general.task_timeout_seconds" => {
146 let timeout: u64 = value
147 .parse()
148 .map_err(|_| SubXError::config("Task timeout must be a positive integer"))?;
149 validate_range(timeout, 30, 3600)?;
150 }
151 "general.enable_progress_bar" => {
152 parse_bool(value)?;
153 }
154 "general.worker_idle_timeout_seconds" => {
155 let timeout: u64 = value
156 .parse()
157 .map_err(|_| SubXError::config("Worker idle timeout must be a positive integer"))?;
158 validate_range(timeout, 10, 3600)?;
159 }
160
161 "parallel.max_workers" => {
163 let workers: usize = value
164 .parse()
165 .map_err(|_| SubXError::config("Max workers must be a positive integer"))?;
166 validate_range(workers, 1, 64)?;
167 }
168 "parallel.task_queue_size" => {
169 let size: usize = value
170 .parse()
171 .map_err(|_| SubXError::config("Task queue size must be a positive integer"))?;
172 validate_range(size, 100, 10000)?;
173 }
174 "parallel.enable_task_priorities" => {
175 parse_bool(value)?;
176 }
177 "parallel.auto_balance_workers" => {
178 parse_bool(value)?;
179 }
180 "parallel.overflow_strategy" => {
181 validate_enum(value, &["Block", "Drop", "Expand"])?;
182 }
183
184 _ => {
185 return Err(SubXError::config(format!(
186 "Unknown configuration key: {key}"
187 )));
188 }
189 }
190
191 Ok(())
192}
193
194pub fn get_field_description(key: &str) -> &'static str {
196 match key {
197 "ai.provider" => "AI service provider (e.g., 'openai', 'azure-openai')",
198 "ai.model" => "AI model name (e.g., 'gpt-4.1-mini')",
199 "ai.api_key" => "API key for the AI service",
200 "ai.base_url" => "Custom API endpoint URL (optional)",
201 "ai.temperature" => "AI response randomness (0.0-2.0)",
202 "ai.max_tokens" => "Maximum tokens in AI response",
203 "ai.max_sample_length" => "Maximum sample length for AI processing",
204 "ai.retry_attempts" => "Number of retry attempts for AI requests",
205 "ai.retry_delay_ms" => "Delay between retry attempts in milliseconds",
206 "ai.request_timeout_seconds" => "Request timeout in seconds",
207 "ai.api_version" => "Azure OpenAI API version (optional, defaults to latest)",
208
209 "sync.default_method" => "Synchronization method ('auto', 'vad', or 'manual')",
210 "sync.max_offset_seconds" => "Maximum allowed time offset in seconds",
211 "sync.vad.enabled" => "Enable voice activity detection",
212 "sync.vad.sensitivity" => "Voice activity detection threshold (0.0-1.0)",
213 "sync.vad.chunk_size" => "VAD processing chunk size (must be power of 2)",
214 "sync.vad.sample_rate" => "Audio sample rate for VAD processing",
215 "sync.vad.padding_chunks" => "Number of padding chunks for VAD",
216 "sync.vad.min_speech_duration_ms" => "Minimum speech duration in milliseconds",
217
218 "formats.default_output" => "Default output format for subtitles",
219 "formats.preserve_styling" => "Preserve subtitle styling information",
220 "formats.default_encoding" => "Default character encoding",
221 "formats.encoding_detection_confidence" => "Confidence threshold for encoding detection",
222
223 "general.backup_enabled" => "Enable automatic backup creation",
224 "general.max_concurrent_jobs" => "Maximum number of concurrent jobs",
225 "general.task_timeout_seconds" => "Task timeout in seconds",
226 "general.enable_progress_bar" => "Enable progress bar display",
227 "general.worker_idle_timeout_seconds" => "Worker idle timeout in seconds",
228
229 "parallel.max_workers" => "Maximum number of worker threads",
230 "parallel.task_queue_size" => "Size of the task queue",
231 "parallel.enable_task_priorities" => "Enable task priority system",
232 "parallel.auto_balance_workers" => "Enable automatic worker load balancing",
233 "parallel.overflow_strategy" => "Strategy for handling queue overflow",
234
235 _ => "Configuration field",
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_validate_ai_fields() {
245 assert!(validate_field("ai.provider", "openai").is_ok());
247 assert!(validate_field("ai.provider", "openrouter").is_ok());
248 assert!(validate_field("ai.provider", "azure-openai").is_ok());
249 assert!(validate_field("ai.temperature", "0.8").is_ok());
250 assert!(validate_field("ai.max_tokens", "4000").is_ok());
251
252 assert!(validate_field("ai.provider", "invalid").is_err());
254 assert!(validate_field("ai.temperature", "3.0").is_err());
255 assert!(validate_field("ai.max_tokens", "0").is_err());
256 }
257
258 #[test]
259 fn test_validate_sync_fields() {
260 assert!(validate_field("sync.default_method", "vad").is_ok());
262 assert!(validate_field("sync.vad.sensitivity", "0.5").is_ok());
263 assert!(validate_field("sync.vad.padding_chunks", "3").is_ok());
264
265 assert!(validate_field("sync.default_method", "invalid").is_err());
267 assert!(validate_field("sync.vad.sensitivity", "1.5").is_err());
268 assert!(validate_field("sync.vad.padding_chunks", "11").is_err()); }
270
271 #[test]
272 fn test_validate_formats_fields() {
273 assert!(validate_field("formats.default_output", "srt").is_ok());
275 assert!(validate_field("formats.preserve_styling", "true").is_ok());
276
277 assert!(validate_field("formats.default_output", "invalid").is_err());
279 assert!(validate_field("formats.preserve_styling", "maybe").is_err());
280 }
281
282 #[test]
283 fn test_validate_unknown_field() {
284 assert!(validate_field("unknown.field", "value").is_err());
285 }
286
287 #[test]
288 fn test_get_field_description() {
289 assert!(!get_field_description("ai.provider").is_empty());
290 assert!(!get_field_description("sync.vad.sensitivity").is_empty());
291 assert_eq!(
292 get_field_description("unknown.field"),
293 "Configuration field"
294 );
295 }
296}