Skip to main content

koan_core/format/
eval.rs

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
14/// Evaluate tokens against a metadata provider.
15pub fn evaluate(tokens: &[Token], provider: &dyn MetadataProvider) -> String {
16    let (result, _) = eval_inner(tokens, provider);
17    result
18}
19
20/// Returns (output_string, all_fields_resolved).
21fn 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                // A failed conditional doesn't poison the parent — it just produces ""
40            }
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                // A function that can't produce a value (wrong arity, out-of-range
55                // argument) leaves the expression unresolved rather than vanishing.
56                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    /// A function that can't produce a value leaves its conditional unresolved,
74    /// rather than emitting the surrounding literals with a hole where it should be.
75    #[test]
76    fn unresolvable_function_suppresses_its_conditional() {
77        let provider = make_provider(&[("artist", "Radiohead"), ("title", "Airbag")]);
78        // $left needs two arguments.
79        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        // Inner conditional fails (date missing), outer still works (artist present)
164        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        // genre is missing, so the conditional fails entirely
193        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        // Missing field produces empty string inline (not inside conditional)
223        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        // Failed conditional produces "", rest of output is fine
237        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        // Simulate $if(x,,y) where arg 1 is empty
269        let tokens = vec![Token::Function {
270            name: "if".into(),
271            args: vec![
272                vec![Token::Literal("truthy".into())],
273                vec![], // empty then branch
274                vec![Token::Literal("else".into())],
275            ],
276        }];
277        // "truthy" is non-empty → takes then branch → ""
278        assert_eq!(evaluate(&tokens, &provider), "");
279    }
280
281    #[test]
282    fn stricmp_with_if_empty_then() {
283        let provider = make_provider(&[("x", "hello")]);
284        // $if($stricmp(%x%,hello),,not hello)
285        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![], // empty then
296                vec![Token::Literal("not hello".into())],
297            ],
298        }];
299        // stricmp("hello", "hello") → "1" (truthy) → takes empty then → ""
300        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        // stricmp("world", "hello") → "" (falsy) → takes else → "not hello"
321        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        // No fields to fail → all_resolved stays true → renders
329        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        // stricmp returns "" but all fields resolved → conditional renders ""
343        assert_eq!(evaluate(&tokens, &provider), "");
344    }
345}