Skip to main content

weaveffi_core/
templates.rs

1use std::collections::HashMap;
2
3use anyhow::{Context, Result};
4use camino::Utf8Path;
5use tera::Tera;
6use weaveffi_ir::ir::{Api, TypeRef};
7
8#[derive(Default)]
9pub struct TemplateEngine {
10    tera: Tera,
11}
12
13impl TemplateEngine {
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    pub fn load_builtin(&mut self, name: &str, content: &str) -> Result<()> {
19        self.tera
20            .add_raw_template(name, content)
21            .with_context(|| format!("failed to load builtin template '{name}'"))
22    }
23
24    pub fn load_dir(&mut self, dir: &Utf8Path) -> Result<()> {
25        let entries = std::fs::read_dir(dir)
26            .with_context(|| format!("failed to read template directory '{dir}'"))?;
27
28        for entry in entries {
29            let entry = entry?;
30            let path = entry.path();
31            if path.extension().is_some_and(|ext| ext == "tera") {
32                let name = path
33                    .file_name()
34                    .expect("file entry must have a name")
35                    .to_string_lossy();
36                let content = std::fs::read_to_string(&path).with_context(|| {
37                    format!("failed to read template file '{}'", path.display())
38                })?;
39                self.tera
40                    .add_raw_template(&name, &content)
41                    .with_context(|| format!("failed to parse template '{name}'"))?;
42            }
43        }
44        Ok(())
45    }
46
47    pub fn render(&self, name: &str, context: &tera::Context) -> Result<String> {
48        self.tera
49            .render(name, context)
50            .with_context(|| format!("failed to render template '{name}'"))
51    }
52}
53
54pub fn type_ref_to_map(ty: &TypeRef) -> HashMap<String, tera::Value> {
55    let mut map: HashMap<String, tera::Value> = HashMap::new();
56    match ty {
57        TypeRef::I32 => {
58            map.insert("kind".into(), "i32".into());
59        }
60        TypeRef::U32 => {
61            map.insert("kind".into(), "u32".into());
62        }
63        TypeRef::I64 => {
64            map.insert("kind".into(), "i64".into());
65        }
66        TypeRef::F64 => {
67            map.insert("kind".into(), "f64".into());
68        }
69        TypeRef::Bool => {
70            map.insert("kind".into(), "bool".into());
71        }
72        TypeRef::StringUtf8 => {
73            map.insert("kind".into(), "string".into());
74        }
75        TypeRef::Bytes => {
76            map.insert("kind".into(), "bytes".into());
77        }
78        TypeRef::BorrowedStr => {
79            map.insert("kind".into(), "borrowed_str".into());
80        }
81        TypeRef::BorrowedBytes => {
82            map.insert("kind".into(), "borrowed_bytes".into());
83        }
84        TypeRef::Handle => {
85            map.insert("kind".into(), "handle".into());
86        }
87        TypeRef::TypedHandle(name) => {
88            map.insert("kind".into(), "handle".into());
89            map.insert("name".into(), name.clone().into());
90        }
91        TypeRef::Struct(name) => {
92            map.insert("kind".into(), "struct".into());
93            map.insert("name".into(), name.clone().into());
94        }
95        TypeRef::Enum(name) => {
96            map.insert("kind".into(), "enum".into());
97            map.insert("name".into(), name.clone().into());
98        }
99        TypeRef::Optional(inner) => {
100            map.insert("kind".into(), "optional".into());
101            map.insert(
102                "inner".into(),
103                serde_json::to_value(type_ref_to_map(inner)).unwrap(),
104            );
105        }
106        TypeRef::List(inner) => {
107            map.insert("kind".into(), "list".into());
108            map.insert(
109                "inner".into(),
110                serde_json::to_value(type_ref_to_map(inner)).unwrap(),
111            );
112        }
113        TypeRef::Map(key, value) => {
114            map.insert("kind".into(), "map".into());
115            map.insert(
116                "key".into(),
117                serde_json::to_value(type_ref_to_map(key)).unwrap(),
118            );
119            map.insert(
120                "value".into(),
121                serde_json::to_value(type_ref_to_map(value)).unwrap(),
122            );
123        }
124        TypeRef::Iterator(inner) => {
125            map.insert("kind".into(), "iterator".into());
126            map.insert(
127                "inner".into(),
128                serde_json::to_value(type_ref_to_map(inner)).unwrap(),
129            );
130        }
131        TypeRef::Callback(_) => todo!("callback template type"),
132    }
133    map
134}
135
136pub fn api_to_context(api: &Api) -> tera::Context {
137    let mut ctx = tera::Context::new();
138    ctx.insert("version", &api.version);
139
140    let modules: Vec<tera::Value> = api
141        .modules
142        .iter()
143        .map(|module| {
144            let functions: Vec<tera::Value> = module
145                .functions
146                .iter()
147                .map(|func| {
148                    let params: Vec<tera::Value> = func
149                        .params
150                        .iter()
151                        .map(|p| {
152                            serde_json::json!({
153                                "name": p.name,
154                                "type": type_ref_to_map(&p.ty),
155                            })
156                        })
157                        .collect();
158
159                    let returns = func
160                        .returns
161                        .as_ref()
162                        .map(|r| serde_json::to_value(type_ref_to_map(r)).unwrap());
163
164                    serde_json::json!({
165                        "name": func.name,
166                        "params": params,
167                        "returns": returns,
168                        "doc": func.doc,
169                    })
170                })
171                .collect();
172
173            let structs: Vec<tera::Value> = module
174                .structs
175                .iter()
176                .map(|s| {
177                    let fields: Vec<tera::Value> = s
178                        .fields
179                        .iter()
180                        .map(|field| {
181                            serde_json::json!({
182                                "name": field.name,
183                                "type": type_ref_to_map(&field.ty),
184                            })
185                        })
186                        .collect();
187                    serde_json::json!({
188                        "name": s.name,
189                        "fields": fields,
190                    })
191                })
192                .collect();
193
194            let enums: Vec<tera::Value> = module
195                .enums
196                .iter()
197                .map(|e| {
198                    let variants: Vec<tera::Value> = e
199                        .variants
200                        .iter()
201                        .map(|v| {
202                            serde_json::json!({
203                                "name": v.name,
204                                "value": v.value,
205                            })
206                        })
207                        .collect();
208                    serde_json::json!({
209                        "name": e.name,
210                        "variants": variants,
211                    })
212                })
213                .collect();
214
215            serde_json::json!({
216                "name": module.name,
217                "functions": functions,
218                "structs": structs,
219                "enums": enums,
220            })
221        })
222        .collect();
223
224    ctx.insert("modules", &modules);
225    ctx
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use weaveffi_ir::ir::{Function, Module, Param, StructDef, StructField};
232
233    #[test]
234    fn api_context_has_modules() {
235        let api = Api {
236            version: "0.1.0".into(),
237            modules: vec![Module {
238                name: "math".into(),
239                functions: vec![Function {
240                    name: "add".into(),
241                    params: vec![
242                        Param {
243                            name: "a".into(),
244                            ty: TypeRef::I32,
245                            mutable: false,
246                        },
247                        Param {
248                            name: "b".into(),
249                            ty: TypeRef::I32,
250                            mutable: false,
251                        },
252                    ],
253                    returns: Some(TypeRef::I32),
254                    doc: Some("Add two numbers".into()),
255                    r#async: false,
256                    cancellable: false,
257                    deprecated: None,
258                    since: None,
259                }],
260                structs: vec![StructDef {
261                    name: "Point".into(),
262                    doc: None,
263                    fields: vec![StructField {
264                        name: "x".into(),
265                        ty: TypeRef::F64,
266                        doc: None,
267                        default: None,
268                    }],
269                    builder: false,
270                }],
271                enums: vec![],
272                callbacks: vec![],
273                listeners: vec![],
274                errors: None,
275                modules: vec![],
276            }],
277            generators: None,
278        };
279
280        let ctx = api_to_context(&api);
281        let json = ctx.into_json();
282
283        assert_eq!(json["version"], "0.1.0");
284
285        let modules = json["modules"].as_array().unwrap();
286        assert_eq!(modules.len(), 1);
287        assert_eq!(modules[0]["name"], "math");
288
289        let funcs = modules[0]["functions"].as_array().unwrap();
290        assert_eq!(funcs.len(), 1);
291        assert_eq!(funcs[0]["name"], "add");
292        assert_eq!(funcs[0]["doc"], "Add two numbers");
293        assert_eq!(funcs[0]["returns"]["kind"], "i32");
294
295        let params = funcs[0]["params"].as_array().unwrap();
296        assert_eq!(params.len(), 2);
297        assert_eq!(params[0]["name"], "a");
298        assert_eq!(params[0]["type"]["kind"], "i32");
299
300        let structs = modules[0]["structs"].as_array().unwrap();
301        assert_eq!(structs.len(), 1);
302        assert_eq!(structs[0]["name"], "Point");
303
304        let fields = structs[0]["fields"].as_array().unwrap();
305        assert_eq!(fields.len(), 1);
306        assert_eq!(fields[0]["name"], "x");
307        assert_eq!(fields[0]["type"]["kind"], "f64");
308    }
309
310    #[test]
311    fn type_ref_context_struct() {
312        let map = type_ref_to_map(&TypeRef::Struct("Point".into()));
313        assert_eq!(map["kind"], "struct");
314        assert_eq!(map["name"], "Point");
315        assert_eq!(map.len(), 2);
316    }
317
318    #[test]
319    fn template_render_basic() {
320        let mut engine = TemplateEngine::new();
321        engine.load_builtin("greeting", "hello {{ name }}").unwrap();
322
323        let mut ctx = tera::Context::new();
324        ctx.insert("name", "world");
325
326        let output = engine.render("greeting", &ctx).unwrap();
327        assert_eq!(output, "hello world");
328    }
329
330    #[test]
331    fn load_dir_overrides_builtin() {
332        let mut engine = TemplateEngine::new();
333        engine
334            .load_builtin("test.tera", "original {{ val }}")
335            .unwrap();
336
337        let dir = tempfile::tempdir().unwrap();
338        let dir_path = Utf8Path::from_path(dir.path()).unwrap();
339        std::fs::write(dir_path.join("test.tera"), "override {{ val }}").unwrap();
340
341        engine.load_dir(dir_path).unwrap();
342
343        let mut ctx = tera::Context::new();
344        ctx.insert("val", "ok");
345        let output = engine.render("test.tera", &ctx).unwrap();
346        assert_eq!(output, "override ok");
347    }
348}