Skip to main content

gglib_core/services/
app_core.rs

1//! `AppCore` - the primary application facade.
2//!
3//! This is the composition root for core services. Adapters (CLI, GUI, Web)
4//! receive an `AppCore` instance and use it to access all functionality.
5
6use crate::ports::Repos;
7use std::sync::Arc;
8
9use super::{ChatHistoryService, ModelService, ModelVerificationService, SettingsService};
10
11/// The core application facade.
12///
13/// `AppCore` provides access to all core services. It's constructed at the
14/// adapter's composition root (main.rs or bootstrap.rs) with concrete
15/// implementations of repositories.
16///
17/// # Example
18///
19/// ```ignore
20/// let repos = Repos { models: model_repo, settings: settings_repo };
21/// let core = AppCore::new(repos);
22///
23/// // Access services
24/// let models = core.models().list().await?;
25/// ```
26pub struct AppCore {
27    models: ModelService,
28    settings: SettingsService,
29    chat_history: ChatHistoryService,
30    verification: Option<Arc<ModelVerificationService>>,
31}
32
33impl AppCore {
34    /// Create a new `AppCore` with the given repositories.
35    pub fn new(repos: Repos) -> Self {
36        Self {
37            models: ModelService::new(repos.models),
38            settings: SettingsService::new(repos.settings),
39            chat_history: ChatHistoryService::new(repos.chat_history),
40            verification: None,
41        }
42    }
43
44    /// Set the verification service (optional).
45    ///
46    /// This should be called during bootstrap if verification features are needed.
47    #[must_use]
48    pub fn with_verification(mut self, verification: Arc<ModelVerificationService>) -> Self {
49        self.verification = Some(verification);
50        self
51    }
52
53    /// Access the model service.
54    pub const fn models(&self) -> &ModelService {
55        &self.models
56    }
57
58    /// Access the settings service.
59    pub const fn settings(&self) -> &SettingsService {
60        &self.settings
61    }
62
63    /// Access the chat history service.
64    pub const fn chat_history(&self) -> &ChatHistoryService {
65        &self.chat_history
66    }
67
68    /// Access the verification service (if available).
69    pub fn verification(&self) -> Option<&ModelVerificationService> {
70        self.verification.as_deref()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::domain::chat::{
78        Conversation, ConversationUpdate, Message, NewConversation, NewMessage,
79    };
80    use crate::domain::mcp::{McpServer, NewMcpServer};
81    use crate::domain::{Model, NewModel};
82    use crate::ports::{
83        ChatHistoryError, ChatHistoryRepository, McpRepositoryError, McpServerRepository,
84        ModelRepository, RepositoryError, SettingsRepository,
85    };
86    use crate::settings::Settings;
87    use async_trait::async_trait;
88    use std::sync::Mutex;
89
90    struct MockModelRepo;
91
92    #[async_trait]
93    impl ModelRepository for MockModelRepo {
94        async fn list(&self) -> Result<Vec<Model>, RepositoryError> {
95            Ok(vec![])
96        }
97        async fn get_by_id(&self, id: i64) -> Result<Model, RepositoryError> {
98            Err(RepositoryError::NotFound(format!("id={id}")))
99        }
100        async fn get_by_name(&self, name: &str) -> Result<Model, RepositoryError> {
101            Err(RepositoryError::NotFound(format!("name={name}")))
102        }
103        async fn insert(&self, _model: &NewModel) -> Result<Model, RepositoryError> {
104            unimplemented!()
105        }
106        async fn update(&self, _model: &Model) -> Result<(), RepositoryError> {
107            unimplemented!()
108        }
109        async fn delete(&self, _id: i64) -> Result<(), RepositoryError> {
110            Ok(())
111        }
112    }
113
114    struct MockMcpRepo;
115
116    #[async_trait]
117    impl McpServerRepository for MockMcpRepo {
118        async fn insert(&self, _server: NewMcpServer) -> Result<McpServer, McpRepositoryError> {
119            unimplemented!()
120        }
121        async fn get_by_id(&self, id: i64) -> Result<McpServer, McpRepositoryError> {
122            Err(McpRepositoryError::NotFound(format!("id={id}")))
123        }
124        async fn get_by_name(&self, name: &str) -> Result<McpServer, McpRepositoryError> {
125            Err(McpRepositoryError::NotFound(format!("name={name}")))
126        }
127        async fn list(&self) -> Result<Vec<McpServer>, McpRepositoryError> {
128            Ok(vec![])
129        }
130        async fn update(&self, _server: &McpServer) -> Result<(), McpRepositoryError> {
131            unimplemented!()
132        }
133        async fn delete(&self, _id: i64) -> Result<(), McpRepositoryError> {
134            Ok(())
135        }
136        async fn update_last_connected(&self, _id: i64) -> Result<(), McpRepositoryError> {
137            Ok(())
138        }
139    }
140
141    struct MockChatHistoryRepo;
142
143    #[async_trait]
144    impl ChatHistoryRepository for MockChatHistoryRepo {
145        async fn create_conversation(
146            &self,
147            _conv: NewConversation,
148        ) -> Result<i64, ChatHistoryError> {
149            Ok(1)
150        }
151        async fn list_conversations(&self) -> Result<Vec<Conversation>, ChatHistoryError> {
152            Ok(vec![])
153        }
154        async fn get_conversation(
155            &self,
156            _id: i64,
157        ) -> Result<Option<Conversation>, ChatHistoryError> {
158            Ok(None)
159        }
160        async fn update_conversation(
161            &self,
162            _id: i64,
163            _update: ConversationUpdate,
164        ) -> Result<(), ChatHistoryError> {
165            Ok(())
166        }
167        async fn delete_conversation(&self, _id: i64) -> Result<(), ChatHistoryError> {
168            Ok(())
169        }
170        async fn get_conversation_count(&self) -> Result<i64, ChatHistoryError> {
171            Ok(0)
172        }
173        async fn get_messages(
174            &self,
175            _conversation_id: i64,
176        ) -> Result<Vec<Message>, ChatHistoryError> {
177            Ok(vec![])
178        }
179        async fn save_message(&self, _msg: NewMessage) -> Result<i64, ChatHistoryError> {
180            Ok(1)
181        }
182        async fn update_message(
183            &self,
184            _id: i64,
185            _content: String,
186            _metadata: Option<serde_json::Value>,
187        ) -> Result<(), ChatHistoryError> {
188            Ok(())
189        }
190        async fn delete_message_and_subsequent(&self, _id: i64) -> Result<i64, ChatHistoryError> {
191            Ok(0)
192        }
193        async fn get_message_count(&self, _conversation_id: i64) -> Result<i64, ChatHistoryError> {
194            Ok(0)
195        }
196    }
197
198    struct MockSettingsRepo {
199        settings: Mutex<Settings>,
200    }
201
202    impl MockSettingsRepo {
203        fn new() -> Self {
204            Self {
205                settings: Mutex::new(Settings::with_defaults()),
206            }
207        }
208    }
209
210    #[async_trait]
211    impl SettingsRepository for MockSettingsRepo {
212        async fn load(&self) -> Result<Settings, RepositoryError> {
213            Ok(self.settings.lock().unwrap().clone())
214        }
215        async fn save(&self, settings: &Settings) -> Result<(), RepositoryError> {
216            *self.settings.lock().unwrap() = settings.clone();
217            Ok(())
218        }
219    }
220
221    #[tokio::test]
222    async fn test_app_core_creation() {
223        let repos = Repos {
224            models: Arc::new(MockModelRepo),
225            settings: Arc::new(MockSettingsRepo::new()),
226            mcp_servers: Arc::new(MockMcpRepo),
227            chat_history: Arc::new(MockChatHistoryRepo),
228        };
229
230        let core = AppCore::new(repos);
231
232        // Verify services are accessible
233        let models = core.models().list().await.unwrap();
234        assert!(models.is_empty());
235
236        let settings = core.settings().get().await.unwrap();
237        assert_eq!(settings.default_context_size, Some(4096));
238    }
239}