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}