1pub mod docker_client;
2pub mod file_system;
3pub mod git_operations;
4pub mod repository_context;
5pub mod terminal;
6pub mod tsk_client;
7
8use crate::notifications::NotificationClient;
9use crate::storage::XdgDirectories;
10use docker_client::DockerClient;
11use file_system::FileSystemOperations;
12use git_operations::GitOperations;
13use repository_context::RepositoryContext;
14use terminal::TerminalOperations;
15
16use std::sync::Arc;
17use tsk_client::TskClient;
18
19#[cfg(test)]
21pub use terminal::TerminalOperations as TerminalOperationsTrait;
22
23#[derive(Clone)]
24pub struct AppContext {
25 docker_client: Arc<dyn DockerClient>,
26 file_system: Arc<dyn FileSystemOperations>,
27 git_operations: Arc<dyn GitOperations>,
28 notification_client: Arc<dyn NotificationClient>,
29 repository_context: Arc<dyn RepositoryContext>,
30 terminal_operations: Arc<dyn TerminalOperations>,
31 tsk_client: Arc<dyn TskClient>,
32 xdg_directories: Arc<XdgDirectories>,
33}
34
35impl AppContext {
36 pub fn builder() -> AppContextBuilder {
37 AppContextBuilder::new()
38 }
39
40 pub fn docker_client(&self) -> Arc<dyn DockerClient> {
41 Arc::clone(&self.docker_client)
42 }
43
44 pub fn file_system(&self) -> Arc<dyn FileSystemOperations> {
45 Arc::clone(&self.file_system)
46 }
47
48 pub fn git_operations(&self) -> Arc<dyn GitOperations> {
49 Arc::clone(&self.git_operations)
50 }
51
52 pub fn notification_client(&self) -> Arc<dyn NotificationClient> {
53 Arc::clone(&self.notification_client)
54 }
55
56 pub fn repository_context(&self) -> Arc<dyn RepositoryContext> {
57 Arc::clone(&self.repository_context)
58 }
59
60 pub fn terminal_operations(&self) -> Arc<dyn TerminalOperations> {
61 Arc::clone(&self.terminal_operations)
62 }
63
64 pub fn tsk_client(&self) -> Arc<dyn TskClient> {
65 Arc::clone(&self.tsk_client)
66 }
67
68 pub fn xdg_directories(&self) -> Arc<XdgDirectories> {
69 Arc::clone(&self.xdg_directories)
70 }
71}
72
73pub struct AppContextBuilder {
74 docker_client: Option<Arc<dyn DockerClient>>,
75 file_system: Option<Arc<dyn FileSystemOperations>>,
76 git_operations: Option<Arc<dyn GitOperations>>,
77 notification_client: Option<Arc<dyn NotificationClient>>,
78 repository_context: Option<Arc<dyn RepositoryContext>>,
79 terminal_operations: Option<Arc<dyn TerminalOperations>>,
80 tsk_client: Option<Arc<dyn TskClient>>,
81 xdg_directories: Option<Arc<XdgDirectories>>,
82}
83
84impl Default for AppContextBuilder {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl AppContextBuilder {
91 pub fn new() -> Self {
92 Self {
93 docker_client: None,
94 file_system: None,
95 git_operations: None,
96 notification_client: None,
97 repository_context: None,
98 terminal_operations: None,
99 tsk_client: None,
100 xdg_directories: None,
101 }
102 }
103
104 pub fn with_docker_client(mut self, docker_client: Arc<dyn DockerClient>) -> Self {
105 self.docker_client = Some(docker_client);
106 self
107 }
108
109 pub fn with_file_system(mut self, file_system: Arc<dyn FileSystemOperations>) -> Self {
110 self.file_system = Some(file_system);
111 self
112 }
113
114 pub fn with_git_operations(mut self, git_operations: Arc<dyn GitOperations>) -> Self {
115 self.git_operations = Some(git_operations);
116 self
117 }
118
119 pub fn with_notification_client(
120 mut self,
121 notification_client: Arc<dyn NotificationClient>,
122 ) -> Self {
123 self.notification_client = Some(notification_client);
124 self
125 }
126
127 pub fn with_repository_context(
128 mut self,
129 repository_context: Arc<dyn RepositoryContext>,
130 ) -> Self {
131 self.repository_context = Some(repository_context);
132 self
133 }
134
135 pub fn with_terminal_operations(
136 mut self,
137 terminal_operations: Arc<dyn TerminalOperations>,
138 ) -> Self {
139 self.terminal_operations = Some(terminal_operations);
140 self
141 }
142
143 pub fn with_tsk_client(mut self, tsk_client: Arc<dyn TskClient>) -> Self {
144 self.tsk_client = Some(tsk_client);
145 self
146 }
147
148 pub fn with_xdg_directories(mut self, xdg_directories: Arc<XdgDirectories>) -> Self {
149 self.xdg_directories = Some(xdg_directories);
150 self
151 }
152
153 pub fn build(self) -> AppContext {
154 let xdg_directories = self.xdg_directories.unwrap_or_else(|| {
155 let xdg = XdgDirectories::new().expect("Failed to initialize XDG directories");
156 xdg.ensure_directories()
158 .expect("Failed to create XDG directories");
159 Arc::new(xdg)
160 });
161
162 let tsk_client = self.tsk_client.unwrap_or_else(|| {
163 Arc::new(tsk_client::DefaultTskClient::new(xdg_directories.clone()))
164 });
165
166 let docker_client = self
167 .docker_client
168 .unwrap_or_else(|| Arc::new(docker_client::DefaultDockerClient::new()));
169
170 let file_system = self
171 .file_system
172 .unwrap_or_else(|| Arc::new(file_system::DefaultFileSystem));
173
174 let repository_context = self.repository_context.unwrap_or_else(|| {
175 Arc::new(repository_context::DefaultRepositoryContext::new(
176 file_system.clone(),
177 ))
178 });
179
180 AppContext {
181 docker_client,
182 file_system,
183 git_operations: self
184 .git_operations
185 .unwrap_or_else(|| Arc::new(git_operations::DefaultGitOperations)),
186 notification_client: self
187 .notification_client
188 .unwrap_or_else(|| crate::notifications::create_notification_client()),
189 repository_context,
190 terminal_operations: self
191 .terminal_operations
192 .unwrap_or_else(|| Arc::new(terminal::DefaultTerminalOperations::new())),
193 tsk_client,
194 xdg_directories,
195 }
196 }
197}
198
199#[cfg(test)]
200impl AppContext {
201 pub fn new_with_test_docker(docker_client: Arc<dyn DockerClient>) -> Self {
202 AppContextBuilder::new()
203 .with_docker_client(docker_client)
204 .build()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::test_utils::FixedResponseDockerClient;
212 use std::sync::Arc;
213
214 #[test]
215 fn test_app_context_creation() {
216 let docker_client = Arc::new(FixedResponseDockerClient::default());
217 let app_context = AppContext::builder()
218 .with_docker_client(docker_client)
219 .build();
220
221 let client = app_context.docker_client();
223 assert!(client.as_any().is::<FixedResponseDockerClient>());
224 }
225
226 #[tokio::test]
227 async fn test_app_context_with_tsk_client() {
228 use crate::test_utils::NoOpTskClient;
229
230 let tsk_client = Arc::new(NoOpTskClient);
231 let app_context = AppContext::builder()
232 .with_tsk_client(tsk_client.clone())
233 .build();
234
235 let client = app_context.tsk_client();
237 assert!(!client.is_server_available().await);
238 assert!(client.list_tasks().await.unwrap().is_empty());
239 }
240
241 #[tokio::test]
242 async fn test_app_context_docker_client_usage() {
243 let docker_client = Arc::new(FixedResponseDockerClient::default());
244 let app_context = AppContext::builder()
245 .with_docker_client(docker_client.clone())
246 .build();
247
248 let client = app_context.docker_client();
250 let container_id = client
251 .create_container(None, bollard::container::Config::default())
252 .await
253 .unwrap();
254
255 assert_eq!(container_id, "test-container-id");
256 }
257
258 #[test]
259 fn test_app_context_test_constructor() {
260 let docker_client = Arc::new(FixedResponseDockerClient::default());
261 let app_context = AppContext::new_with_test_docker(docker_client);
262
263 let client = app_context.docker_client();
265 assert!(client.as_any().is::<FixedResponseDockerClient>());
266 }
267
268 #[tokio::test]
269 async fn test_app_context_with_file_system() {
270 use crate::context::file_system::tests::MockFileSystem;
271
272 let docker_client = Arc::new(FixedResponseDockerClient::default());
273 let file_system =
274 Arc::new(MockFileSystem::new().with_file("/test/file.txt", "test content"));
275
276 let app_context = AppContext::builder()
277 .with_docker_client(docker_client)
278 .with_file_system(file_system.clone())
279 .build();
280
281 let fs = app_context.file_system();
283 let content = fs
284 .read_file(std::path::Path::new("/test/file.txt"))
285 .await
286 .unwrap();
287 assert_eq!(content, "test content");
288 }
289
290 #[test]
291 fn test_app_context_with_terminal_operations() {
292 use std::sync::Mutex;
293
294 #[derive(Default)]
296 struct MockTerminalOperations {
297 titles: Mutex<Vec<String>>,
298 restore_called: Mutex<bool>,
299 }
300
301 impl TerminalOperations for MockTerminalOperations {
302 fn set_title(&self, title: &str) {
303 self.titles.lock().unwrap().push(title.to_string());
304 }
305
306 fn restore_title(&self) {
307 *self.restore_called.lock().unwrap() = true;
308 }
309 }
310
311 let mock_terminal = Arc::new(MockTerminalOperations::default());
312 let app_context = AppContext::builder()
313 .with_terminal_operations(mock_terminal.clone())
314 .build();
315
316 let terminal = app_context.terminal_operations();
318 terminal.set_title("Test Title 1");
319 terminal.set_title("Test Title 2");
320 terminal.restore_title();
321
322 assert_eq!(
324 *mock_terminal.titles.lock().unwrap(),
325 vec!["Test Title 1", "Test Title 2"]
326 );
327 assert!(*mock_terminal.restore_called.lock().unwrap());
328 }
329}