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