dev_workspaces/
lib.rs

1use std::{collections::HashMap, fs, path::PathBuf};
2
3use anyhow::{anyhow, Context, Result};
4use serde::Deserialize;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct Config {
8    root: String,
9    workspaces: HashMap<String, Workspace>,
10}
11
12#[derive(Debug, Clone, Deserialize)]
13pub struct Workspace {
14    projects: HashMap<String, Project>,     // make optional
15    workspaces: HashMap<String, Workspace>, // make optional
16}
17
18#[derive(Debug, Clone, Deserialize)]
19pub struct Project;
20
21impl Config {
22    pub fn file_path() -> Result<PathBuf> {
23        let home_dir = home::home_dir().expect("Could not determine home directory");
24        Ok(home_dir.clone().join(".config/workspaces/workspaces.yaml"))
25    }
26
27    pub fn from_config_file() -> Result<Self> {
28        let config_file = fs::read_to_string(Self::file_path()?)
29            .context("Tried reading ~/.config/workspaces/workspaces.yaml")?;
30
31        Self::from_str(config_file.as_str())
32    }
33
34    pub(crate) fn from_str(contents: &str) -> Result<Self> {
35        let home_dir = home::home_dir().expect("Could not determine home directory");
36
37        serde_yaml::from_str(contents)
38            .context("Tried loading config from ~/.config/workspaces/workspaces.yaml")
39            .and_then(|c: Self| {
40                if !c.root.starts_with("~") {
41                    return Ok(c);
42                }
43                let mut c = c;
44                c.root = home_dir
45                    .into_os_string()
46                    .into_string()
47                    .map_err(|err| anyhow!("Error: {:?}", err))
48                    .context("Something unexpected happened")?;
49                Ok(c)
50            })
51    }
52
53    pub fn collect_workspace_paths(&self) -> Vec<PathBuf> {
54        let parent = PathBuf::from(self.root.clone());
55
56        self.workspaces
57            .iter()
58            .map(|(name, ws)| {
59                let path = parent.clone().join(name);
60                let mut nested = ws.collect_workspace_paths(path.clone());
61                nested.push(path);
62                nested
63            })
64            .collect::<Vec<Vec<PathBuf>>>()
65            .concat()
66    }
67
68    pub fn collect_project_paths(&self) -> Vec<PathBuf> {
69        let parent = PathBuf::from(self.root.clone());
70
71        self.workspaces
72            .iter()
73            .map(|(name, ws)| {
74                let path = parent.clone().join(name);
75                ws.collect_project_paths(path.clone())
76            })
77            .collect::<Vec<Vec<PathBuf>>>()
78            .concat()
79    }
80}
81
82impl Workspace {
83    pub fn collect_workspace_paths(&self, parent: PathBuf) -> Vec<PathBuf> {
84        self.workspaces
85            .iter()
86            .map(|(name, ws)| {
87                let path = parent.clone().join(name);
88                let mut nested = ws.collect_workspace_paths(path.clone());
89                nested.push(path);
90                nested
91            })
92            .collect::<Vec<Vec<PathBuf>>>()
93            .concat()
94    }
95
96    pub fn collect_project_paths(&self, parent: PathBuf) -> Vec<PathBuf> {
97        let projects = self
98            .projects
99            .iter()
100            .map(|(name, _)| parent.clone().join(name))
101            .collect::<Vec<PathBuf>>();
102
103        let nested_projects = self
104            .workspaces
105            .iter()
106            .map(|(name, ws)| {
107                let path = parent.clone().join(name);
108                ws.collect_project_paths(path.clone())
109            })
110            .collect::<Vec<Vec<PathBuf>>>()
111            .concat();
112
113        vec![projects, nested_projects].concat()
114    }
115}
116
117pub struct DoctorDiagnosis {
118    missing_workspaces: Vec<PathBuf>,
119    missing_projects: Vec<PathBuf>,
120}
121
122impl DoctorDiagnosis {
123    pub fn print(&self) {
124        println!("Dev Workspaces Doctor Diagnosis:\n");
125
126        println!("The following workspaces are missing:\n");
127
128        for w in self.missing_workspaces.iter() {
129            println!(
130                "\t{:}",
131                w.clone()
132                    .into_os_string()
133                    .into_string()
134                    .expect("Something unexpected happened")
135            );
136        }
137        println!("");
138
139        println!("The following projects are missing:\n");
140
141        for p in self.missing_projects.iter() {
142            println!(
143                "\t{:}",
144                p.clone()
145                    .into_os_string()
146                    .into_string()
147                    .expect("Something unexpected happened")
148            );
149        }
150        println!("");
151    }
152}
153
154pub fn doctor(config: &Config) -> Result<DoctorDiagnosis> {
155    let missing_workspaces = config
156        .collect_workspace_paths()
157        .iter()
158        .filter(|p| !p.exists())
159        .map(Clone::clone)
160        .collect::<Vec<PathBuf>>();
161    let missing_projects = config
162        .collect_project_paths()
163        .iter()
164        .filter(|p| !p.exists())
165        .map(Clone::clone)
166        .collect::<Vec<PathBuf>>();
167
168    Ok(DoctorDiagnosis {
169        missing_workspaces,
170        missing_projects,
171    })
172}
173
174#[cfg(test)]
175mod should {
176
177    use std::path::PathBuf;
178
179    use rstest::*;
180
181    #[rstest]
182    fn list_workspaces() {
183        let contents = r#"---
184root: /some/root
185workspaces:
186  w0:
187    projects:
188      p0:
189    workspaces:
190      w1:
191        projects:
192          p1:
193        workspaces:
194          w2:
195            projects:
196              p2:
197            workspaces:
198              w3:
199                projects:
200                workspaces:
201"#;
202
203        let config = super::Config::from_str(contents);
204
205        assert!(config.is_ok());
206
207        let config = config.unwrap();
208
209        let mut workspaces = config.collect_workspace_paths();
210
211        assert_eq!(
212            workspaces.sort(),
213            vec![
214                PathBuf::from("/some/root/w0"),
215                PathBuf::from("/some/root/w0/w1"),
216                PathBuf::from("/some/root/w0/w1/w2"),
217                PathBuf::from("/some/root/w0/w1/w2/w3"),
218            ]
219            .sort()
220        );
221    }
222
223    #[rstest]
224    fn list_projects() {
225        let contents = r#"---
226root: /some/root
227workspaces:
228  w0:
229    projects:
230      p0:
231    workspaces:
232      w1:
233        projects:
234          p1:
235        workspaces:
236          w2:
237            projects:
238              p2:
239            workspaces:
240              w3:
241                projects:
242                workspaces:
243"#;
244
245        let config = super::Config::from_str(contents);
246
247        assert!(config.is_ok());
248
249        let config = config.unwrap();
250
251        let mut projects = config.collect_project_paths();
252
253        assert_eq!(
254            projects.sort(),
255            vec![
256                PathBuf::from("/some/root/w0/p0"),
257                PathBuf::from("/some/root/w0/w1/p1"),
258                PathBuf::from("/some/root/w0/w1/w2/p2"),
259            ]
260            .sort()
261        );
262    }
263}