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