Skip to main content

angular_switcher/
strategy.rs

1use crate::config::Config;
2use crate::error::SwitcherError;
3use crate::resolver::{sibling_for_target, CurrentMatch};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Mode {
8    Direct,
9    CycleForward,
10    CycleBackward,
11}
12
13pub fn select(
14    current: &CurrentMatch,
15    mode: Mode,
16    target: Option<&str>,
17    config: &Config,
18) -> Result<PathBuf, SwitcherError> {
19    match mode {
20        Mode::Direct => {
21            let target =
22                target.ok_or_else(|| SwitcherError::Input("--to requires a target name".into()))?;
23            if !config.targets.contains_key(target) {
24                return Err(SwitcherError::Input(format!(
25                    "unknown target '{target}'. Known targets: {}",
26                    config
27                        .targets
28                        .keys()
29                        .cloned()
30                        .collect::<Vec<_>>()
31                        .join(", ")
32                )));
33            }
34            sibling_for_target(current, target, config).ok_or_else(|| SwitcherError::NoSibling {
35                target: Some(target.to_string()),
36            })
37        }
38        Mode::CycleForward | Mode::CycleBackward => cycle(current, mode, config),
39    }
40}
41
42fn cycle(current: &CurrentMatch, mode: Mode, config: &Config) -> Result<PathBuf, SwitcherError> {
43    let order = &config.cycle.order;
44    let n: i64 = i64::try_from(order.len())
45        .map_err(|_| SwitcherError::Config("cycle.order is too large".into()))?;
46    let dir: i64 = if mode == Mode::CycleForward { 1 } else { -1 };
47    let start_idx = order.iter().position(|t| t == &current.target);
48
49    for step in 1..=n {
50        let raw: i64 = match start_idx {
51            Some(idx) => {
52                let idx_i = i64::try_from(idx).expect("position within Vec fits in i64");
53                idx_i + dir * step
54            }
55            None => {
56                if mode == Mode::CycleForward {
57                    step - 1
58                } else {
59                    n - step
60                }
61            }
62        };
63        let idx_i64: i64 = if config.cycle.wrap {
64            raw.rem_euclid(n)
65        } else if !(0..n).contains(&raw) {
66            continue;
67        } else {
68            raw
69        };
70        let idx = usize::try_from(idx_i64).expect("non-negative after rem_euclid / bounds check");
71        let candidate_target = &order[idx];
72        if !current.target.is_empty() && candidate_target == &current.target {
73            continue;
74        }
75        if let Some(path) = sibling_for_target(current, candidate_target, config) {
76            return Ok(path);
77        }
78        if !config.cycle.skip_missing {
79            return Err(SwitcherError::NoSibling {
80                target: Some(candidate_target.clone()),
81            });
82        }
83    }
84    Err(SwitcherError::NoSibling { target: None })
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::resolver::identify_current;
91    use std::fs;
92    use std::path::Path;
93    use tempfile::tempdir;
94
95    fn touch(dir: &Path, name: &str) -> PathBuf {
96        let p = dir.join(name);
97        fs::write(&p, "").unwrap();
98        p
99    }
100
101    #[test]
102    fn cycle_ts_to_html_when_all_exist() {
103        let dir = tempdir().unwrap();
104        let ts = touch(dir.path(), "foo.component.ts");
105        touch(dir.path(), "foo.component.html");
106        touch(dir.path(), "foo.component.scss");
107        touch(dir.path(), "foo.component.spec.ts");
108
109        let cfg = Config::default();
110        let cur = identify_current(&ts, &cfg).unwrap();
111        let next = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
112        assert_eq!(next.file_name().unwrap(), "foo.component.html");
113    }
114
115    #[test]
116    fn cycle_full_loop() {
117        let dir = tempdir().unwrap();
118        let ts = touch(dir.path(), "foo.component.ts");
119        touch(dir.path(), "foo.component.html");
120        touch(dir.path(), "foo.component.scss");
121        touch(dir.path(), "foo.component.spec.ts");
122        let cfg = Config::default();
123
124        let cur = identify_current(&ts, &cfg).unwrap();
125        let html = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
126        let cur2 = identify_current(&html, &cfg).unwrap();
127        let scss = select(&cur2, Mode::CycleForward, None, &cfg).unwrap();
128        let cur3 = identify_current(&scss, &cfg).unwrap();
129        let spec = select(&cur3, Mode::CycleForward, None, &cfg).unwrap();
130        let cur4 = identify_current(&spec, &cfg).unwrap();
131        let back_to_ts = select(&cur4, Mode::CycleForward, None, &cfg).unwrap();
132
133        assert_eq!(html.file_name().unwrap(), "foo.component.html");
134        assert_eq!(scss.file_name().unwrap(), "foo.component.scss");
135        assert_eq!(spec.file_name().unwrap(), "foo.component.spec.ts");
136        assert_eq!(back_to_ts.file_name().unwrap(), "foo.component.ts");
137    }
138
139    #[test]
140    fn cycle_skips_missing_sibling() {
141        let dir = tempdir().unwrap();
142        let ts = touch(dir.path(), "foo.component.ts");
143        // No html, no scss
144        touch(dir.path(), "foo.component.spec.ts");
145
146        let cfg = Config::default();
147        let cur = identify_current(&ts, &cfg).unwrap();
148        let next = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
149        assert_eq!(next.file_name().unwrap(), "foo.component.spec.ts");
150    }
151
152    #[test]
153    fn cycle_reverse_goes_backwards() {
154        let dir = tempdir().unwrap();
155        let ts = touch(dir.path(), "foo.component.ts");
156        touch(dir.path(), "foo.component.html");
157        touch(dir.path(), "foo.component.scss");
158        touch(dir.path(), "foo.component.spec.ts");
159
160        let cfg = Config::default();
161        let cur = identify_current(&ts, &cfg).unwrap();
162        let prev = select(&cur, Mode::CycleBackward, None, &cfg).unwrap();
163        assert_eq!(prev.file_name().unwrap(), "foo.component.spec.ts");
164    }
165
166    #[test]
167    fn direct_to_spec() {
168        let dir = tempdir().unwrap();
169        let ts = touch(dir.path(), "foo.component.ts");
170        touch(dir.path(), "foo.component.spec.ts");
171        let cfg = Config::default();
172        let cur = identify_current(&ts, &cfg).unwrap();
173        let spec = select(&cur, Mode::Direct, Some("spec"), &cfg).unwrap();
174        assert_eq!(spec.file_name().unwrap(), "foo.component.spec.ts");
175    }
176
177    #[test]
178    fn direct_unknown_target_errors() {
179        let dir = tempdir().unwrap();
180        let ts = touch(dir.path(), "foo.component.ts");
181        let cfg = Config::default();
182        let cur = identify_current(&ts, &cfg).unwrap();
183        let err = select(&cur, Mode::Direct, Some("nope"), &cfg).unwrap_err();
184        assert!(matches!(err, SwitcherError::Input(_)));
185    }
186
187    #[test]
188    fn direct_missing_sibling_errors() {
189        let dir = tempdir().unwrap();
190        let ts = touch(dir.path(), "foo.component.ts");
191        let cfg = Config::default();
192        let cur = identify_current(&ts, &cfg).unwrap();
193        let err = select(&cur, Mode::Direct, Some("spec"), &cfg).unwrap_err();
194        match err {
195            SwitcherError::NoSibling { target } => assert_eq!(target.as_deref(), Some("spec")),
196            other => panic!("unexpected: {other:?}"),
197        }
198    }
199
200    #[test]
201    fn style_preference_prefers_scss() {
202        let dir = tempdir().unwrap();
203        let ts = touch(dir.path(), "foo.component.ts");
204        touch(dir.path(), "foo.component.css");
205        touch(dir.path(), "foo.component.scss");
206        let cfg = Config::default();
207        let cur = identify_current(&ts, &cfg).unwrap();
208        let style = select(&cur, Mode::Direct, Some("style"), &cfg).unwrap();
209        assert_eq!(style.file_name().unwrap(), "foo.component.scss");
210    }
211
212    #[test]
213    fn no_siblings_returns_no_sibling_err() {
214        let dir = tempdir().unwrap();
215        let ts = touch(dir.path(), "lonely.component.ts");
216        let cfg = Config::default();
217        let cur = identify_current(&ts, &cfg).unwrap();
218        let err = select(&cur, Mode::CycleForward, None, &cfg).unwrap_err();
219        assert!(matches!(err, SwitcherError::NoSibling { .. }));
220    }
221}