angular_switcher/
resolver.rs1use crate::config::Config;
2use crate::error::SwitcherError;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct CurrentMatch {
7 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}