qk/
config.rs

1use crate::{commands_parser, Command};
2use std::{
3    collections::HashMap,
4    path::{Path, PathBuf},
5};
6
7use anyhow::{ensure, Result};
8use clap::crate_name;
9use serde::{Deserialize, Serialize};
10
11/// Configuration options
12///
13/// This determines the layout of the configuration file
14#[derive(Debug, Serialize, Deserialize)]
15pub struct Config {
16    /// Default editor to execute when creating or opening projects
17    editor: Option<String>,
18
19    /// Default shell to use for executing commands when creating projects
20    shell: Option<String>,
21
22    /// Templates to use for creating new projects
23    #[serde(default)]
24    templates: HashMap<String, TemplateConfig>,
25}
26
27impl Config {
28    pub fn editor(&self) -> Option<&String> {
29        self.editor.as_ref()
30    }
31
32    pub fn shell(&self) -> Option<&String> {
33        self.shell.as_ref()
34    }
35
36    pub fn find_template(&self, template: &str) -> Option<Template> {
37        self.templates.get(template).map(Into::into)
38    }
39
40    /// Returns the templates in the config
41    pub fn templates(&self) -> HashMap<String, Template> {
42        self.templates
43            .iter()
44            .map(|(k, v)| (k.into(), v.into()))
45            .collect()
46    }
47
48    /// Loads the config from the system's config directory
49    pub fn load() -> Result<Self> {
50        let name = crate_name!();
51        Ok(confy::load(name, name)?)
52    }
53
54    /// Loads the config from the `path` file
55    pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
56        ensure!(path.as_ref().is_file(), "config path is not a file");
57        Ok(confy::load_path(path)?)
58    }
59}
60
61impl Default for Config {
62    fn default() -> Self {
63        let mut templates = HashMap::new();
64        templates.insert(
65            String::from("example"),
66            TemplateConfig::Complete(Template {
67                projects_dir: PathBuf::from("/path/to/example/"),
68                editor: Some(String::from("code")),
69                shell: Some(String::from("bash")),
70                commands: vec![
71                    String::from("echo hello"),
72                    String::from("echo $PWD"),
73                    String::from("echo $QK_PROJECT_NAME"),
74                ],
75                name: String::from("example"),
76            }),
77        );
78        Self {
79            editor: Some(String::from("vi")),
80            shell: Some(String::from("sh")),
81            templates,
82        }
83    }
84}
85
86/// The container of a template
87///
88/// ```toml
89/// # Allows having both:
90/// [templates]
91/// example1 = "/path/to/example1/"
92///
93/// # and:
94/// [templates.example2]
95/// projects_dir = "/path/to/example2/"
96/// ```
97#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(untagged)]
99pub enum TemplateConfig {
100    /// Contains projects_dir
101    OnlyProjectsDir(String),
102    Complete(Template),
103}
104
105#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
106pub struct Template {
107    #[serde(skip)]
108    name: String,
109
110    /// The directory where new projects with this template will be created
111    projects_dir: PathBuf,
112
113    /// The editor to execute when creating or opening projects with this template
114    editor: Option<String>,
115
116    /// The shell to use for executing commands when creating projects with this template
117    shell: Option<String>,
118
119    #[serde(default)]
120    /// The commands to execute when creating a project with this template
121    commands: Vec<String>,
122}
123
124impl Template {
125    pub fn name(&self) -> &str {
126        &self.name
127    }
128
129    pub fn projects_dir(&self) -> &Path {
130        &self.projects_dir
131    }
132
133    pub fn editor(&self) -> Option<&String> {
134        self.editor.as_ref()
135    }
136
137    pub fn shell(&self) -> Option<&String> {
138        self.shell.as_ref()
139    }
140
141    /// Returns the commands in this template after parsing them
142    pub fn commands(&self) -> Result<Vec<Command>> {
143        let mut commands = vec![];
144        for cmd in self.commands.iter() {
145            let units = commands_parser::parse(cmd)?;
146            commands.push(units);
147        }
148        Ok(commands)
149    }
150}
151
152impl From<&TemplateConfig> for Template {
153    fn from(template: &TemplateConfig) -> Template {
154        match template {
155            TemplateConfig::OnlyProjectsDir(projects_dir) => Template {
156                projects_dir: projects_dir.into(),
157                editor: None,
158                shell: None,
159                commands: Vec::new(),
160                name: String::from(""),
161            },
162            TemplateConfig::Complete(template) => template.clone(),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::Unit;
171
172    #[test]
173    fn test_find_template_returns_none_when_empty() {
174        let config = Config {
175            editor: None,
176            shell: None,
177            templates: HashMap::new(),
178        };
179
180        assert_eq!(config.find_template("a"), None);
181    }
182
183    #[test]
184    fn test_find_template_returns_none_when_not_present() {
185        let mut templates = HashMap::new();
186        templates.insert(
187            String::from("a"),
188            TemplateConfig::OnlyProjectsDir(String::from("a")),
189        );
190        let config = Config {
191            editor: None,
192            shell: None,
193            templates,
194        };
195
196        assert_eq!(config.find_template("b"), None);
197    }
198
199    #[test]
200    fn test_find_template_returns_the_template() {
201        let mut templates = HashMap::new();
202
203        templates.insert(
204            String::from("a"),
205            TemplateConfig::OnlyProjectsDir(String::from("a")),
206        );
207
208        templates.insert(
209            String::from("b"),
210            TemplateConfig::OnlyProjectsDir(String::from("b")),
211        );
212
213        templates.insert(
214            String::from("c"),
215            TemplateConfig::OnlyProjectsDir(String::from("c")),
216        );
217
218        let config = Config {
219            editor: None,
220            shell: None,
221            templates,
222        };
223
224        assert_eq!(
225            config.find_template("b"),
226            Some(Template {
227                projects_dir: PathBuf::from("b"),
228                editor: None,
229                shell: None,
230                commands: vec![],
231                name: String::from("")
232            })
233        );
234    }
235
236    #[test]
237    fn test_templates() {
238        let mut templates = HashMap::new();
239
240        templates.insert(
241            String::from("a"),
242            TemplateConfig::OnlyProjectsDir(String::from("a")),
243        );
244
245        templates.insert(
246            String::from("b"),
247            TemplateConfig::Complete(Template {
248                projects_dir: PathBuf::from("b"),
249                editor: Some(String::from("vi")),
250                shell: Some(String::from("zsh")),
251                commands: vec![String::from("echo hello")],
252                name: String::from("b"),
253            }),
254        );
255
256        templates.insert(
257            String::from("c"),
258            TemplateConfig::OnlyProjectsDir(String::from("c")),
259        );
260
261        let config = Config {
262            editor: None,
263            shell: None,
264            templates,
265        };
266
267        let mut expected_templates = HashMap::new();
268
269        expected_templates.insert(
270            String::from("a"),
271            Template {
272                projects_dir: PathBuf::from("a"),
273                editor: None,
274                shell: None,
275                commands: vec![],
276                name: String::from(""),
277            },
278        );
279
280        expected_templates.insert(
281            String::from("b"),
282            Template {
283                projects_dir: PathBuf::from("b"),
284                editor: Some(String::from("vi")),
285                shell: Some(String::from("zsh")),
286                commands: vec![String::from("echo hello")],
287                name: String::from("b"),
288            },
289        );
290
291        expected_templates.insert(
292            String::from("c"),
293            Template {
294                projects_dir: PathBuf::from("c"),
295                editor: None,
296                shell: None,
297                commands: vec![],
298                name: String::from(""),
299            },
300        );
301
302        assert_eq!(config.templates(), expected_templates);
303    }
304
305    #[test]
306    fn test_from_template_config() {
307        let template_config1 = TemplateConfig::OnlyProjectsDir(String::from("a"));
308        let template_config2 = TemplateConfig::Complete(Template {
309            projects_dir: PathBuf::from("b"),
310            editor: Some(String::from("vi")),
311            shell: Some(String::from("fish")),
312            commands: vec![String::from("echo hello")],
313            name: String::from("b"),
314        });
315
316        let template1: Template = (&template_config1).into();
317        let template2: Template = (&template_config2).into();
318
319        assert_eq!(
320            template1,
321            Template {
322                projects_dir: PathBuf::from("a"),
323                editor: None,
324                shell: None,
325                commands: vec![],
326                name: String::from("")
327            }
328        );
329
330        assert_eq!(
331            template2,
332            Template {
333                projects_dir: PathBuf::from("b"),
334                editor: Some(String::from("vi")),
335                shell: Some(String::from("fish")),
336                commands: vec![String::from("echo hello")],
337                name: String::from("b")
338            }
339        );
340    }
341
342    #[test]
343    fn test_commands_method_with_simple_commands() {
344        let template = Template {
345            name: "a".to_string(),
346            projects_dir: "a".into(),
347            editor: None,
348            shell: None,
349            commands: vec![String::from("echo hello world"), String::from("echo hey!")],
350        };
351
352        assert_eq!(
353            template.commands().unwrap(),
354            vec![
355                vec![Unit::Text("echo hello world".to_string())],
356                vec![Unit::Text("echo hey!".to_string())]
357            ]
358        );
359    }
360}