Skip to main content

weaveffi_core/
templates.rs

1use std::collections::BTreeMap;
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) -> BTreeMap<String, tera::Value> {
55    let mut map: BTreeMap<String, tera::Value> = BTreeMap::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    }
132    map
133}
134
135pub fn api_to_context(api: &Api) -> tera::Context {
136    let mut ctx = tera::Context::new();
137    ctx.insert("version", &api.version);
138
139    let modules: Vec<tera::Value> = api
140        .modules
141        .iter()
142        .map(|module| {
143            let functions: Vec<tera::Value> = module
144                .functions
145                .iter()
146                .map(|func| {
147                    let params: Vec<tera::Value> = func
148                        .params
149                        .iter()
150                        .map(|p| {
151                            serde_json::json!({
152                                "name": p.name,
153                                "type": type_ref_to_map(&p.ty),
154                            })
155                        })
156                        .collect();
157
158                    let returns = func
159                        .returns
160                        .as_ref()
161                        .map(|r| serde_json::to_value(type_ref_to_map(r)).unwrap());
162
163                    serde_json::json!({
164                        "name": func.name,
165                        "params": params,
166                        "returns": returns,
167                        "doc": func.doc,
168                    })
169                })
170                .collect();
171
172            let structs: Vec<tera::Value> = module
173                .structs
174                .iter()
175                .map(|s| {
176                    let fields: Vec<tera::Value> = s
177                        .fields
178                        .iter()
179                        .map(|field| {
180                            serde_json::json!({
181                                "name": field.name,
182                                "type": type_ref_to_map(&field.ty),
183                            })
184                        })
185                        .collect();
186                    serde_json::json!({
187                        "name": s.name,
188                        "fields": fields,
189                    })
190                })
191                .collect();
192
193            let enums: Vec<tera::Value> = module
194                .enums
195                .iter()
196                .map(|e| {
197                    let variants: Vec<tera::Value> = e
198                        .variants
199                        .iter()
200                        .map(|v| {
201                            serde_json::json!({
202                                "name": v.name,
203                                "value": v.value,
204                            })
205                        })
206                        .collect();
207                    serde_json::json!({
208                        "name": e.name,
209                        "variants": variants,
210                    })
211                })
212                .collect();
213
214            serde_json::json!({
215                "name": module.name,
216                "functions": functions,
217                "structs": structs,
218                "enums": enums,
219            })
220        })
221        .collect();
222
223    ctx.insert("modules", &modules);
224    ctx
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use weaveffi_ir::ir::{Function, Module, Param, StructDef, StructField};
231
232    #[test]
233    fn api_context_has_modules() {
234        let api = Api {
235            version: "0.1.0".into(),
236            modules: vec![Module {
237                name: "math".into(),
238                functions: vec![Function {
239                    name: "add".into(),
240                    params: vec![
241                        Param {
242                            name: "a".into(),
243                            ty: TypeRef::I32,
244                            mutable: false,
245                            doc: None,
246                        },
247                        Param {
248                            name: "b".into(),
249                            ty: TypeRef::I32,
250                            mutable: false,
251                            doc: None,
252                        },
253                    ],
254                    returns: Some(TypeRef::I32),
255                    doc: Some("Add two numbers".into()),
256                    r#async: false,
257                    cancellable: false,
258                    deprecated: None,
259                    since: None,
260                }],
261                structs: vec![StructDef {
262                    name: "Point".into(),
263                    doc: None,
264                    fields: vec![StructField {
265                        name: "x".into(),
266                        ty: TypeRef::F64,
267                        doc: None,
268                        default: None,
269                    }],
270                    builder: false,
271                }],
272                enums: vec![],
273                callbacks: vec![],
274                listeners: vec![],
275                errors: None,
276                modules: vec![],
277            }],
278            generators: None,
279        };
280
281        let ctx = api_to_context(&api);
282        let json = ctx.into_json();
283
284        assert_eq!(json["version"], "0.1.0");
285
286        let modules = json["modules"].as_array().unwrap();
287        assert_eq!(modules.len(), 1);
288        assert_eq!(modules[0]["name"], "math");
289
290        let funcs = modules[0]["functions"].as_array().unwrap();
291        assert_eq!(funcs.len(), 1);
292        assert_eq!(funcs[0]["name"], "add");
293        assert_eq!(funcs[0]["doc"], "Add two numbers");
294        assert_eq!(funcs[0]["returns"]["kind"], "i32");
295
296        let params = funcs[0]["params"].as_array().unwrap();
297        assert_eq!(params.len(), 2);
298        assert_eq!(params[0]["name"], "a");
299        assert_eq!(params[0]["type"]["kind"], "i32");
300
301        let structs = modules[0]["structs"].as_array().unwrap();
302        assert_eq!(structs.len(), 1);
303        assert_eq!(structs[0]["name"], "Point");
304
305        let fields = structs[0]["fields"].as_array().unwrap();
306        assert_eq!(fields.len(), 1);
307        assert_eq!(fields[0]["name"], "x");
308        assert_eq!(fields[0]["type"]["kind"], "f64");
309    }
310
311    #[test]
312    fn type_ref_context_struct() {
313        let map = type_ref_to_map(&TypeRef::Struct("Point".into()));
314        assert_eq!(map["kind"], "struct");
315        assert_eq!(map["name"], "Point");
316        assert_eq!(map.len(), 2);
317    }
318
319    #[test]
320    fn template_render_basic() {
321        let mut engine = TemplateEngine::new();
322        engine.load_builtin("greeting", "hello {{ name }}").unwrap();
323
324        let mut ctx = tera::Context::new();
325        ctx.insert("name", "world");
326
327        let output = engine.render("greeting", &ctx).unwrap();
328        assert_eq!(output, "hello world");
329    }
330
331    #[test]
332    fn load_dir_overrides_builtin() {
333        let mut engine = TemplateEngine::new();
334        engine
335            .load_builtin("test.tera", "original {{ val }}")
336            .unwrap();
337
338        let dir = tempfile::tempdir().unwrap();
339        let dir_path = Utf8Path::from_path(dir.path()).unwrap();
340        std::fs::write(dir_path.join("test.tera"), "override {{ val }}").unwrap();
341
342        engine.load_dir(dir_path).unwrap();
343
344        let mut ctx = tera::Context::new();
345        ctx.insert("val", "ok");
346        let output = engine.render("test.tera", &ctx).unwrap();
347        assert_eq!(output, "override ok");
348    }
349}