1use std::path::Path;
16
17use crate::config::PathsSpec;
18
19#[derive(Debug, Clone)]
22pub struct PathTokens {
23 pub path: String,
24 pub dir: String,
25 pub basename: String,
26 pub stem: String,
27 pub ext: String,
28 pub parent_name: String,
29}
30
31impl PathTokens {
32 pub fn from_path(rel: &Path) -> Self {
35 Self {
36 path: rel.display().to_string(),
37 dir: rel
38 .parent()
39 .map(|p| p.display().to_string())
40 .unwrap_or_default(),
41 basename: rel
42 .file_name()
43 .and_then(|s| s.to_str())
44 .unwrap_or_default()
45 .to_string(),
46 stem: rel
47 .file_stem()
48 .and_then(|s| s.to_str())
49 .unwrap_or_default()
50 .to_string(),
51 ext: rel
52 .extension()
53 .and_then(|s| s.to_str())
54 .unwrap_or_default()
55 .to_string(),
56 parent_name: rel
57 .parent()
58 .and_then(|p| p.file_name())
59 .and_then(|s| s.to_str())
60 .unwrap_or_default()
61 .to_string(),
62 }
63 }
64}
65
66pub fn render_path(template: &str, t: &PathTokens) -> String {
72 let mut out = template.to_string();
73 out = out.replace("{parent_name}", &t.parent_name);
75 out = out.replace("{basename}", &t.basename);
76 out = out.replace("{path}", &t.path);
77 out = out.replace("{stem}", &t.stem);
78 out = out.replace("{dir}", &t.dir);
79 out = out.replace("{ext}", &t.ext);
80 out
81}
82
83pub fn render_mapping(m: serde_yaml_ng::Mapping, tokens: &PathTokens) -> serde_yaml_ng::Mapping {
95 let mut out = serde_yaml_ng::Mapping::with_capacity(m.len());
96 for (k, v) in m {
97 out.insert(k, render_value(v, tokens));
98 }
99 out
100}
101
102pub fn render_value(v: serde_yaml_ng::Value, tokens: &PathTokens) -> serde_yaml_ng::Value {
104 use serde_yaml_ng::Value;
105 match v {
106 Value::String(s) => Value::String(render_path(&s, tokens)),
107 Value::Sequence(seq) => {
108 Value::Sequence(seq.into_iter().map(|e| render_value(e, tokens)).collect())
109 }
110 Value::Mapping(m) => Value::Mapping(render_mapping(m, tokens)),
111 other => other,
112 }
113}
114
115pub fn render_paths_spec(spec: &PathsSpec, tokens: &PathTokens) -> PathsSpec {
117 match spec {
118 PathsSpec::Single(s) => PathsSpec::Single(render_path(s, tokens)),
119 PathsSpec::Many(v) => PathsSpec::Many(v.iter().map(|s| render_path(s, tokens)).collect()),
120 PathsSpec::IncludeExclude { include, exclude } => PathsSpec::IncludeExclude {
121 include: include.iter().map(|s| render_path(s, tokens)).collect(),
122 exclude: exclude.iter().map(|s| render_path(s, tokens)).collect(),
123 },
124 }
125}
126
127pub fn render_message<F>(template: &str, resolve: F) -> String
128where
129 F: Fn(&str, &str) -> Option<String>,
130{
131 let mut out = String::with_capacity(template.len());
132 let mut rest = template;
133 while let Some(start) = rest.find("{{") {
134 out.push_str(&rest[..start]);
135 let after = &rest[start + 2..];
136 let Some(end) = after.find("}}") else {
137 out.push_str(&rest[start..]);
139 return out;
140 };
141 let inner = after[..end].trim();
142 let rendered = inner
143 .split_once('.')
144 .and_then(|(ns, key)| resolve(ns.trim(), key.trim()));
145 if let Some(val) = rendered {
146 out.push_str(&val);
147 } else {
148 out.push_str("{{");
149 out.push_str(&after[..end]);
150 out.push_str("}}");
151 }
152 rest = &after[end + 2..];
153 }
154 out.push_str(rest);
155 out
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use std::path::Path;
162
163 #[test]
164 fn path_tokens_basic_rs_file() {
165 let t = PathTokens::from_path(Path::new("crates/alint-core/src/lib.rs"));
166 assert_eq!(t.path, "crates/alint-core/src/lib.rs");
167 assert_eq!(t.dir, "crates/alint-core/src");
168 assert_eq!(t.basename, "lib.rs");
169 assert_eq!(t.stem, "lib");
170 assert_eq!(t.ext, "rs");
171 assert_eq!(t.parent_name, "src");
172 }
173
174 #[test]
175 fn path_tokens_root_file() {
176 let t = PathTokens::from_path(Path::new("README.md"));
177 assert_eq!(t.path, "README.md");
178 assert_eq!(t.dir, "");
179 assert_eq!(t.basename, "README.md");
180 assert_eq!(t.stem, "README");
181 assert_eq!(t.ext, "md");
182 assert_eq!(t.parent_name, "");
183 }
184
185 #[test]
186 fn render_path_c_to_h() {
187 let t = PathTokens::from_path(Path::new("src/mod/foo.c"));
188 assert_eq!(render_path("{dir}/{stem}.h", &t), "src/mod/foo.h");
189 }
190
191 #[test]
192 fn render_path_unknown_token_preserved() {
193 let t = PathTokens::from_path(Path::new("a.c"));
194 assert_eq!(render_path("{bogus}/{stem}.x", &t), "{bogus}/a.x");
195 }
196
197 #[test]
198 fn render_message_simple() {
199 let out = render_message("{{ctx.primary}} → {{ctx.partner}}", |ns, key| {
200 match (ns, key) {
201 ("ctx", "primary") => Some("a.c".into()),
202 ("ctx", "partner") => Some("a.h".into()),
203 _ => None,
204 }
205 });
206 assert_eq!(out, "a.c → a.h");
207 }
208
209 #[test]
210 fn render_message_ignores_inner_whitespace() {
211 let out = render_message("[{{ ctx . primary }}]", |ns, key| {
212 if ns == "ctx" && key == "primary" {
213 Some("x".into())
214 } else {
215 None
216 }
217 });
218 assert_eq!(out, "[x]");
219 }
220
221 #[test]
222 fn render_message_unknown_key_preserved() {
223 let out = render_message("{{ctx.unknown}}", |_, _| None);
224 assert_eq!(out, "{{ctx.unknown}}");
225 }
226
227 #[test]
228 fn render_message_unterminated_is_preserved() {
229 let out = render_message("before {{ctx.primary", |_, _| Some("X".into()));
230 assert_eq!(out, "before {{ctx.primary");
231 }
232
233 #[test]
234 fn render_message_no_placeholders() {
235 let out = render_message("plain text", |_, _| Some("never".into()));
236 assert_eq!(out, "plain text");
237 }
238}