use std::path::Path;
use crate::config::PathsSpec;
#[derive(Debug, Clone)]
pub struct PathTokens {
pub path: String,
pub dir: String,
pub basename: String,
pub stem: String,
pub ext: String,
pub parent_name: String,
}
impl PathTokens {
pub fn from_path(rel: &Path) -> Self {
Self {
path: rel.display().to_string(),
dir: rel
.parent()
.map(|p| p.display().to_string())
.unwrap_or_default(),
basename: rel
.file_name()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string(),
stem: rel
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string(),
ext: rel
.extension()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string(),
parent_name: rel
.parent()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string(),
}
}
}
pub fn render_path(template: &str, t: &PathTokens) -> String {
let mut out = template.to_string();
out = out.replace("{parent_name}", &t.parent_name);
out = out.replace("{basename}", &t.basename);
out = out.replace("{path}", &t.path);
out = out.replace("{stem}", &t.stem);
out = out.replace("{dir}", &t.dir);
out = out.replace("{ext}", &t.ext);
out
}
pub fn render_mapping(m: serde_yaml_ng::Mapping, tokens: &PathTokens) -> serde_yaml_ng::Mapping {
let mut out = serde_yaml_ng::Mapping::with_capacity(m.len());
for (k, v) in m {
out.insert(k, render_value(v, tokens));
}
out
}
pub fn render_value(v: serde_yaml_ng::Value, tokens: &PathTokens) -> serde_yaml_ng::Value {
use serde_yaml_ng::Value;
match v {
Value::String(s) => Value::String(render_path(&s, tokens)),
Value::Sequence(seq) => {
Value::Sequence(seq.into_iter().map(|e| render_value(e, tokens)).collect())
}
Value::Mapping(m) => Value::Mapping(render_mapping(m, tokens)),
other => other,
}
}
pub fn render_paths_spec(spec: &PathsSpec, tokens: &PathTokens) -> PathsSpec {
match spec {
PathsSpec::Single(s) => PathsSpec::Single(render_path(s, tokens)),
PathsSpec::Many(v) => PathsSpec::Many(v.iter().map(|s| render_path(s, tokens)).collect()),
PathsSpec::IncludeExclude { include, exclude } => PathsSpec::IncludeExclude {
include: include.iter().map(|s| render_path(s, tokens)).collect(),
exclude: exclude.iter().map(|s| render_path(s, tokens)).collect(),
},
}
}
pub fn render_message<F>(template: &str, resolve: F) -> String
where
F: Fn(&str, &str) -> Option<String>,
{
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(start) = rest.find("{{") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[start..]);
return out;
};
let inner = after[..end].trim();
let rendered = inner
.split_once('.')
.and_then(|(ns, key)| resolve(ns.trim(), key.trim()));
if let Some(val) = rendered {
out.push_str(&val);
} else {
out.push_str("{{");
out.push_str(&after[..end]);
out.push_str("}}");
}
rest = &after[end + 2..];
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn path_tokens_basic_rs_file() {
let t = PathTokens::from_path(Path::new("crates/alint-core/src/lib.rs"));
assert_eq!(t.path, "crates/alint-core/src/lib.rs");
assert_eq!(t.dir, "crates/alint-core/src");
assert_eq!(t.basename, "lib.rs");
assert_eq!(t.stem, "lib");
assert_eq!(t.ext, "rs");
assert_eq!(t.parent_name, "src");
}
#[test]
fn path_tokens_root_file() {
let t = PathTokens::from_path(Path::new("README.md"));
assert_eq!(t.path, "README.md");
assert_eq!(t.dir, "");
assert_eq!(t.basename, "README.md");
assert_eq!(t.stem, "README");
assert_eq!(t.ext, "md");
assert_eq!(t.parent_name, "");
}
#[test]
fn render_path_c_to_h() {
let t = PathTokens::from_path(Path::new("src/mod/foo.c"));
assert_eq!(render_path("{dir}/{stem}.h", &t), "src/mod/foo.h");
}
#[test]
fn render_path_unknown_token_preserved() {
let t = PathTokens::from_path(Path::new("a.c"));
assert_eq!(render_path("{bogus}/{stem}.x", &t), "{bogus}/a.x");
}
#[test]
fn render_message_simple() {
let out = render_message("{{ctx.primary}} → {{ctx.partner}}", |ns, key| {
match (ns, key) {
("ctx", "primary") => Some("a.c".into()),
("ctx", "partner") => Some("a.h".into()),
_ => None,
}
});
assert_eq!(out, "a.c → a.h");
}
#[test]
fn render_message_ignores_inner_whitespace() {
let out = render_message("[{{ ctx . primary }}]", |ns, key| {
if ns == "ctx" && key == "primary" {
Some("x".into())
} else {
None
}
});
assert_eq!(out, "[x]");
}
#[test]
fn render_message_unknown_key_preserved() {
let out = render_message("{{ctx.unknown}}", |_, _| None);
assert_eq!(out, "{{ctx.unknown}}");
}
#[test]
fn render_message_unterminated_is_preserved() {
let out = render_message("before {{ctx.primary", |_, _| Some("X".into()));
assert_eq!(out, "before {{ctx.primary");
}
#[test]
fn render_message_no_placeholders() {
let out = render_message("plain text", |_, _| Some("never".into()));
assert_eq!(out, "plain text");
}
}