tsk/context/
repository_context.rs1use anyhow::Result;
2use async_trait::async_trait;
3use std::path::Path;
4use std::sync::Arc;
5
6use super::FileSystemOperations;
7
8#[async_trait]
10pub trait RepositoryContext: Send + Sync {
11 async fn detect_tech_stack(&self, repo_path: &Path) -> Result<String>;
13
14 async fn detect_project_name(&self, repo_path: &Path) -> Result<String>;
16}
17
18pub struct DefaultRepositoryContext {
20 file_system: Arc<dyn FileSystemOperations>,
21}
22
23impl DefaultRepositoryContext {
24 pub fn new(file_system: Arc<dyn FileSystemOperations>) -> Self {
26 Self { file_system }
27 }
28
29 async fn file_exists(&self, repo_path: &Path, file_name: &str) -> bool {
31 let file_path = repo_path.join(file_name);
32 self.file_system.exists(&file_path).await.unwrap_or(false)
33 }
34
35 fn clean_project_name(name: &str) -> String {
37 let cleaned: String = name
39 .chars()
40 .map(|c| {
41 if c.is_alphanumeric() || c == '-' {
42 c
43 } else {
44 '-'
45 }
46 })
47 .collect::<String>()
48 .to_lowercase();
49
50 let mut result = String::new();
52 let mut prev_dash = false;
53 for c in cleaned.chars() {
54 if c == '-' {
55 if !prev_dash {
56 result.push(c);
57 }
58 prev_dash = true;
59 } else {
60 result.push(c);
61 prev_dash = false;
62 }
63 }
64
65 result.trim_matches('-').to_string()
67 }
68}
69
70#[async_trait]
71impl RepositoryContext for DefaultRepositoryContext {
72 async fn detect_tech_stack(&self, repo_path: &Path) -> Result<String> {
73 let tech_stack = if self.file_exists(repo_path, "Cargo.toml").await {
75 "rust"
76 } else if self.file_exists(repo_path, "pyproject.toml").await
77 || self.file_exists(repo_path, "requirements.txt").await
78 || self.file_exists(repo_path, "setup.py").await
79 {
80 "python"
81 } else if self.file_exists(repo_path, "package.json").await {
82 "node"
83 } else if self.file_exists(repo_path, "go.mod").await {
84 "go"
85 } else if self.file_exists(repo_path, "pom.xml").await
86 || self.file_exists(repo_path, "build.gradle").await
87 || self.file_exists(repo_path, "build.gradle.kts").await
88 {
89 "java"
90 } else if self.file_exists(repo_path, "rockspec").await
91 || self.file_exists(repo_path, ".luacheckrc").await
92 || self.file_exists(repo_path, "init.lua").await
93 {
94 "lua"
95 } else {
96 "default"
97 };
98
99 Ok(tech_stack.to_string())
100 }
101
102 async fn detect_project_name(&self, repo_path: &Path) -> Result<String> {
103 let project_name = repo_path
105 .file_name()
106 .and_then(|name| name.to_str())
107 .map(Self::clean_project_name)
108 .unwrap_or_else(|| "default".to_string());
109
110 let project_name = if project_name.is_empty() {
112 "default".to_string()
113 } else {
114 project_name
115 };
116
117 Ok(project_name)
118 }
119}
120
121pub struct MockRepositoryContext {
123 tech_stack: String,
124 project_name: String,
125}
126
127impl MockRepositoryContext {
128 pub fn new(tech_stack: String, project_name: String) -> Self {
130 Self {
131 tech_stack,
132 project_name,
133 }
134 }
135}
136
137#[async_trait]
138impl RepositoryContext for MockRepositoryContext {
139 async fn detect_tech_stack(&self, _repo_path: &Path) -> Result<String> {
140 Ok(self.tech_stack.clone())
141 }
142
143 async fn detect_project_name(&self, _repo_path: &Path) -> Result<String> {
144 Ok(self.project_name.clone())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::context::file_system::tests::MockFileSystem;
152
153 #[tokio::test]
154 async fn test_detect_rust_tech_stack() {
155 let mock_fs = MockFileSystem::new().with_file("/repo/Cargo.toml", "");
156
157 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
158 let result = repo_context
159 .detect_tech_stack(Path::new("/repo"))
160 .await
161 .unwrap();
162
163 assert_eq!(result, "rust");
164 }
165
166 #[tokio::test]
167 async fn test_detect_python_tech_stack() {
168 let mock_fs = MockFileSystem::new().with_file("/repo/pyproject.toml", "");
169
170 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
171 let result = repo_context
172 .detect_tech_stack(Path::new("/repo"))
173 .await
174 .unwrap();
175
176 assert_eq!(result, "python");
177 }
178
179 #[tokio::test]
180 async fn test_detect_python_tech_stack_requirements() {
181 let mock_fs = MockFileSystem::new().with_file("/repo/requirements.txt", "");
182
183 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
184 let result = repo_context
185 .detect_tech_stack(Path::new("/repo"))
186 .await
187 .unwrap();
188
189 assert_eq!(result, "python");
190 }
191
192 #[tokio::test]
193 async fn test_detect_node_tech_stack() {
194 let mock_fs = MockFileSystem::new().with_file("/repo/package.json", "");
195
196 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
197 let result = repo_context
198 .detect_tech_stack(Path::new("/repo"))
199 .await
200 .unwrap();
201
202 assert_eq!(result, "node");
203 }
204
205 #[tokio::test]
206 async fn test_detect_go_tech_stack() {
207 let mock_fs = MockFileSystem::new().with_file("/repo/go.mod", "");
208
209 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
210 let result = repo_context
211 .detect_tech_stack(Path::new("/repo"))
212 .await
213 .unwrap();
214
215 assert_eq!(result, "go");
216 }
217
218 #[tokio::test]
219 async fn test_detect_java_tech_stack() {
220 let mock_fs = MockFileSystem::new().with_file("/repo/pom.xml", "");
221
222 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
223 let result = repo_context
224 .detect_tech_stack(Path::new("/repo"))
225 .await
226 .unwrap();
227
228 assert_eq!(result, "java");
229 }
230
231 #[tokio::test]
232 async fn test_detect_lua_tech_stack_rockspec() {
233 let mock_fs = MockFileSystem::new().with_file("/repo/rockspec", "");
234
235 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
236 let result = repo_context
237 .detect_tech_stack(Path::new("/repo"))
238 .await
239 .unwrap();
240
241 assert_eq!(result, "lua");
242 }
243
244 #[tokio::test]
245 async fn test_detect_lua_tech_stack_luacheckrc() {
246 let mock_fs = MockFileSystem::new().with_file("/repo/.luacheckrc", "");
247
248 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
249 let result = repo_context
250 .detect_tech_stack(Path::new("/repo"))
251 .await
252 .unwrap();
253
254 assert_eq!(result, "lua");
255 }
256
257 #[tokio::test]
258 async fn test_detect_lua_tech_stack_init() {
259 let mock_fs = MockFileSystem::new().with_file("/repo/init.lua", "");
260
261 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
262 let result = repo_context
263 .detect_tech_stack(Path::new("/repo"))
264 .await
265 .unwrap();
266
267 assert_eq!(result, "lua");
268 }
269
270 #[tokio::test]
271 async fn test_detect_default_tech_stack() {
272 let mock_fs = MockFileSystem::new();
273
274 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
275 let result = repo_context
276 .detect_tech_stack(Path::new("/repo"))
277 .await
278 .unwrap();
279
280 assert_eq!(result, "default");
281 }
282
283 #[tokio::test]
284 async fn test_detect_project_name() {
285 let mock_fs = MockFileSystem::new();
286 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
287
288 let result = repo_context
289 .detect_project_name(Path::new("/home/user/my-awesome-project"))
290 .await
291 .unwrap();
292 assert_eq!(result, "my-awesome-project");
293 }
294
295 #[tokio::test]
296 async fn test_clean_project_name() {
297 let mock_fs = MockFileSystem::new();
298 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
299
300 let result = repo_context
301 .detect_project_name(Path::new("/home/user/My_Awesome Project!"))
302 .await
303 .unwrap();
304 assert_eq!(result, "my-awesome-project");
305 }
306
307 #[tokio::test]
308 async fn test_project_name_with_special_chars() {
309 let mock_fs = MockFileSystem::new();
310 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
311
312 let result = repo_context
313 .detect_project_name(Path::new("/home/user/test@#$%project"))
314 .await
315 .unwrap();
316 assert_eq!(result, "test-project");
317 }
318
319 #[tokio::test]
320 async fn test_project_name_fallback() {
321 let mock_fs = MockFileSystem::new();
322 let repo_context = DefaultRepositoryContext::new(Arc::new(mock_fs));
323
324 let result = repo_context
325 .detect_project_name(Path::new("/"))
326 .await
327 .unwrap();
328 assert_eq!(result, "default");
329 }
330
331 #[tokio::test]
332 async fn test_mock_repository_context() {
333 let mock =
334 MockRepositoryContext::new("custom-stack".to_string(), "custom-project".to_string());
335
336 assert_eq!(
337 mock.detect_tech_stack(Path::new("/any")).await.unwrap(),
338 "custom-stack"
339 );
340 assert_eq!(
341 mock.detect_project_name(Path::new("/any")).await.unwrap(),
342 "custom-project"
343 );
344 }
345}