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        };
80        Ok(MatchEngine::new(ai_provider, match_config))
81    }
82
83    /// Create a file manager with general configuration.
84    ///
85    /// Returns a properly configured FileManager instance using
86    /// the general configuration section.
87    pub fn create_file_manager(&self) -> FileManager {
88        // For now, FileManager doesn't take configuration in its constructor
89        // This will be updated when FileManager is refactored to accept config
90        FileManager::new()
91    }
92
93    /// Create an AI provider with AI configuration.
94    ///
95    /// Returns a properly configured AI provider instance based on
96    /// the provider type specified in the AI configuration.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the provider type is unsupported or
101    /// provider creation fails.
102    pub fn create_ai_provider(&self) -> Result<Box<dyn AIProvider>> {
103        create_ai_provider(&self.config.ai)
104    }
105
106    /// Get a reference to the current configuration.
107    ///
108    /// Returns a reference to the configuration used by this factory.
109    pub fn config(&self) -> &Config {
110        &self.config
111    }
112
113    /// Create a VAD sync detector with VAD configuration.
114    ///
115    /// Returns a properly configured VadSyncDetector instance using the VAD settings.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if VAD sync detector creation fails.
120    pub fn create_vad_sync_detector(&self) -> Result<VadSyncDetector> {
121        VadSyncDetector::new(self.config.sync.vad.clone())
122    }
123
124    /// Create a local VAD detector for audio processing.
125    ///
126    /// Returns a properly configured LocalVadDetector instance.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if local VAD detector initialization fails.
131    pub fn create_vad_detector(&self) -> Result<LocalVadDetector> {
132        LocalVadDetector::new(self.config.sync.vad.clone())
133    }
134
135    /// Create an audio processor for VAD operations.
136    ///
137    /// Returns a properly configured VadAudioProcessor instance.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if audio processor initialization fails.
142    pub fn create_audio_processor(&self) -> Result<VadAudioProcessor> {
143        VadAudioProcessor::new()
144    }
145}
146
147/// Create an AI provider from AI configuration.
148///
149/// This function creates the appropriate AI provider based on the
150/// provider type specified in the configuration.
151///
152/// # Arguments
153///
154/// * `ai_config` - AI configuration containing provider settings
155///
156/// # Errors
157///
158/// Returns an error if the provider type is unsupported or creation fails.
159/// Validate AI configuration parameters.
160fn validate_ai_config(ai_config: &crate::config::AIConfig) -> Result<()> {
161    if ai_config.api_key.as_deref().unwrap_or("").trim().is_empty() {
162        return Err(SubXError::config(
163            "AI API key is required. Set ai.api_key in configuration or use environment variable."
164                .to_string(),
165        ));
166    }
167    if ai_config.model.trim().is_empty() {
168        return Err(SubXError::config(
169            "AI model is required. Set ai.model in configuration.".to_string(),
170        ));
171    }
172    if ai_config.temperature < 0.0 || ai_config.temperature > 2.0 {
173        return Err(SubXError::config(
174            "AI temperature must be between 0.0 and 2.0.".to_string(),
175        ));
176    }
177    if ai_config.max_tokens == 0 {
178        return Err(SubXError::config(
179            "AI max_tokens must be greater than 0.".to_string(),
180        ));
181    }
182    Ok(())
183}
184
185/// Create an AI provider from AI configuration.
186///
187/// This function creates the appropriate AI provider based on the
188/// provider type specified in the configuration.
189pub fn create_ai_provider(ai_config: &crate::config::AIConfig) -> Result<Box<dyn AIProvider>> {
190    match ai_config.provider.as_str() {
191        "openai" => {
192            validate_ai_config(ai_config)?;
193            let client = OpenAIClient::from_config(ai_config)?;
194            Ok(Box::new(client))
195        }
196        "openrouter" => {
197            validate_ai_config(ai_config)?;
198            let client = OpenRouterClient::from_config(ai_config)?;
199            Ok(Box::new(client))
200        }
201        "azure-openai" => {
202            validate_ai_config(ai_config)?;
203            let client =
204                crate::services::ai::azure_openai::AzureOpenAIClient::from_config(ai_config)?;
205            Ok(Box::new(client))
206        }
207        other => Err(SubXError::config(format!(
208            "Unsupported AI provider: {}. Supported providers: openai, openrouter, anthropic, azure-openai",
209            other
210        ))),
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::config::test_service::TestConfigService;
218
219    #[test]
220    fn test_component_factory_creation() {
221        let config_service = TestConfigService::default();
222        let factory = ComponentFactory::new(&config_service);
223        assert!(factory.is_ok());
224    }
225
226    #[test]
227    fn test_factory_creation() {
228        let config_service = TestConfigService::default();
229        let factory = ComponentFactory::new(&config_service);
230        assert!(factory.is_ok());
231    }
232
233    #[test]
234    fn test_create_file_manager() {
235        let config_service = TestConfigService::default();
236        let factory = ComponentFactory::new(&config_service).unwrap();
237
238        let _file_manager = factory.create_file_manager();
239        // Basic validation that file manager was created
240        // FileManager doesn't expose config yet, so just verify creation succeeds
241    }
242
243    #[test]
244    fn test_unsupported_ai_provider() {
245        let mut config = crate::config::Config::default();
246        config.ai.provider = "unsupported".to_string();
247
248        let result: Result<Box<dyn AIProvider>> = create_ai_provider(&config.ai);
249        assert!(result.is_err());
250
251        match result {
252            Err(e) => {
253                let error_msg = e.to_string();
254                assert!(error_msg.contains("Unsupported AI provider"));
255            }
256            Ok(_) => panic!("Expected error for unsupported provider"),
257        }
258    }
259
260    #[test]
261    fn test_create_vad_sync_detector() {
262        let config_service = TestConfigService::default();
263        let factory = ComponentFactory::new(&config_service).unwrap();
264        let result = factory.create_vad_sync_detector();
265        assert!(result.is_ok());
266    }
267
268    #[test]
269    fn test_create_vad_detector() {
270        let config_service = TestConfigService::default();
271        let factory = ComponentFactory::new(&config_service).unwrap();
272        let result = factory.create_vad_detector();
273        assert!(result.is_ok());
274    }
275
276    #[test]
277    fn test_create_audio_processor() {
278        let config_service = TestConfigService::default();
279        let factory = ComponentFactory::new(&config_service).unwrap();
280        let result = factory.create_audio_processor();
281        assert!(result.is_ok());
282    }
283
284    #[test]
285    fn test_create_ai_provider_openai_success() {
286        let config_service = TestConfigService::default();
287        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
288        let factory = ComponentFactory::new(&config_service).unwrap();
289        let result = factory.create_ai_provider();
290        assert!(result.is_ok());
291    }
292
293    #[test]
294    fn test_create_ai_provider_missing_api_key() {
295        let config_service = TestConfigService::default();
296        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "");
297        let factory = ComponentFactory::new(&config_service).unwrap();
298        let result = factory.create_ai_provider();
299        assert!(result.is_err());
300        let error_msg = result.err().unwrap().to_string();
301        assert!(error_msg.contains("API key is required"));
302    }
303
304    #[test]
305    fn test_create_ai_provider_unsupported_provider() {
306        let config_service = TestConfigService::default();
307        config_service.set_ai_settings_and_key("unsupported-provider", "model", "key");
308        let factory = ComponentFactory::new(&config_service).unwrap();
309        let result = factory.create_ai_provider();
310        assert!(result.is_err());
311        let error_msg = result.err().unwrap().to_string();
312        assert!(error_msg.contains("Unsupported AI provider"));
313    }
314
315    #[test]
316    fn test_create_ai_provider_with_custom_base_url() {
317        let config_service = TestConfigService::default();
318        config_service.set_ai_settings_and_key("openai", "gpt-4.1-mini", "test-api-key");
319        config_service.config_mut().ai.base_url = "https://custom-api.com/v1".to_string();
320        let factory = ComponentFactory::new(&config_service).unwrap();
321        let result = factory.create_ai_provider();
322        assert!(result.is_ok());
323    }
324
325    #[test]
326    fn test_create_ai_provider_openrouter_success() {
327        let config_service = TestConfigService::default();
328        config_service.set_ai_settings_and_key(
329            "openrouter",
330            "deepseek/deepseek-r1-0528:free",
331            "test-openrouter-key",
332        );
333        let factory = ComponentFactory::new(&config_service).unwrap();
334        let result = factory.create_ai_provider();
335        assert!(result.is_ok());
336    }
337
338    #[test]
339    fn test_create_ai_provider_azure_openai_success() {
340        let mut config = crate::config::Config::default();
341        config.ai.provider = "azure-openai".to_string();
342        config.ai.api_key = Some("azure-key-123".to_string());
343        config.ai.model = "dep123".to_string();
344        config.ai.api_version = Some("2025-04-01-preview".to_string());
345        config.ai.base_url = "https://example.openai.azure.com".to_string();
346        let result = create_ai_provider(&config.ai);
347        assert!(result.is_ok());
348    }
349}