1use std::collections::BTreeSet;
7use std::path::Path;
8
9use crate::errors::{DomainError, DomainResult};
10use ito_common::fs::FileSystem;
11use ito_common::paths;
12
13fn list_child_dirs<F: FileSystem>(fs: &F, dir: &Path) -> DomainResult<Vec<String>> {
14 if !fs.exists(dir) {
15 return Ok(Vec::new());
16 }
17
18 let entries = fs
19 .read_dir(dir)
20 .map_err(|source| DomainError::io("listing directory entries", source))?;
21 let mut out: Vec<String> = Vec::new();
22 for path in entries {
23 if !fs.is_dir(&path) {
24 continue;
25 }
26
27 let Some(name) = path.file_name() else {
28 continue;
29 };
30 let name = name.to_string_lossy().to_string();
31 if name.starts_with('.') {
32 continue;
33 }
34 out.push(name);
35 }
36
37 out.sort();
38 Ok(out)
39}
40
41pub fn list_dir_names<F: FileSystem>(fs: &F, dir: &Path) -> DomainResult<Vec<String>> {
45 list_child_dirs(fs, dir)
46}
47
48pub fn list_change_dir_names<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
50 let mut out = list_child_dirs(fs, paths::changes_dir(ito_path).as_path())?;
51 out.retain(|n| n != "archive");
52 Ok(out)
53}
54
55pub fn list_module_dir_names<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
57 list_child_dirs(fs, paths::modules_dir(ito_path).as_path())
58}
59
60pub fn list_module_ids<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<BTreeSet<String>> {
62 let mut ids: BTreeSet<String> = BTreeSet::new();
63 for name in list_module_dir_names(fs, ito_path)? {
64 let Some((id_part, _)) = name.split_once('_') else {
65 continue;
66 };
67 if id_part.len() == 3 && id_part.chars().all(|c| c.is_ascii_digit()) {
68 ids.insert(id_part.to_string());
69 }
70 }
71 Ok(ids)
72}
73
74pub fn list_spec_dir_names<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
76 list_child_dirs(fs, paths::specs_dir(ito_path).as_path())
77}
78
79pub fn list_changes<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
82 list_change_dir_names(fs, ito_path)
83}
84
85pub fn list_modules<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
87 list_module_dir_names(fs, ito_path)
88}
89
90pub fn list_specs<F: FileSystem>(fs: &F, ito_path: &Path) -> DomainResult<Vec<String>> {
92 list_spec_dir_names(fs, ito_path)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 use ito_common::fs::StdFs;
100
101 #[test]
102 fn list_changes_skips_archive_dir() {
103 let td = tempfile::tempdir().unwrap();
104 let ito_path = td.path().join(".ito");
105 std::fs::create_dir_all(ito_path.join("changes/archive")).unwrap();
106 std::fs::create_dir_all(ito_path.join("changes/001-01_test")).unwrap();
107
108 let fs = StdFs;
109 let changes = list_changes(&fs, &ito_path).unwrap();
110 assert_eq!(changes, vec!["001-01_test".to_string()]);
111 }
112
113 #[test]
114 fn list_modules_only_returns_directories() {
115 let td = tempfile::tempdir().unwrap();
116 let ito_path = td.path().join(".ito");
117 std::fs::create_dir_all(ito_path.join("modules/001_project-setup")).unwrap();
118 std::fs::create_dir_all(ito_path.join("modules/.hidden")).unwrap();
119 std::fs::create_dir_all(ito_path.join("modules/not-a-module")).unwrap();
120 std::fs::write(ito_path.join("modules/file.txt"), "x").unwrap();
121
122 let fs = StdFs;
123 let modules = list_modules(&fs, &ito_path).unwrap();
124 assert_eq!(
125 modules,
126 vec!["001_project-setup".to_string(), "not-a-module".to_string()]
127 );
128 }
129
130 #[test]
131 fn list_module_ids_extracts_numeric_prefixes() {
132 let td = tempfile::tempdir().unwrap();
133 let ito_path = td.path().join(".ito");
134 std::fs::create_dir_all(ito_path.join("modules/001_project-setup")).unwrap();
135 std::fs::create_dir_all(ito_path.join("modules/002_tools")).unwrap();
136 std::fs::create_dir_all(ito_path.join("modules/not-a-module")).unwrap();
137
138 let fs = StdFs;
139 let ids = list_module_ids(&fs, &ito_path).unwrap();
140 assert_eq!(
141 ids.into_iter().collect::<Vec<_>>(),
142 vec!["001".to_string(), "002".to_string()]
143 );
144 }
145}