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}