1mod load;
2
3use std::{
4 cell::{Ref, RefCell},
5 collections::hash_map::{Entry, HashMap},
6 fs::{self, File},
7 io::Read,
8 path::PathBuf,
9 rc::Rc,
10};
11use thiserror::Error;
12
13use self::load::LoadRootConfig;
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
16pub struct GrassCategory {
17 pub name: String,
18 pub alias: Vec<String>,
19}
20
21#[derive(Debug, Clone)]
22pub struct GrassConfig {
23 pub category: HashMap<String, Rc<RefCell<GrassCategory>>>,
24 pub aliases: HashMap<String, Rc<RefCell<GrassCategory>>>,
25 pub base_dir: PathBuf,
26}
27
28#[derive(Debug, Clone)]
29pub struct RootConfig {
30 pub grass: GrassConfig,
31}
32
33impl GrassConfig {
34 pub fn try_default() -> Option<Self> {
35 Some(Self {
36 category: HashMap::default(),
37 aliases: HashMap::default(),
38 base_dir: dirs::home_dir()?.join("repos"),
39 })
40 }
41 pub fn get_from_category_or_alias<T>(&self, name: T) -> Option<Ref<GrassCategory>>
42 where
43 T: AsRef<str>,
44 {
45 self.category
46 .get(name.as_ref())
47 .or_else(|| self.aliases.get(name.as_ref()))
48 .map(|value| value.borrow())
49 }
50
51 pub fn get_by_category<T>(&self, category_name: T) -> Option<Ref<GrassCategory>>
52 where
53 T: AsRef<str>,
54 {
55 self.category
56 .get(category_name.as_ref())
57 .map(|value| value.borrow())
58 }
59
60 pub fn get_by_alias<T>(&self, alias_name: T) -> Option<Ref<GrassCategory>>
61 where
62 T: AsRef<str>,
63 {
64 self.aliases
65 .get(alias_name.as_ref())
66 .map(|value| value.borrow())
67 }
68}
69
70#[derive(Error, Debug)]
71pub enum MergeError {
72 #[error("Cannot find home directory")]
73 MissingHomeDirectory,
74}
75
76impl RootConfig {
77 pub fn try_default() -> Option<Self> {
78 Some(Self {
79 grass: GrassConfig::try_default()?,
80 })
81 }
82
83 pub fn merge(&mut self, next: &LoadRootConfig) -> Result<&mut Self, MergeError> {
85 let grass = if let Some(grass) = &next.grass {
86 grass
87 } else {
88 return Ok(self);
89 };
90
91 if let Some(base_dir) = &grass.base_dir {
92 match base_dir.strip_prefix("~/") {
93 Some(suffix) => {
94 if let Some(home_dir) = dirs::home_dir() {
95 self.grass.base_dir = home_dir.join(suffix);
96 } else {
97 return Err(MergeError::MissingHomeDirectory);
98 };
99 }
100 None => self.grass.base_dir = PathBuf::from(base_dir),
101 }
102 };
103
104 for (key, category) in &grass.category {
105 let category_rc = match self.grass.category.entry(key.clone()) {
106 Entry::Vacant(e) => {
107 let result = Rc::from(RefCell::from(GrassCategory {
108 name: key.clone(),
109 alias: category.alias.clone(),
110 }));
111 e.insert(result).clone()
112 }
113 Entry::Occupied(e) => {
114 e.get().borrow_mut().name = key.clone();
115 e.get().clone()
116 }
117 };
118
119 for alias in &category.alias {
120 self.grass
121 .aliases
122 .insert(alias.clone(), category_rc.clone());
123 }
124 }
125
126 Ok(self)
127 }
128}
129
130#[derive(Error, Debug)]
131pub enum LoadUserError {
132 #[error("Cannot find home directory")]
133 MissingHomeDirectory,
134 #[error("Cannot find configuration directory")]
135 MissingConfigurationDirectory,
136 #[error("Cannot read configuration file:\n{io_error}")]
137 CannotReadConfigurationFile { io_error: std::io::Error },
138 #[error("Cannot create default configuration")]
139 CannotCreateDefault,
140 #[error("The configuration file\n''\nwas improperly formatted:\n{reason}")]
141 ImproperlyFormatted { file: PathBuf, reason: String },
142}
143
144pub fn load_user_config() -> Result<RootConfig, LoadUserError> {
145 let mut file = File::open(
146 dirs::config_dir()
147 .ok_or(LoadUserError::MissingHomeDirectory)?
148 .join("grass/config.toml"),
149 )
150 .map_err(|error| LoadUserError::CannotReadConfigurationFile { io_error: error })?;
151
152 let mut contents = String::new();
153 file.read_to_string(&mut contents)
154 .map_err(|error| LoadUserError::CannotReadConfigurationFile { io_error: error })?;
155
156 let mut config = RootConfig::try_default().ok_or(LoadUserError::CannotCreateDefault)?;
157
158 let config_dir = dirs::config_dir()
159 .ok_or(LoadUserError::MissingConfigurationDirectory)?
160 .join("grass");
161 if let Ok(entries) = fs::read_dir(&config_dir) {
162 for file_name in
163 entries
164 .filter_map(|entry| entry.ok())
165 .filter_map(|entry| match entry.metadata() {
166 Ok(metadata) if metadata.is_file() => Some(entry.file_name()),
167 _ => None,
168 })
169 {
170 let mut file = if let Ok(file) = File::open(&config_dir.join(&file_name)) {
171 file
172 } else {
173 continue;
174 };
175
176 let mut contents = String::new();
177
178 if file.read_to_string(&mut contents).is_err() {
179 continue;
180 };
181
182 let load_config: LoadRootConfig =
183 toml::from_str(&contents).map_err(|error| LoadUserError::ImproperlyFormatted {
184 file: PathBuf::from(file_name),
185 reason: error.to_string(),
186 })?;
187 config
188 .merge(&load_config)
189 .map_err(|_| LoadUserError::MissingHomeDirectory)?;
190 }
191 }
192
193 Ok(config)
194}
195
196pub fn load_example_config() -> RootConfig {
197 let general = Rc::from(RefCell::from(GrassCategory {
198 name: String::from("general"),
199 alias: vec![String::from("gen")],
200 }));
201 let work = Rc::from(RefCell::from(GrassCategory {
202 name: String::from("work"),
203 alias: Vec::new(),
204 }));
205 RootConfig {
206 grass: GrassConfig {
207 aliases: HashMap::from([(String::from("gen"), general.clone())]),
208 category: HashMap::from([
209 (String::from("general"), general),
210 (String::from("work"), work),
211 ]),
212 base_dir: dirs::home_dir().unwrap().join("repos"),
213 },
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::{
220 load::{LoadGrassCategory, LoadGrassConfig},
221 *,
222 };
223
224 fn get_load_config() -> LoadRootConfig {
225 LoadRootConfig {
226 grass: Some(LoadGrassConfig {
227 category: HashMap::from([
228 (String::from("work"), LoadGrassCategory { alias: vec![] }),
229 (
230 String::from("general"),
231 LoadGrassCategory {
232 alias: vec![String::from("gen")],
233 },
234 ),
235 ]),
236 base_dir: Some(String::from("~/my-repositories")),
237 }),
238 }
239 }
240
241 #[test]
242 fn test_config_merge() {
243 let mut config = RootConfig::try_default().unwrap();
244 config.merge(&get_load_config()).expect("Could not merge");
245
246 assert_eq!(
249 config.grass.base_dir,
250 dirs::home_dir().unwrap().join("my-repositories")
251 );
252 assert_eq!(
253 config.grass.category.get("work").unwrap().borrow().name,
254 "work"
255 );
256 assert_eq!(
257 config.grass.category.get("general").unwrap().borrow().name,
258 "general"
259 );
260 assert_eq!(
261 config.grass.aliases.get("gen").unwrap().borrow().name,
262 "general"
263 );
264 }
265
266 #[test]
267 fn test_config_get_category() {
268 let config = load_example_config();
269
270 let result_work = config.grass.get_by_category("work").unwrap();
271 let result_general = config.grass.get_by_category("general").unwrap();
272
273 assert_eq!(
274 *result_work,
275 GrassCategory {
276 name: String::from("work"),
277 alias: vec![]
278 }
279 );
280
281 assert_eq!(
282 *result_general,
283 GrassCategory {
284 name: String::from("general"),
285 alias: vec![String::from("gen")]
286 }
287 );
288 }
289
290 #[test]
291 fn test_config_get_alias() {
292 let config = load_example_config();
293
294 let result_gen = config.grass.get_by_alias("gen").unwrap();
295
296 assert_eq!(
297 *result_gen,
298 GrassCategory {
299 name: String::from("general"),
300 alias: vec![String::from("gen")]
301 }
302 );
303 }
304}