Skip to main content

angular_switcher/
resolver.rs

1use crate::config::Config;
2use crate::error::SwitcherError;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct CurrentMatch {
7    /// Target name from config, or empty string if the file did not match any target
8    /// and we fell back to the filename stem.
9    pub target: String,
10    pub basename: String,
11    pub parent: PathBuf,
12    pub original: PathBuf,
13}
14
15pub fn identify_current(path: &Path, config: &Config) -> Result<CurrentMatch, SwitcherError> {
16    let file_name = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
17        SwitcherError::Input(format!("path has no UTF-8 filename: {}", path.display()))
18    })?;
19    let parent = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
20
21    let mut best: Option<(usize, String, String)> = None;
22    for (target_name, target) in &config.targets {
23        for ext in &target.extensions {
24            let suffix = format!(".{ext}");
25            if !file_name.ends_with(&suffix) {
26                continue;
27            }
28            let excluded = target
29                .exclude_suffixes
30                .iter()
31                .any(|sfx| file_name.ends_with(&format!(".{sfx}")));
32            if excluded {
33                continue;
34            }
35            let basename = &file_name[..file_name.len() - suffix.len()];
36            let matched_len = ext.len();
37            if best.as_ref().map_or(true, |b| matched_len > b.0) {
38                best = Some((matched_len, target_name.clone(), basename.to_string()));
39            }
40        }
41    }
42
43    if let Some((_, target, basename)) = best {
44        return Ok(CurrentMatch {
45            target,
46            basename,
47            parent,
48            original: path.to_path_buf(),
49        });
50    }
51
52    if config.naming.fallback_to_stem {
53        let stem = Path::new(file_name)
54            .file_stem()
55            .and_then(|s| s.to_str())
56            .unwrap_or(file_name)
57            .to_string();
58        return Ok(CurrentMatch {
59            target: String::new(),
60            basename: stem,
61            parent,
62            original: path.to_path_buf(),
63        });
64    }
65
66    Err(SwitcherError::Input(format!(
67        "could not identify an Angular target for {}",
68        path.display()
69    )))
70}
71
72pub fn sibling_for_target(
73    base: &CurrentMatch,
74    target_name: &str,
75    config: &Config,
76) -> Option<PathBuf> {
77    let target = config.targets.get(target_name)?;
78    let ordered_exts: Vec<&String> = if target.preference.is_empty() {
79        target.extensions.iter().collect()
80    } else {
81        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
82        let mut out: Vec<&String> = Vec::new();
83        for ext in &target.preference {
84            if target.extensions.contains(ext) && seen.insert(ext.as_str()) {
85                out.push(ext);
86            }
87        }
88        for ext in &target.extensions {
89            if seen.insert(ext.as_str()) {
90                out.push(ext);
91            }
92        }
93        out
94    };
95
96    for ext in ordered_exts {
97        let candidate = base.parent.join(format!("{}.{}", base.basename, ext));
98        if candidate == base.original {
99            continue;
100        }
101        if candidate.is_file() {
102            return Some(candidate);
103        }
104    }
105    None
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::path::Path;
112
113    fn cfg() -> Config {
114        Config::default()
115    }
116
117    #[test]
118    fn identifies_ts() {
119        let m = identify_current(Path::new("app/foo.component.ts"), &cfg()).unwrap();
120        assert_eq!(m.target, "ts");
121        assert_eq!(m.basename, "foo.component");
122        assert_eq!(m.parent, PathBuf::from("app"));
123    }
124
125    #[test]
126    fn identifies_spec_not_ts() {
127        let m = identify_current(Path::new("app/foo.component.spec.ts"), &cfg()).unwrap();
128        assert_eq!(m.target, "spec");
129        assert_eq!(m.basename, "foo.component");
130    }
131
132    #[test]
133    fn identifies_html() {
134        let m = identify_current(Path::new("app/foo.component.html"), &cfg()).unwrap();
135        assert_eq!(m.target, "html");
136        assert_eq!(m.basename, "foo.component");
137    }
138
139    #[test]
140    fn identifies_scss_as_style() {
141        let m = identify_current(Path::new("app/foo.component.scss"), &cfg()).unwrap();
142        assert_eq!(m.target, "style");
143        assert_eq!(m.basename, "foo.component");
144    }
145
146    #[test]
147    fn falls_back_to_stem_for_unknown() {
148        let m = identify_current(Path::new("README.md"), &cfg()).unwrap();
149        assert_eq!(m.target, "");
150        assert_eq!(m.basename, "README");
151    }
152}