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>, workspaces: HashMap<String, Workspace>, }
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}