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                if let Some(result) = call_function(name, &eval_args) {
55                    output.push_str(&result);
56                }
57            }
58        }
59    }
60
61    (output, all_resolved)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::collections::HashMap;
68
69    fn make_provider(data: &[(&str, &str)]) -> HashMap<String, String> {
70        data.iter()
71            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
72            .collect()
73    }
74
75    impl MetadataProvider for HashMap<String, String> {
76        fn get_field(&self, name: &str) -> Option<String> {
77            self.get(name).cloned()
78        }
79    }
80
81    #[test]
82    fn basic_field() {
83        let provider = make_provider(&[("title", "Song")]);
84        let tokens = vec![Token::Field("title".into())];
85        assert_eq!(evaluate(&tokens, &provider), "Song");
86    }
87
88    #[test]
89    fn missing_field() {
90        let provider = make_provider(&[]);
91        let tokens = vec![Token::Field("title".into())];
92        assert_eq!(evaluate(&tokens, &provider), "");
93    }
94
95    #[test]
96    fn conditional_present() {
97        let provider = make_provider(&[("artist", "Radiohead")]);
98        let tokens = vec![
99            Token::Conditional(vec![
100                Token::Field("artist".into()),
101                Token::Literal(" - ".into()),
102            ]),
103            Token::Literal("OK Computer".into()),
104        ];
105        assert_eq!(evaluate(&tokens, &provider), "Radiohead - OK Computer");
106    }
107
108    #[test]
109    fn conditional_missing() {
110        let provider = make_provider(&[]);
111        let tokens = vec![
112            Token::Conditional(vec![
113                Token::Field("artist".into()),
114                Token::Literal(" - ".into()),
115            ]),
116            Token::Literal("OK Computer".into()),
117        ];
118        assert_eq!(evaluate(&tokens, &provider), "OK Computer");
119    }
120
121    #[test]
122    fn nested_conditional() {
123        let provider = make_provider(&[("artist", "Radiohead"), ("date", "1997")]);
124        let tokens = vec![Token::Conditional(vec![
125            Token::Field("artist".into()),
126            Token::Conditional(vec![
127                Token::Literal(" (".into()),
128                Token::Field("date".into()),
129                Token::Literal(")".into()),
130            ]),
131        ])];
132        assert_eq!(evaluate(&tokens, &provider), "Radiohead (1997)");
133    }
134
135    #[test]
136    fn nested_conditional_inner_missing() {
137        let provider = make_provider(&[("artist", "Radiohead")]);
138        let tokens = vec![Token::Conditional(vec![
139            Token::Field("artist".into()),
140            Token::Conditional(vec![
141                Token::Literal(" (".into()),
142                Token::Field("date".into()),
143                Token::Literal(")".into()),
144            ]),
145        ])];
146        // Inner conditional fails (date missing), outer still works (artist present)
147        assert_eq!(evaluate(&tokens, &provider), "Radiohead");
148    }
149
150    #[test]
151    fn function_in_conditional() {
152        let provider = make_provider(&[("genre", "Rock")]);
153        let tokens = vec![Token::Conditional(vec![Token::Function {
154            name: "if".into(),
155            args: vec![
156                vec![Token::Field("genre".into())],
157                vec![Token::Field("genre".into())],
158                vec![Token::Literal("Unknown".into())],
159            ],
160        }])];
161        assert_eq!(evaluate(&tokens, &provider), "Rock");
162    }
163
164    #[test]
165    fn function_in_conditional_field_missing() {
166        let provider = make_provider(&[]);
167        let tokens = vec![Token::Conditional(vec![Token::Function {
168            name: "if".into(),
169            args: vec![
170                vec![Token::Field("genre".into())],
171                vec![Token::Field("genre".into())],
172                vec![Token::Literal("Unknown".into())],
173            ],
174        }])];
175        // genre is missing, so the conditional fails entirely
176        assert_eq!(evaluate(&tokens, &provider), "");
177    }
178
179    #[test]
180    fn literal_passthrough() {
181        let provider = make_provider(&[]);
182        let tokens = vec![Token::Literal("hello".into())];
183        assert_eq!(evaluate(&tokens, &provider), "hello");
184    }
185
186    #[test]
187    fn multiple_fields() {
188        let provider = make_provider(&[("artist", "Radiohead"), ("title", "Creep")]);
189        let tokens = vec![
190            Token::Field("artist".into()),
191            Token::Literal(" - ".into()),
192            Token::Field("title".into()),
193        ];
194        assert_eq!(evaluate(&tokens, &provider), "Radiohead - Creep");
195    }
196
197    #[test]
198    fn missing_field_produces_empty() {
199        let provider = make_provider(&[("title", "Creep")]);
200        let tokens = vec![
201            Token::Field("artist".into()),
202            Token::Literal(" - ".into()),
203            Token::Field("title".into()),
204        ];
205        // Missing field produces empty string inline (not inside conditional)
206        assert_eq!(evaluate(&tokens, &provider), " - Creep");
207    }
208
209    #[test]
210    fn conditional_does_not_poison_parent() {
211        let provider = make_provider(&[("title", "Creep")]);
212        let tokens = vec![
213            Token::Conditional(vec![
214                Token::Field("artist".into()),
215                Token::Literal(" - ".into()),
216            ]),
217            Token::Field("title".into()),
218        ];
219        // Failed conditional produces "", rest of output is fine
220        assert_eq!(evaluate(&tokens, &provider), "Creep");
221    }
222
223    #[test]
224    fn multiple_failed_conditionals() {
225        let provider = make_provider(&[("title", "Creep")]);
226        let tokens = vec![
227            Token::Conditional(vec![Token::Field("artist".into())]),
228            Token::Conditional(vec![Token::Field("album".into())]),
229            Token::Conditional(vec![Token::Field("date".into())]),
230            Token::Field("title".into()),
231        ];
232        assert_eq!(evaluate(&tokens, &provider), "Creep");
233    }
234
235    #[test]
236    fn deeply_nested_conditionals() {
237        let provider = make_provider(&[("a", "1"), ("b", "2"), ("c", "3")]);
238        let tokens = vec![Token::Conditional(vec![
239            Token::Field("a".into()),
240            Token::Conditional(vec![
241                Token::Field("b".into()),
242                Token::Conditional(vec![Token::Field("c".into())]),
243            ]),
244        ])];
245        assert_eq!(evaluate(&tokens, &provider), "123");
246    }
247
248    #[test]
249    fn function_with_empty_arg() {
250        let provider = make_provider(&[]);
251        // Simulate $if(x,,y) where arg 1 is empty
252        let tokens = vec![Token::Function {
253            name: "if".into(),
254            args: vec![
255                vec![Token::Literal("truthy".into())],
256                vec![], // empty then branch
257                vec![Token::Literal("else".into())],
258            ],
259        }];
260        // "truthy" is non-empty → takes then branch → ""
261        assert_eq!(evaluate(&tokens, &provider), "");
262    }
263
264    #[test]
265    fn stricmp_with_if_empty_then() {
266        let provider = make_provider(&[("x", "hello")]);
267        // $if($stricmp(%x%,hello),,not hello)
268        let tokens = vec![Token::Function {
269            name: "if".into(),
270            args: vec![
271                vec![Token::Function {
272                    name: "stricmp".into(),
273                    args: vec![
274                        vec![Token::Field("x".into())],
275                        vec![Token::Literal("hello".into())],
276                    ],
277                }],
278                vec![], // empty then
279                vec![Token::Literal("not hello".into())],
280            ],
281        }];
282        // stricmp("hello", "hello") → "1" (truthy) → takes empty then → ""
283        assert_eq!(evaluate(&tokens, &provider), "");
284    }
285
286    #[test]
287    fn stricmp_with_if_else() {
288        let provider = make_provider(&[("x", "world")]);
289        let tokens = vec![Token::Function {
290            name: "if".into(),
291            args: vec![
292                vec![Token::Function {
293                    name: "stricmp".into(),
294                    args: vec![
295                        vec![Token::Field("x".into())],
296                        vec![Token::Literal("hello".into())],
297                    ],
298                }],
299                vec![],
300                vec![Token::Literal("not hello".into())],
301            ],
302        }];
303        // stricmp("world", "hello") → "" (falsy) → takes else → "not hello"
304        assert_eq!(evaluate(&tokens, &provider), "not hello");
305    }
306
307    #[test]
308    fn conditional_with_only_literals_always_renders() {
309        let provider = make_provider(&[]);
310        let tokens = vec![Token::Conditional(vec![Token::Literal("always".into())])];
311        // No fields to fail → all_resolved stays true → renders
312        assert_eq!(evaluate(&tokens, &provider), "always");
313    }
314
315    #[test]
316    fn conditional_with_function_returning_empty() {
317        let provider = make_provider(&[("x", "nope")]);
318        let tokens = vec![Token::Conditional(vec![Token::Function {
319            name: "stricmp".into(),
320            args: vec![
321                vec![Token::Field("x".into())],
322                vec![Token::Literal("hello".into())],
323            ],
324        }])];
325        // stricmp returns "" but all fields resolved → conditional renders ""
326        assert_eq!(evaluate(&tokens, &provider), "");
327    }
328}