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