subx_cli/config/
validator.rs

1//! Configuration validators for unified configuration management.
2
3use crate::config::Config;
4use crate::config::manager::ConfigError;
5
6/// Trait for configuration validation.
7pub trait ConfigValidator: Send + Sync {
8    /// Validate the given configuration.
9    fn validate(&self, config: &Config) -> Result<(), ConfigError>;
10    /// Return the name of the validator.
11    fn validator_name(&self) -> &'static str;
12}
13
14/// Validator for AI-related configuration.
15pub struct AIConfigValidator;
16
17impl ConfigValidator for AIConfigValidator {
18    fn validate(&self, config: &Config) -> Result<(), ConfigError> {
19        // 驗證 provider 是否受支援
20        match config.ai.provider.as_str() {
21            "openai" => {}
22            other => {
23                return Err(ConfigError::InvalidValue(
24                    "ai.provider".to_string(),
25                    format!("不支援的 AI 提供商: {}", other),
26                ));
27            }
28        }
29        if let Some(ref api_key) = config.ai.api_key {
30            if !api_key.starts_with("sk-") {
31                return Err(ConfigError::InvalidValue(
32                    "ai.api_key".to_string(),
33                    "OpenAI API 金鑰必須以 'sk-' 開頭".to_string(),
34                ));
35            }
36        }
37        let valid_models = [
38            "gpt-4",
39            "gpt-4-turbo",
40            "gpt-4o",
41            "gpt-4o-mini",
42            "gpt-3.5-turbo",
43        ];
44        if !valid_models.contains(&config.ai.model.as_str()) {
45            return Err(ConfigError::InvalidValue(
46                "ai.model".to_string(),
47                format!(
48                    "不支援的模型: {},支援的模型: {:?}",
49                    config.ai.model, valid_models
50                ),
51            ));
52        }
53        if config.ai.temperature < 0.0 || config.ai.temperature > 2.0 {
54            return Err(ConfigError::InvalidValue(
55                "ai.temperature".to_string(),
56                "溫度值必須在 0.0 到 2.0 之間".to_string(),
57            ));
58        }
59        if config.ai.retry_attempts > 10 {
60            return Err(ConfigError::InvalidValue(
61                "ai.retry_attempts".to_string(),
62                "重試次數不能超過 10 次".to_string(),
63            ));
64        }
65        // Validate base_url format
66        if let Err(e) = validate_base_url(&config.ai.base_url) {
67            return Err(ConfigError::InvalidValue(
68                "ai.base_url".to_string(),
69                e.to_string(),
70            ));
71        }
72        Ok(())
73    }
74
75    fn validator_name(&self) -> &'static str {
76        "ai_config"
77    }
78}
79
80fn validate_base_url(url: &str) -> Result<(), String> {
81    use url::Url;
82    let parsed = Url::parse(url).map_err(|e| format!("無效的 URL 格式: {}", e))?;
83
84    if !matches!(parsed.scheme(), "http" | "https") {
85        return Err("base URL 必須使用 http 或 https 協定".to_string());
86    }
87
88    if parsed.host().is_none() {
89        return Err("base URL 必須包含有效的主機名稱".to_string());
90    }
91
92    Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::validate_base_url;
98
99    #[test]
100    fn valid_base_url_https() {
101        assert!(validate_base_url("https://api.example.com/v1").is_ok());
102    }
103
104    #[test]
105    fn valid_base_url_http() {
106        assert!(validate_base_url("http://localhost:8000").is_ok());
107    }
108
109    #[test]
110    fn invalid_base_url_scheme() {
111        assert!(validate_base_url("ftp://example.com").is_err());
112    }
113
114    #[test]
115    fn invalid_base_url_no_host() {
116        // Valid scheme but missing authority/host
117        assert!(validate_base_url("http://").is_err());
118    }
119}
120
121/// Validator for synchronization-related configuration.
122pub struct SyncConfigValidator;
123
124impl ConfigValidator for SyncConfigValidator {
125    fn validate(&self, config: &Config) -> Result<(), ConfigError> {
126        if config.sync.max_offset_seconds <= 0.0 || config.sync.max_offset_seconds > 300.0 {
127            return Err(ConfigError::InvalidValue(
128                "sync.max_offset_seconds".to_string(),
129                "最大偏移秒數必須在 0.0 到 300.0 之間".to_string(),
130            ));
131        }
132        if config.sync.correlation_threshold < 0.0 || config.sync.correlation_threshold > 1.0 {
133            return Err(ConfigError::InvalidValue(
134                "sync.correlation_threshold".to_string(),
135                "相關性閾值必須在 0.0 到 1.0 之間".to_string(),
136            ));
137        }
138        Ok(())
139    }
140
141    fn validator_name(&self) -> &'static str {
142        "sync_config"
143    }
144}
145
146/// Validator for subtitle formats configuration.
147pub struct FormatsConfigValidator;
148
149impl ConfigValidator for FormatsConfigValidator {
150    fn validate(&self, config: &Config) -> Result<(), ConfigError> {
151        if config.formats.default_output.trim().is_empty() {
152            return Err(ConfigError::InvalidValue(
153                "formats.default_output".to_string(),
154                "預設輸出格式不能為空".to_string(),
155            ));
156        }
157        if config.formats.default_encoding.trim().is_empty() {
158            return Err(ConfigError::InvalidValue(
159                "formats.default_encoding".to_string(),
160                "預設編碼不能為空".to_string(),
161            ));
162        }
163        if config.formats.encoding_detection_confidence < 0.0
164            || config.formats.encoding_detection_confidence > 1.0
165        {
166            return Err(ConfigError::InvalidValue(
167                "formats.encoding_detection_confidence".to_string(),
168                "編碼檢測信心度必須介於 0.0 和 1.0 之間".to_string(),
169            ));
170        }
171        Ok(())
172    }
173
174    fn validator_name(&self) -> &'static str {
175        "formats_config"
176    }
177}
178
179/// Validator for general application configuration.
180pub struct GeneralConfigValidator;
181
182impl ConfigValidator for GeneralConfigValidator {
183    fn validate(&self, config: &Config) -> Result<(), ConfigError> {
184        if config.general.max_concurrent_jobs == 0 {
185            return Err(ConfigError::InvalidValue(
186                "general.max_concurrent_jobs".to_string(),
187                "最大併發工作數必須大於 0".to_string(),
188            ));
189        }
190        Ok(())
191    }
192
193    fn validator_name(&self) -> &'static str {
194        "general_config"
195    }
196}