use super::validation::*;
use crate::Result;
use crate::config::Config;
use crate::config::{
AIConfig, FormatsConfig, GeneralConfig, ParallelConfig, SyncConfig, VadConfig,
};
use crate::error::SubXError;
pub fn validate_config(config: &Config) -> Result<()> {
validate_ai_config(&config.ai)?;
validate_sync_config(&config.sync)?;
validate_general_config(&config.general)?;
validate_formats_config(&config.formats)?;
validate_parallel_config(&config.parallel)?;
validate_config_consistency(config)?;
Ok(())
}
pub fn validate_ai_config(ai_config: &AIConfig) -> Result<()> {
validate_non_empty_string(&ai_config.provider, "AI provider")?;
match ai_config.provider.as_str() {
"openai" => {
if let Some(api_key) = &ai_config.api_key {
if !api_key.is_empty() {
validate_api_key(api_key)?;
if !api_key.starts_with("sk-") {
return Err(SubXError::config("OpenAI API key must start with 'sk-'"));
}
}
}
validate_ai_model(&ai_config.model)?;
validate_temperature(ai_config.temperature)?;
validate_positive_number(ai_config.max_tokens as f64)?;
if !ai_config.base_url.is_empty() {
validate_url_format(&ai_config.base_url)?;
}
}
"openrouter" => {
if let Some(api_key) = &ai_config.api_key {
if !api_key.is_empty() {
validate_api_key(api_key)?;
}
}
validate_ai_model(&ai_config.model)?;
validate_temperature(ai_config.temperature)?;
validate_positive_number(ai_config.max_tokens as f64)?;
if !ai_config.base_url.is_empty() {
validate_url_format(&ai_config.base_url)?;
}
}
"anthropic" => {
if let Some(api_key) = &ai_config.api_key {
if !api_key.is_empty() {
validate_api_key(api_key)?;
}
}
validate_ai_model(&ai_config.model)?;
validate_temperature(ai_config.temperature)?;
}
"azure-openai" => {
if let Some(api_key) = &ai_config.api_key {
if !api_key.is_empty() {
validate_api_key(api_key)?;
}
}
validate_ai_model(&ai_config.model)?;
validate_temperature(ai_config.temperature)?;
validate_positive_number(ai_config.max_tokens as f64)?;
if let Some(ver) = &ai_config.api_version {
if ver.trim().is_empty() {
return Err(SubXError::config(
"Azure OpenAI api_version must not be empty",
));
}
}
if !ai_config.base_url.is_empty() {
validate_url_format(&ai_config.base_url)?;
}
}
_ => {
return Err(SubXError::config(format!(
"Unsupported AI provider: {}. Supported providers: openai, openrouter, anthropic, azure-openai",
ai_config.provider
)));
}
}
validate_positive_number(ai_config.retry_attempts as f64)?;
if ai_config.retry_attempts > 10 {
return Err(SubXError::config("Retry count cannot exceed 10 times"));
}
validate_range(ai_config.request_timeout_seconds as f64, 10.0, 600.0)
.map_err(|_| SubXError::config("Request timeout must be between 10 and 600 seconds"))?;
Ok(())
}
pub fn validate_sync_config(sync_config: &SyncConfig) -> Result<()> {
sync_config.validate()
}
pub fn validate_general_config(general_config: &GeneralConfig) -> Result<()> {
validate_positive_number(general_config.max_concurrent_jobs as f64)?;
if general_config.max_concurrent_jobs > 64 {
return Err(SubXError::config(
"Maximum concurrent jobs should not exceed 64",
));
}
validate_range(general_config.task_timeout_seconds as f64, 30.0, 3600.0)
.map_err(|_| SubXError::config("Task timeout must be between 30 and 3600 seconds"))?;
validate_range(
general_config.worker_idle_timeout_seconds as f64,
10.0,
3600.0,
)
.map_err(|_| SubXError::config("Worker idle timeout must be between 10 and 3600 seconds"))?;
Ok(())
}
pub fn validate_formats_config(formats_config: &FormatsConfig) -> Result<()> {
validate_non_empty_string(&formats_config.default_output, "Default output format")?;
validate_enum(
&formats_config.default_output,
&["srt", "ass", "vtt", "webvtt"],
)?;
validate_non_empty_string(&formats_config.default_encoding, "Default encoding")?;
validate_enum(
&formats_config.default_encoding,
&["utf-8", "gbk", "big5", "shift_jis"],
)?;
validate_range(formats_config.encoding_detection_confidence, 0.0, 1.0).map_err(|_| {
SubXError::config("Encoding detection confidence must be between 0.0 and 1.0")
})?;
Ok(())
}
pub fn validate_parallel_config(parallel_config: &ParallelConfig) -> Result<()> {
validate_positive_number(parallel_config.max_workers as f64)?;
if parallel_config.max_workers > 64 {
return Err(SubXError::config("Maximum workers should not exceed 64"));
}
validate_positive_number(parallel_config.task_queue_size as f64)?;
if parallel_config.task_queue_size < 100 {
return Err(SubXError::config("Task queue size should be at least 100"));
}
Ok(())
}
fn validate_config_consistency(config: &Config) -> Result<()> {
if config.ai.provider == "openai" {
if let Some(api_key) = &config.ai.api_key {
if api_key.is_empty() {
return Err(SubXError::config(
"OpenAI provider is selected but API key is empty",
));
}
}
}
if config.parallel.max_workers > config.general.max_concurrent_jobs {
log::warn!(
"Parallel max_workers ({}) exceeds general max_concurrent_jobs ({})",
config.parallel.max_workers,
config.general.max_concurrent_jobs
);
}
Ok(())
}
impl SyncConfig {
pub fn validate(&self) -> Result<()> {
validate_enum(&self.default_method, &["vad", "auto", "manual"])?;
validate_positive_number(self.max_offset_seconds)?;
if self.max_offset_seconds > 3600.0 {
return Err(SubXError::config(
"sync.max_offset_seconds should not exceed 3600 seconds (1 hour). If a larger value is needed, please verify the sync requirements are reasonable.",
));
}
if self.max_offset_seconds < 5.0 {
log::warn!(
"sync.max_offset_seconds is set to {:.1}s which may be too small. Consider using 30.0-60.0 seconds.",
self.max_offset_seconds
);
} else if self.max_offset_seconds > 600.0 && self.max_offset_seconds <= 3600.0 {
log::warn!(
"sync.max_offset_seconds is set to {:.1}s which is quite large. Please confirm this meets your requirements.",
self.max_offset_seconds
);
}
self.vad.validate()?;
Ok(())
}
}
impl VadConfig {
pub fn validate(&self) -> Result<()> {
if !(0.0..=1.0).contains(&self.sensitivity) {
return Err(SubXError::config(
"VAD sensitivity must be between 0.0 and 1.0",
));
}
if self.padding_chunks > 10 {
return Err(SubXError::config("VAD padding_chunks must not exceed 10"));
}
if self.min_speech_duration_ms > 5000 {
return Err(SubXError::config(
"VAD min_speech_duration_ms must not exceed 5000ms",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{AIConfig, Config, SyncConfig, VadConfig};
#[test]
fn test_validate_default_config() {
let config = Config::default();
assert!(validate_config(&config).is_ok());
}
#[test]
fn test_validate_ai_config_valid() {
let ai_config = AIConfig {
provider: "openai".to_string(),
api_key: Some("sk-test123456789".to_string()),
temperature: 0.8,
..Default::default()
};
assert!(validate_ai_config(&ai_config).is_ok());
let ai_config = AIConfig {
provider: "openrouter".to_string(),
api_key: Some("test-openrouter-key".to_string()),
model: "deepseek/deepseek-r1-0528:free".to_string(),
..Default::default()
};
assert!(validate_ai_config(&ai_config).is_ok());
let ai_config = AIConfig {
provider: "azure-openai".to_string(),
api_key: Some("azure-key-123".to_string()),
model: "dep123".to_string(),
api_version: Some("2025-04-01-preview".to_string()),
..Default::default()
};
assert!(validate_ai_config(&ai_config).is_ok());
}
#[test]
fn test_validate_ai_config_invalid_provider() {
let ai_config = AIConfig {
provider: "invalid".to_string(),
..Default::default()
};
let err = validate_ai_config(&ai_config).unwrap_err();
assert!(err.to_string().contains(
"Unsupported AI provider: invalid. Supported providers: openai, openrouter, anthropic, azure-openai"
));
}
#[test]
fn test_validate_ai_config_invalid_temperature() {
let ai_config = AIConfig {
provider: "openai".to_string(),
temperature: 3.0, ..Default::default()
};
assert!(validate_ai_config(&ai_config).is_err());
}
#[test]
fn test_validate_ai_config_invalid_openai_key() {
let ai_config = AIConfig {
provider: "openai".to_string(),
api_key: Some("invalid-key".to_string()),
..Default::default()
};
assert!(validate_ai_config(&ai_config).is_err());
}
#[test]
fn test_validate_sync_config_valid() {
let sync_config = SyncConfig::default();
assert!(validate_sync_config(&sync_config).is_ok());
}
#[test]
fn test_validate_vad_config_invalid_sensitivity() {
let vad_config = VadConfig {
sensitivity: 1.5, ..Default::default()
};
assert!(vad_config.validate().is_err());
}
#[test]
fn test_validate_config_consistency() {
let mut config = Config::default();
config.ai.provider = "openai".to_string();
config.ai.api_key = Some("".to_string()); assert!(validate_config(&config).is_err());
config.ai.api_key = Some("sk-valid123".to_string());
assert!(validate_config(&config).is_ok());
config.ai.api_key = None;
assert!(validate_config(&config).is_ok());
}
}