Skip to main content

runex_core/
expand.rs

1use crate::model::{Config, ExpandResult};
2
3/// Expand a token using the config.
4///
5/// `command_exists` is injected for testability (DI).
6pub fn expand<F>(config: &Config, token: &str, command_exists: F) -> ExpandResult
7where
8    F: Fn(&str) -> bool,
9{
10    for abbr in &config.abbr {
11        if abbr.key != token {
12            continue;
13        }
14        // Infinite-loop guard: key == expand means no-op.
15        if abbr.key == abbr.expand {
16            continue;
17        }
18        // Check when_command_exists condition.
19        if let Some(cmds) = &abbr.when_command_exists {
20            if !cmds.iter().all(|c| command_exists(c)) {
21                continue;
22            }
23        }
24        return ExpandResult::Expanded(abbr.expand.clone());
25    }
26    ExpandResult::PassThrough(token.to_string())
27}
28
29/// List all abbreviations as (key, expand) pairs.
30pub fn list(config: &Config) -> Vec<(&str, &str)> {
31    config
32        .abbr
33        .iter()
34        .map(|a| (a.key.as_str(), a.expand.as_str()))
35        .collect()
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use crate::model::{Abbr, Config};
42
43    fn cfg(abbrs: Vec<Abbr>) -> Config {
44        Config {
45            version: 1,
46            keybind: crate::model::KeybindConfig::default(),
47            abbr: abbrs,
48        }
49    }
50
51    fn abbr(key: &str, expand: &str) -> Abbr {
52        Abbr {
53            key: key.into(),
54            expand: expand.into(),
55            when_command_exists: None,
56        }
57    }
58
59    fn abbr_when(key: &str, exp: &str, cmds: Vec<&str>) -> Abbr {
60        Abbr {
61            key: key.into(),
62            expand: exp.into(),
63            when_command_exists: Some(cmds.into_iter().map(String::from).collect()),
64        }
65    }
66
67    #[test]
68    fn match_expands() {
69        let c = cfg(vec![abbr("gcm", "git commit -m")]);
70        assert_eq!(
71            expand(&c, "gcm", |_| true),
72            ExpandResult::Expanded("git commit -m".into())
73        );
74    }
75
76    #[test]
77    fn no_match_passes_through() {
78        let c = cfg(vec![abbr("gcm", "git commit -m")]);
79        assert_eq!(
80            expand(&c, "xyz", |_| true),
81            ExpandResult::PassThrough("xyz".into())
82        );
83    }
84
85    #[test]
86    fn selects_correct_abbr() {
87        let c = cfg(vec![
88            abbr("gcm", "git commit -m"),
89            abbr("gp", "git push"),
90        ]);
91        assert_eq!(
92            expand(&c, "gp", |_| true),
93            ExpandResult::Expanded("git push".into())
94        );
95    }
96
97    #[test]
98    fn key_eq_expand_passes_through() {
99        let c = cfg(vec![abbr("ls", "ls")]);
100        assert_eq!(
101            expand(&c, "ls", |_| true),
102            ExpandResult::PassThrough("ls".into())
103        );
104    }
105
106    #[test]
107    fn when_command_exists_present() {
108        let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
109        assert_eq!(
110            expand(&c, "ls", |_| true),
111            ExpandResult::Expanded("lsd".into())
112        );
113    }
114
115    #[test]
116    fn when_command_exists_absent() {
117        let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
118        assert_eq!(
119            expand(&c, "ls", |_| false),
120            ExpandResult::PassThrough("ls".into())
121        );
122    }
123
124    #[test]
125    fn list_returns_all_pairs() {
126        let c = cfg(vec![
127            abbr("gcm", "git commit -m"),
128            abbr("gp", "git push"),
129        ]);
130        let pairs = list(&c);
131        assert_eq!(pairs, vec![("gcm", "git commit -m"), ("gp", "git push")]);
132    }
133}