handlebars/helpers/
scripting.rs

1use std::collections::{BTreeMap, HashMap};
2
3use crate::context::Context;
4use crate::error::{RenderError, RenderErrorReason};
5use crate::helpers::HelperDef;
6use crate::json::value::{PathAndJson, ScopedJson};
7use crate::registry::Registry;
8use crate::render::{Helper, RenderContext};
9
10use rhai::serde::{from_dynamic, to_dynamic};
11use rhai::{Dynamic, Engine, Scope, AST};
12
13use serde_json::value::Value as Json;
14
15pub(crate) struct ScriptHelper {
16    pub(crate) script: AST,
17}
18
19#[inline]
20fn call_script_helper<'reg: 'rc, 'rc>(
21    params: &[PathAndJson<'rc>],
22    hash: &BTreeMap<&'reg str, PathAndJson<'rc>>,
23    engine: &Engine,
24    script: &AST,
25) -> Result<ScopedJson<'rc>, RenderError> {
26    let params: Dynamic = to_dynamic(params.iter().map(|p| p.value()).collect::<Vec<&Json>>())
27        .map_err(RenderErrorReason::from)?;
28
29    let hash: Dynamic = to_dynamic(
30        hash.iter()
31            .map(|(k, v)| ((*k).to_owned(), v.value()))
32            .collect::<HashMap<String, &Json>>(),
33    )
34    .map_err(RenderErrorReason::from)?;
35
36    let mut scope = Scope::new();
37    scope.push_dynamic("params", params);
38    scope.push_dynamic("hash", hash);
39
40    let result = engine
41        .eval_ast_with_scope::<Dynamic>(&mut scope, script)
42        .map_err(RenderErrorReason::from)?;
43
44    let result_json: Json = from_dynamic(&result).map_err(RenderErrorReason::from)?;
45
46    Ok(ScopedJson::Derived(result_json))
47}
48
49impl HelperDef for ScriptHelper {
50    fn call_inner<'reg: 'rc, 'rc>(
51        &self,
52        h: &Helper<'rc>,
53        reg: &'reg Registry<'reg>,
54        _ctx: &'rc Context,
55        _rc: &mut RenderContext<'reg, 'rc>,
56    ) -> Result<ScopedJson<'rc>, RenderError> {
57        call_script_helper(h.params(), h.hash(), &reg.engine, &self.script)
58    }
59}
60
61#[cfg(test)]
62mod test {
63    use super::*;
64    use crate::json::value::{PathAndJson, ScopedJson};
65    use rhai::Engine;
66
67    #[test]
68    fn test_dynamic_convert() {
69        let j0 = json! {
70            [{"name": "tomcat"}, {"name": "jetty"}]
71        };
72
73        let d0 = to_dynamic(j0).unwrap();
74        assert_eq!("array", d0.type_name());
75
76        let j1 = json!({
77            "name": "tomcat",
78            "value": 4000,
79        });
80
81        let d1 = to_dynamic(j1).unwrap();
82        assert_eq!("map", d1.type_name());
83    }
84
85    #[test]
86    fn test_to_json() {
87        let d0 = Dynamic::from("tomcat".to_owned());
88
89        assert_eq!(
90            Json::String("tomcat".to_owned()),
91            from_dynamic::<Json>(&d0).unwrap()
92        );
93    }
94
95    #[test]
96    fn test_script_helper_value_access() {
97        let engine = Engine::new();
98
99        let script = "let plen = len(params); let p0 = params[0]; let hlen = len(hash); let hme = hash[\"me\"]; plen + \",\" + p0 + \",\" + hlen + \",\" + hme";
100        let ast = engine.compile(script).unwrap();
101
102        let params = vec![PathAndJson::new(None, ScopedJson::Derived(json!(true)))];
103
104        let mut hash = BTreeMap::new();
105        hash.insert(
106            "me",
107            PathAndJson::new(None, ScopedJson::Derived(json!("no"))),
108        );
109        hash.insert(
110            "you",
111            PathAndJson::new(None, ScopedJson::Derived(json!("yes"))),
112        );
113
114        let result = call_script_helper(&params, &hash, &engine, &ast)
115            .unwrap()
116            .render();
117        assert_eq!("1,true,2,no", &result);
118    }
119}