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 find_by_path(
104            &self,
105            _path: &std::path::Path,
106        ) -> Result<Option<Model>, RepositoryError> {
107            // `unimplemented!()` like its siblings, not `Ok(None)`. `Ok(None)`
108            // reads as "no duplicate found", which is a specific and wrong
109            // answer for a double that stores nothing.
110            unimplemented!()
111        }
112        async fn insert(&self, _model: &NewModel) -> Result<Model, RepositoryError> {
113            unimplemented!()
114        }
115        async fn update(&self, _model: &Model) -> Result<(), RepositoryError> {
116            unimplemented!()
117        }
118        async fn delete(&self, _id: i64) -> Result<(), RepositoryError> {
119            Ok(())
120        }
121    }
122
123    struct MockMcpRepo;
124
125    #[async_trait]
126    impl McpServerRepository for MockMcpRepo {
127        async fn insert(&self, _server: NewMcpServer) -> Result<McpServer, McpRepositoryError> {
128            unimplemented!()
129        }
130        async fn get_by_id(&self, id: i64) -> Result<McpServer, McpRepositoryError> {
131            Err(McpRepositoryError::NotFound(format!("id={id}")))
132        }
133        async fn get_by_name(&self, name: &str) -> Result<McpServer, McpRepositoryError> {
134            Err(McpRepositoryError::NotFound(format!("name={name}")))
135        }
136        async fn list(&self) -> Result<Vec<McpServer>, McpRepositoryError> {
137            Ok(vec![])
138        }
139        async fn update(&self, _server: &McpServer) -> Result<(), McpRepositoryError> {
140            unimplemented!()
141        }
142        async fn delete(&self, _id: i64) -> Result<(), McpRepositoryError> {
143            Ok(())
144        }
145        async fn update_last_connected(&self, _id: i64) -> Result<(), McpRepositoryError> {
146            Ok(())
147        }
148    }
149
150    struct MockChatHistoryRepo;
151
152    #[async_trait]
153    impl ChatHistoryRepository for MockChatHistoryRepo {
154        async fn create_conversation(
155            &self,
156            _conv: NewConversation,
157        ) -> Result<i64, ChatHistoryError> {
158            Ok(1)
159        }
160        async fn list_conversations(&self) -> Result<Vec<Conversation>, ChatHistoryError> {
161            Ok(vec![])
162        }
163        async fn get_conversation(
164            &self,
165            _id: i64,
166        ) -> Result<Option<Conversation>, ChatHistoryError> {
167            Ok(None)
168        }
169        async fn update_conversation(
170            &self,
171            _id: i64,
172            _update: ConversationUpdate,
173        ) -> Result<(), ChatHistoryError> {
174            Ok(())
175        }
176        async fn delete_conversation(&self, _id: i64) -> Result<(), ChatHistoryError> {
177            Ok(())
178        }
179        async fn get_conversation_count(&self) -> Result<i64, ChatHistoryError> {
180            Ok(0)
181        }
182        async fn get_messages(
183            &self,
184            _conversation_id: i64,
185        ) -> Result<Vec<Message>, ChatHistoryError> {
186            Ok(vec![])
187        }
188        async fn save_message(&self, _msg: NewMessage) -> Result<i64, ChatHistoryError> {
189            Ok(1)
190        }
191        async fn update_message(
192            &self,
193            _id: i64,
194            _content: String,
195            _metadata: Option<serde_json::Value>,
196        ) -> Result<(), ChatHistoryError> {
197            Ok(())
198        }
199        async fn delete_message_and_subsequent(&self, _id: i64) -> Result<i64, ChatHistoryError> {
200            Ok(0)
201        }
202        async fn get_message_count(&self, _conversation_id: i64) -> Result<i64, ChatHistoryError> {
203            Ok(0)
204        }
205    }
206
207    struct MockSettingsRepo {
208        settings: Mutex<Settings>,
209    }
210
211    impl MockSettingsRepo {
212        fn new() -> Self {
213            Self {
214                settings: Mutex::new(Settings::with_defaults()),
215            }
216        }
217    }
218
219    #[async_trait]
220    impl SettingsRepository for MockSettingsRepo {
221        async fn load(&self) -> Result<Settings, RepositoryError> {
222            Ok(self.settings.lock().unwrap().clone())
223        }
224        async fn save(&self, settings: &Settings) -> Result<(), RepositoryError> {
225            *self.settings.lock().unwrap() = settings.clone();
226            Ok(())
227        }
228    }
229
230    #[tokio::test]
231    async fn test_app_core_creation() {
232        let repos = Repos {
233            models: Arc::new(MockModelRepo),
234            settings: Arc::new(MockSettingsRepo::new()),
235            mcp_servers: Arc::new(MockMcpRepo),
236            chat_history: Arc::new(MockChatHistoryRepo),
237        };
238
239        let core = AppCore::new(repos);
240
241        // Verify services are accessible
242        let models = core.models().list().await.unwrap();
243        assert!(models.is_empty());
244
245        let settings = core.settings().get().await.unwrap();
246        // Unset, not the floor: nothing has chosen a global default here.
247        assert_eq!(settings.default_context_size, None);
248    }
249}