grass/dev/
config.rs

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    // TODO: Return a Result
84    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        // TODO: This tests depends on the existance of a home directory
247        // Figure out a way that it doesn't.
248        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}