Skip to main content

subx_cli/core/
factory.rs

1//! Component factory for creating configured instances of core components.
2//!
3//! This module provides a centralized factory for creating instances of core
4//! components with proper configuration injection, eliminating the need for
5//! global configuration access within individual components.
6
7use crate::services::ai::openai::OpenAIClient;
8use crate::services::ai::openrouter::OpenRouterClient;
9use crate::services::vad::{LocalVadDetector, VadAudioProcessor, VadSyncDetector};
10use crate::{
11    Result,
12    config::{Config, ConfigService},
13    core::{file_manager::FileManager, matcher::engine::MatchEngine},
14    error::SubXError,
15    services::ai::AIProvider,
16};
17
18/// Component factory for creating configured instances.
19///
20/// This factory provides a centralized way to create core components
21/// with proper configuration injection, ensuring consistent component
22/// initialization across the application.
23///
24/// # Examples
25///
26/// ```rust
27/// use subx_cli::core::ComponentFactory;
28/// use subx_cli::config::ProductionConfigService;
29/// use std::sync::Arc;
30///
31/// # async fn example() -> subx_cli::Result<()> {
32/// let config_service = Arc::new(ProductionConfigService::new()?);
33/// let factory = ComponentFactory::new(config_service.as_ref())?;
34///
35/// // Create components with proper configuration
36/// let match_engine = factory.create_match_engine()?;
37/// let file_manager = factory.create_file_manager();
38/// let ai_provider = factory.create_ai_provider()?;
39/// # Ok(())
40/// # }
41/// ```
42pub struct ComponentFactory {
43    config: Config,
44}
45
46impl ComponentFactory {
47    /// Create a new component factory with the given configuration service.
48    ///
49    /// # Arguments
50    ///
51    /// * `config_service` - Configuration service to load configuration from
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if configuration loading fails.
56    pub fn new(config_service: &dyn ConfigService) -> Result<Self> {
57        let config = config_service.get_config()?;
58        Ok(Self { config })
59    }
60
61    /// Create a match engine with AI configuration.
62    ///
63    /// Returns a properly configured MatchEngine instance using
64    /// the AI configuration section.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if AI provider creation fails.
69    pub fn create_match_engine(&self) -> Result<MatchEngine> {
70        let ai_provider = self.create_ai_provider()?;
71        let match_config = crate::core::matcher::MatchConfig {
72            confidence_threshold: 0.8, // Default value, can be configurable
73            max_sample_length: self.config.ai.max_sample_length,
74            enable_content_analysis: true,
75            backup_enabled: self.config.general.backup_enabled,
76            relocation_mode: crate::core::matcher::engine::FileRelocationMode::None,
77            conflict_resolution: crate::core::matcher::engine::ConflictResolution::AutoRename,
78            ai_model: self.config.ai.model.clone(),
79            max_subtitle_bytes: self.config.general.max_subtitle_bytes,
80        };
81        Ok(MatchEngine::new(ai_provider, match_config))
82    }
83
84    /// Create a file manager with general configuration.
85    ///
86    /// Returns a properly configured FileManager instance using
87    /// the general configuration section.
88    pub fn create_file_manager(&self) -> FileManager {
89        // For now, FileManager doesn't take configuration in its constructor
90        // This will be updated when FileManager is refactored to accept config
91        FileManager::new()
92    }
93
94    /// Create an AI provider with AI configuration.
95    ///
96    /// Returns a properly configured AI provider instance based on
97    /// the provider type specified in the AI configuration.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if the provider type is unsupported or
102    /// provider creation fails.
103    pub fn create_ai_provider(&self) -> Result<Box<dyn AIProvider>> {
104        create_ai_provider(&self.config.ai)
105    }
106
107    /// Get a reference to the current configuration.
108    ///
109    /// Returns a reference to the configuration used by this factory.
110    pub fn config(&self) -> &Config {
111        &self.config
112    }
113
114    /// Create a VAD sync detector with VAD configuration.
115    ///
116    /// Returns a properly configured VadSyncDetector instance using the VAD settings.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if VAD sync detector creation fails.
121    pub fn create_vad_sync_detector(&self) -> Result<VadSyncDetector> {
122        VadSyncDetector::new(self.config.sync.vad.clone())
123    }
124
125    /// Create a local VAD detector for audio processing.
126    ///
127    /// Returns a properly configured LocalVadDetector instance.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if local VAD detector initialization fails.
132    pub fn create_vad_detector(&self) -> Result<LocalVadDetector> {
133        LocalVadDetector::new(self.config.sync.vad.clone())
134    }
135
136    /// Create an audio processor for VAD operations.
137    ///
138    /// Returns a properly configured VadAudioProcessor instance.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if audio processor initialization fails.
143    pub fn create_audio_processor(&self) -> Result<VadAudioProcessor> {
144        VadAudioProcessor::new()
145    }
146}
147
148/// Create an AI provider from AI configuration.
149///
150/// This function creates the appropriate AI provider based on the
151/// provider type specified in the configuration.
152///
153/// # Arguments
154///
155/// * `ai_config` - AI configuration containing provider settings
156///
157/// # Errors
158///
159/// Returns an error if the provider type is unsupported or creation fails.
160/// Validate AI configuration parameters.
161fn validate_ai_config(ai_config: &crate::config::AIConfig) -> Result<()> {
162    if ai_config.api_key.as_deref().unwrap_or("").trim().is_empty() {
163        return Err(SubXError::config(
164            "AI API key is required. Set ai.api_key in configuration or use environment variable."
165                .to_string(),
166        ));
167    }
168    if ai_config.model.trim().is_empty() {
169        return Err(SubXError::config(
170            "AI model is required. Set ai.model in configuration.".to_string(),
171        ));
172    }
173    if ai_config.temperature < 0.0 || ai_config.temperature > 2.0 {
174        return Err(SubXError::config(
175            "AI temperature must be between 0.0 and 2.0.".to_string(),
176        ));
177    }
178    if ai_config.max_tokens == 0 {
179        return Err(SubXError::config(
180            "AI max_tokens must be greater than 0.".to_string(),
181        ));
182    }
183    Ok(())
184}
185
186/// Create an AI provider from AI configuration.
187///
188/// This function creates the appropriate AI provider based on the
189/// provider type specified in the configuration.
190pub fn create_ai_provider(ai_config: &crate::config::AIConfig) -> Result<Box<dyn AIProvider>> {
191    match ai_config.provider.as_str() {
192        "openai" => {
193            validate_ai_config(ai_config)?;
194            let client = OpenAIClient::from_config(ai_config)?;
195            Ok(Box::new(client))
196        }
197        "openrouter" => {
198            validate_ai_config(ai_config)?;
199            let client = OpenRouterClient::from_config(ai_config)?;
200            Ok(Box::new(client))
201        }
202        "azure-openai" => {
203            validate_ai_config(ai_config)?;
204            let client =
205                crate::services::ai::azure_openai::AzureOpenAIClient::from_config(ai_config)?;
206            Ok(Box::new(client))
207        }
208        other => Err(SubXError::config(format!(
209            "Unsupported AI provider: {}. Supported providers: openai, openrouter, anthropic, azure-openai",
210            other
211        ))),
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::config::test_service::TestConfigService;
219
220    #[test]
221    fn test_component_factory_creation() {
222        let config_service = TestConfigService::default();
223        let factory = ComponentFactory::new(&config_service);
224        assert!(factory.is_ok());
225    }
226
227    #[test]
228    fn test_factory_creation() {
229        let config_service = TestConfigService::default();
230        let factory = ComponentFactory::new(&config_service);
231        assert!(factory.is_ok());
232    }
233
234    #[test]
235    fn test_create_file_manager() {
236        let config_service = TestConfigService::default();
237        let factory = ComponentFactory::new(&config_service).unwrap();
238
239        let _file_manager = factory.create_file_manager();
240        // Basic validation that file manager was created
241        // FileManager doesn't expose config yet, so just verify creation succeeds
242    }
243
244    #[test]
245    fn test_unsupported_ai_provider() {
246        let mut config = crate::config::Config::default();
247        config.ai.provider = "unsupported".to_string();
248
249        let result: Result<Box<dyn AIProvider>> = create_ai_provider(&config.ai);
250        assert!(result.is_err());
251
252        match result {
253            Err(e) => {
254                let error_msg = e.to_string();
255                assert!(error_msg.contains("Unsupported AI provider"));
256            }
257            Ok(_) => panic!("Expected error for unsupported provider"),
258        }
259    }
260
261    #[test]
262    fn test_create_vad_sync_detector() {
263        let config_service = TestConfigService::default();
264        let factory = ComponentFactory::new(&config_service).unwrap();
265        let result = factory.create_vad_sync_detector();
266        assert!(result.is_ok());
267    }
268
269    #[test]
270    fn test_create_vad_detector() {
271        let config_service = TestConfigService::default();
272        let factory = ComponentFactory::new(&config_service).unwrap();
273        let result = factory.create_vad_detector();
274        assert!(result.is_ok());
275    }
276
277    #[test]
278    fn test_create_audio_processor() {
279        let config_service = TestConfigService::default();
280        let factory = ComponentFactory::new(&config_service).unwrap();
281        let result = factory.create_audio_processor();
282        assert!(result.is_ok());
283    }
284
285    #[test]
286    fn test_create_ai_provider_openai_success() {
287        let config_service = TestConfigService::default();
288        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
289        let factory = ComponentFactory::new(&config_service).unwrap();
290        let result = factory.create_ai_provider();
291        assert!(result.is_ok());
292    }
293
294    #[test]
295    fn test_create_ai_provider_missing_api_key() {
296        let config_service = TestConfigService::default();
297        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "");
298        let factory = ComponentFactory::new(&config_service).unwrap();
299        let result = factory.create_ai_provider();
300        assert!(result.is_err());
301        let error_msg = result.err().unwrap().to_string();
302        assert!(error_msg.contains("API key is required"));
303    }
304
305    #[test]
306    fn test_create_ai_provider_unsupported_provider() {
307        let config_service = TestConfigService::default();
308        config_service.set_ai_settings_and_key("unsupported-provider", "model", "key");
309        let factory = ComponentFactory::new(&config_service).unwrap();
310        let result = factory.create_ai_provider();
311        assert!(result.is_err());
312        let error_msg = result.err().unwrap().to_string();
313        assert!(error_msg.contains("Unsupported AI provider"));
314    }
315
316    #[test]
317    fn test_create_ai_provider_with_custom_base_url() {
318        let config_service = TestConfigService::default();
319        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
320        config_service.config_mut().ai.base_url = "https://custom-api.com/v1".to_string();
321        let factory = ComponentFactory::new(&config_service).unwrap();
322        let result = factory.create_ai_provider();
323        assert!(result.is_ok());
324    }
325
326    #[test]
327    fn test_create_ai_provider_openrouter_success() {
328        let config_service = TestConfigService::default();
329        config_service.set_ai_settings_and_key(
330            "openrouter",
331            "deepseek/deepseek-r1-0528:free",
332            "test-openrouter-key",
333        );
334        let factory = ComponentFactory::new(&config_service).unwrap();
335        let result = factory.create_ai_provider();
336        assert!(result.is_ok());
337    }
338
339    #[test]
340    fn test_create_ai_provider_azure_openai_success() {
341        let mut config = crate::config::Config::default();
342        config.ai.provider = "azure-openai".to_string();
343        config.ai.api_key = Some("azure-key-123".to_string());
344        config.ai.model = "dep123".to_string();
345        config.ai.api_version = Some("2025-04-01-preview".to_string());
346        config.ai.base_url = "https://example.openai.azure.com".to_string();
347        let result = create_ai_provider(&config.ai);
348        assert!(result.is_ok());
349    }
350}