use crate::config::Config;
use crate::error::SwitcherError;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CurrentMatch {
pub target: String,
pub basename: String,
pub parent: PathBuf,
pub original: PathBuf,
}
pub fn identify_current(path: &Path, config: &Config) -> Result<CurrentMatch, SwitcherError> {
let file_name = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
SwitcherError::Input(format!("path has no UTF-8 filename: {}", path.display()))
})?;
let parent = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
let mut best: Option<(usize, String, String)> = None;
for (target_name, target) in &config.targets {
for ext in &target.extensions {
let suffix = format!(".{ext}");
if !file_name.ends_with(&suffix) {
continue;
}
let excluded = target
.exclude_suffixes
.iter()
.any(|sfx| file_name.ends_with(&format!(".{sfx}")));
if excluded {
continue;
}
let basename = &file_name[..file_name.len() - suffix.len()];
let matched_len = ext.len();
if best.as_ref().map_or(true, |b| matched_len > b.0) {
best = Some((matched_len, target_name.clone(), basename.to_string()));
}
}
}
if let Some((_, target, basename)) = best {
return Ok(CurrentMatch {
target,
basename,
parent,
original: path.to_path_buf(),
});
}
if config.naming.fallback_to_stem {
let stem = Path::new(file_name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(file_name)
.to_string();
return Ok(CurrentMatch {
target: String::new(),
basename: stem,
parent,
original: path.to_path_buf(),
});
}
Err(SwitcherError::Input(format!(
"could not identify an Angular target for {}",
path.display()
)))
}
pub fn sibling_for_target(
base: &CurrentMatch,
target_name: &str,
config: &Config,
) -> Option<PathBuf> {
let target = config.targets.get(target_name)?;
let ordered_exts: Vec<&String> = if target.preference.is_empty() {
target.extensions.iter().collect()
} else {
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut out: Vec<&String> = Vec::new();
for ext in &target.preference {
if target.extensions.contains(ext) && seen.insert(ext.as_str()) {
out.push(ext);
}
}
for ext in &target.extensions {
if seen.insert(ext.as_str()) {
out.push(ext);
}
}
out
};
for ext in ordered_exts {
let candidate = base.parent.join(format!("{}.{}", base.basename, ext));
if candidate == base.original {
continue;
}
if candidate.is_file() {
return Some(candidate);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn cfg() -> Config {
Config::default()
}
#[test]
fn identifies_ts() {
let m = identify_current(Path::new("app/foo.component.ts"), &cfg()).unwrap();
assert_eq!(m.target, "ts");
assert_eq!(m.basename, "foo.component");
assert_eq!(m.parent, PathBuf::from("app"));
}
#[test]
fn identifies_spec_not_ts() {
let m = identify_current(Path::new("app/foo.component.spec.ts"), &cfg()).unwrap();
assert_eq!(m.target, "spec");
assert_eq!(m.basename, "foo.component");
}
#[test]
fn identifies_html() {
let m = identify_current(Path::new("app/foo.component.html"), &cfg()).unwrap();
assert_eq!(m.target, "html");
assert_eq!(m.basename, "foo.component");
}
#[test]
fn identifies_scss_as_style() {
let m = identify_current(Path::new("app/foo.component.scss"), &cfg()).unwrap();
assert_eq!(m.target, "style");
assert_eq!(m.basename, "foo.component");
}
#[test]
fn falls_back_to_stem_for_unknown() {
let m = identify_current(Path::new("README.md"), &cfg()).unwrap();
assert_eq!(m.target, "");
assert_eq!(m.basename, "README");
}
}