1use crate::format::functions::call_function;
2use crate::format::parser::Token;
3
4pub trait MetadataProvider {
5 fn get_field(&self, name: &str) -> Option<String>;
6}
7
8impl<F: Fn(&str) -> Option<String>> MetadataProvider for F {
9 fn get_field(&self, name: &str) -> Option<String> {
10 self(name)
11 }
12}
13
14pub fn evaluate(tokens: &[Token], provider: &dyn MetadataProvider) -> String {
16 let (result, _) = eval_inner(tokens, provider);
17 result
18}
19
20fn eval_inner(tokens: &[Token], provider: &dyn MetadataProvider) -> (String, bool) {
22 let mut output = String::new();
23 let mut all_resolved = true;
24
25 for token in tokens {
26 match token {
27 Token::Literal(s) => output.push_str(s),
28 Token::Field(name) => match provider.get_field(name) {
29 Some(val) => output.push_str(&val),
30 None => {
31 all_resolved = false;
32 }
33 },
34 Token::Conditional(inner) => {
35 let (inner_output, inner_resolved) = eval_inner(inner, provider);
36 if inner_resolved {
37 output.push_str(&inner_output);
38 }
39 }
41 Token::Function { name, args } => {
42 let mut eval_args = Vec::new();
43 let mut func_resolved = true;
44 for arg_tokens in args {
45 let (arg_val, arg_ok) = eval_inner(arg_tokens, provider);
46 if !arg_ok {
47 func_resolved = false;
48 }
49 eval_args.push(arg_val);
50 }
51 if !func_resolved {
52 all_resolved = false;
53 }
54 match call_function(name, &eval_args) {
57 Some(result) => output.push_str(&result),
58 None => all_resolved = false,
59 }
60 }
61 }
62 }
63
64 (output, all_resolved)
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use crate::format::parse;
71 use std::collections::HashMap;
72
73 #[test]
76 fn unresolvable_function_suppresses_its_conditional() {
77 let provider = make_provider(&[("artist", "Radiohead"), ("title", "Airbag")]);
78 let tokens = parse("[%artist% - $left(%title%)]").unwrap();
80 assert_eq!(evaluate(&tokens, &provider), "");
81
82 let tokens = parse("[%artist% - $left(%title%,3)]").unwrap();
83 assert_eq!(evaluate(&tokens, &provider), "Radiohead - Air");
84 }
85
86 fn make_provider(data: &[(&str, &str)]) -> HashMap<String, String> {
87 data.iter()
88 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
89 .collect()
90 }
91
92 impl MetadataProvider for HashMap<String, String> {
93 fn get_field(&self, name: &str) -> Option<String> {
94 self.get(name).cloned()
95 }
96 }
97
98 #[test]
99 fn basic_field() {
100 let provider = make_provider(&[("title", "Song")]);
101 let tokens = vec![Token::Field("title".into())];
102 assert_eq!(evaluate(&tokens, &provider), "Song");
103 }
104
105 #[test]
106 fn missing_field() {
107 let provider = make_provider(&[]);
108 let tokens = vec![Token::Field("title".into())];
109 assert_eq!(evaluate(&tokens, &provider), "");
110 }
111
112 #[test]
113 fn conditional_present() {
114 let provider = make_provider(&[("artist", "Radiohead")]);
115 let tokens = vec![
116 Token::Conditional(vec![
117 Token::Field("artist".into()),
118 Token::Literal(" - ".into()),
119 ]),
120 Token::Literal("OK Computer".into()),
121 ];
122 assert_eq!(evaluate(&tokens, &provider), "Radiohead - OK Computer");
123 }
124
125 #[test]
126 fn conditional_missing() {
127 let provider = make_provider(&[]);
128 let tokens = vec![
129 Token::Conditional(vec![
130 Token::Field("artist".into()),
131 Token::Literal(" - ".into()),
132 ]),
133 Token::Literal("OK Computer".into()),
134 ];
135 assert_eq!(evaluate(&tokens, &provider), "OK Computer");
136 }
137
138 #[test]
139 fn nested_conditional() {
140 let provider = make_provider(&[("artist", "Radiohead"), ("date", "1997")]);
141 let tokens = vec![Token::Conditional(vec![
142 Token::Field("artist".into()),
143 Token::Conditional(vec![
144 Token::Literal(" (".into()),
145 Token::Field("date".into()),
146 Token::Literal(")".into()),
147 ]),
148 ])];
149 assert_eq!(evaluate(&tokens, &provider), "Radiohead (1997)");
150 }
151
152 #[test]
153 fn nested_conditional_inner_missing() {
154 let provider = make_provider(&[("artist", "Radiohead")]);
155 let tokens = vec![Token::Conditional(vec![
156 Token::Field("artist".into()),
157 Token::Conditional(vec![
158 Token::Literal(" (".into()),
159 Token::Field("date".into()),
160 Token::Literal(")".into()),
161 ]),
162 ])];
163 assert_eq!(evaluate(&tokens, &provider), "Radiohead");
165 }
166
167 #[test]
168 fn function_in_conditional() {
169 let provider = make_provider(&[("genre", "Rock")]);
170 let tokens = vec![Token::Conditional(vec![Token::Function {
171 name: "if".into(),
172 args: vec![
173 vec![Token::Field("genre".into())],
174 vec![Token::Field("genre".into())],
175 vec![Token::Literal("Unknown".into())],
176 ],
177 }])];
178 assert_eq!(evaluate(&tokens, &provider), "Rock");
179 }
180
181 #[test]
182 fn function_in_conditional_field_missing() {
183 let provider = make_provider(&[]);
184 let tokens = vec![Token::Conditional(vec![Token::Function {
185 name: "if".into(),
186 args: vec![
187 vec![Token::Field("genre".into())],
188 vec![Token::Field("genre".into())],
189 vec![Token::Literal("Unknown".into())],
190 ],
191 }])];
192 assert_eq!(evaluate(&tokens, &provider), "");
194 }
195
196 #[test]
197 fn literal_passthrough() {
198 let provider = make_provider(&[]);
199 let tokens = vec![Token::Literal("hello".into())];
200 assert_eq!(evaluate(&tokens, &provider), "hello");
201 }
202
203 #[test]
204 fn multiple_fields() {
205 let provider = make_provider(&[("artist", "Radiohead"), ("title", "Creep")]);
206 let tokens = vec![
207 Token::Field("artist".into()),
208 Token::Literal(" - ".into()),
209 Token::Field("title".into()),
210 ];
211 assert_eq!(evaluate(&tokens, &provider), "Radiohead - Creep");
212 }
213
214 #[test]
215 fn missing_field_produces_empty() {
216 let provider = make_provider(&[("title", "Creep")]);
217 let tokens = vec![
218 Token::Field("artist".into()),
219 Token::Literal(" - ".into()),
220 Token::Field("title".into()),
221 ];
222 assert_eq!(evaluate(&tokens, &provider), " - Creep");
224 }
225
226 #[test]
227 fn conditional_does_not_poison_parent() {
228 let provider = make_provider(&[("title", "Creep")]);
229 let tokens = vec![
230 Token::Conditional(vec![
231 Token::Field("artist".into()),
232 Token::Literal(" - ".into()),
233 ]),
234 Token::Field("title".into()),
235 ];
236 assert_eq!(evaluate(&tokens, &provider), "Creep");
238 }
239
240 #[test]
241 fn multiple_failed_conditionals() {
242 let provider = make_provider(&[("title", "Creep")]);
243 let tokens = vec![
244 Token::Conditional(vec![Token::Field("artist".into())]),
245 Token::Conditional(vec![Token::Field("album".into())]),
246 Token::Conditional(vec![Token::Field("date".into())]),
247 Token::Field("title".into()),
248 ];
249 assert_eq!(evaluate(&tokens, &provider), "Creep");
250 }
251
252 #[test]
253 fn deeply_nested_conditionals() {
254 let provider = make_provider(&[("a", "1"), ("b", "2"), ("c", "3")]);
255 let tokens = vec![Token::Conditional(vec![
256 Token::Field("a".into()),
257 Token::Conditional(vec![
258 Token::Field("b".into()),
259 Token::Conditional(vec![Token::Field("c".into())]),
260 ]),
261 ])];
262 assert_eq!(evaluate(&tokens, &provider), "123");
263 }
264
265 #[test]
266 fn function_with_empty_arg() {
267 let provider = make_provider(&[]);
268 let tokens = vec![Token::Function {
270 name: "if".into(),
271 args: vec![
272 vec![Token::Literal("truthy".into())],
273 vec![], vec![Token::Literal("else".into())],
275 ],
276 }];
277 assert_eq!(evaluate(&tokens, &provider), "");
279 }
280
281 #[test]
282 fn stricmp_with_if_empty_then() {
283 let provider = make_provider(&[("x", "hello")]);
284 let tokens = vec![Token::Function {
286 name: "if".into(),
287 args: vec![
288 vec![Token::Function {
289 name: "stricmp".into(),
290 args: vec![
291 vec![Token::Field("x".into())],
292 vec![Token::Literal("hello".into())],
293 ],
294 }],
295 vec![], vec![Token::Literal("not hello".into())],
297 ],
298 }];
299 assert_eq!(evaluate(&tokens, &provider), "");
301 }
302
303 #[test]
304 fn stricmp_with_if_else() {
305 let provider = make_provider(&[("x", "world")]);
306 let tokens = vec![Token::Function {
307 name: "if".into(),
308 args: vec![
309 vec![Token::Function {
310 name: "stricmp".into(),
311 args: vec![
312 vec![Token::Field("x".into())],
313 vec![Token::Literal("hello".into())],
314 ],
315 }],
316 vec![],
317 vec![Token::Literal("not hello".into())],
318 ],
319 }];
320 assert_eq!(evaluate(&tokens, &provider), "not hello");
322 }
323
324 #[test]
325 fn conditional_with_only_literals_always_renders() {
326 let provider = make_provider(&[]);
327 let tokens = vec![Token::Conditional(vec![Token::Literal("always".into())])];
328 assert_eq!(evaluate(&tokens, &provider), "always");
330 }
331
332 #[test]
333 fn conditional_with_function_returning_empty() {
334 let provider = make_provider(&[("x", "nope")]);
335 let tokens = vec![Token::Conditional(vec![Token::Function {
336 name: "stricmp".into(),
337 args: vec![
338 vec![Token::Field("x".into())],
339 vec![Token::Literal("hello".into())],
340 ],
341 }])];
342 assert_eq!(evaluate(&tokens, &provider), "");
344 }
345}