use std::collections::BTreeMap;
use anyhow::{Context, Result};
use camino::Utf8Path;
use tera::Tera;
use weaveffi_ir::ir::{Api, TypeRef};
#[derive(Default)]
pub struct TemplateEngine {
tera: Tera,
}
impl TemplateEngine {
pub fn new() -> Self {
Self::default()
}
pub fn load_builtin(&mut self, name: &str, content: &str) -> Result<()> {
self.tera
.add_raw_template(name, content)
.with_context(|| format!("failed to load builtin template '{name}'"))
}
pub fn load_dir(&mut self, dir: &Utf8Path) -> Result<()> {
let entries = std::fs::read_dir(dir)
.with_context(|| format!("failed to read template directory '{dir}'"))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "tera") {
let name = path
.file_name()
.expect("file entry must have a name")
.to_string_lossy();
let content = std::fs::read_to_string(&path).with_context(|| {
format!("failed to read template file '{}'", path.display())
})?;
self.tera
.add_raw_template(&name, &content)
.with_context(|| format!("failed to parse template '{name}'"))?;
}
}
Ok(())
}
pub fn render(&self, name: &str, context: &tera::Context) -> Result<String> {
self.tera
.render(name, context)
.with_context(|| format!("failed to render template '{name}'"))
}
}
pub fn type_ref_to_map(ty: &TypeRef) -> BTreeMap<String, tera::Value> {
let mut map: BTreeMap<String, tera::Value> = BTreeMap::new();
match ty {
TypeRef::I32 => {
map.insert("kind".into(), "i32".into());
}
TypeRef::U32 => {
map.insert("kind".into(), "u32".into());
}
TypeRef::I64 => {
map.insert("kind".into(), "i64".into());
}
TypeRef::F64 => {
map.insert("kind".into(), "f64".into());
}
TypeRef::Bool => {
map.insert("kind".into(), "bool".into());
}
TypeRef::StringUtf8 => {
map.insert("kind".into(), "string".into());
}
TypeRef::Bytes => {
map.insert("kind".into(), "bytes".into());
}
TypeRef::BorrowedStr => {
map.insert("kind".into(), "borrowed_str".into());
}
TypeRef::BorrowedBytes => {
map.insert("kind".into(), "borrowed_bytes".into());
}
TypeRef::Handle => {
map.insert("kind".into(), "handle".into());
}
TypeRef::TypedHandle(name) => {
map.insert("kind".into(), "handle".into());
map.insert("name".into(), name.clone().into());
}
TypeRef::Struct(name) => {
map.insert("kind".into(), "struct".into());
map.insert("name".into(), name.clone().into());
}
TypeRef::Enum(name) => {
map.insert("kind".into(), "enum".into());
map.insert("name".into(), name.clone().into());
}
TypeRef::Optional(inner) => {
map.insert("kind".into(), "optional".into());
map.insert(
"inner".into(),
serde_json::to_value(type_ref_to_map(inner)).unwrap(),
);
}
TypeRef::List(inner) => {
map.insert("kind".into(), "list".into());
map.insert(
"inner".into(),
serde_json::to_value(type_ref_to_map(inner)).unwrap(),
);
}
TypeRef::Map(key, value) => {
map.insert("kind".into(), "map".into());
map.insert(
"key".into(),
serde_json::to_value(type_ref_to_map(key)).unwrap(),
);
map.insert(
"value".into(),
serde_json::to_value(type_ref_to_map(value)).unwrap(),
);
}
TypeRef::Iterator(inner) => {
map.insert("kind".into(), "iterator".into());
map.insert(
"inner".into(),
serde_json::to_value(type_ref_to_map(inner)).unwrap(),
);
}
}
map
}
pub fn api_to_context(api: &Api) -> tera::Context {
let mut ctx = tera::Context::new();
ctx.insert("version", &api.version);
let modules: Vec<tera::Value> = api
.modules
.iter()
.map(|module| {
let functions: Vec<tera::Value> = module
.functions
.iter()
.map(|func| {
let params: Vec<tera::Value> = func
.params
.iter()
.map(|p| {
serde_json::json!({
"name": p.name,
"type": type_ref_to_map(&p.ty),
})
})
.collect();
let returns = func
.returns
.as_ref()
.map(|r| serde_json::to_value(type_ref_to_map(r)).unwrap());
serde_json::json!({
"name": func.name,
"params": params,
"returns": returns,
"doc": func.doc,
})
})
.collect();
let structs: Vec<tera::Value> = module
.structs
.iter()
.map(|s| {
let fields: Vec<tera::Value> = s
.fields
.iter()
.map(|field| {
serde_json::json!({
"name": field.name,
"type": type_ref_to_map(&field.ty),
})
})
.collect();
serde_json::json!({
"name": s.name,
"fields": fields,
})
})
.collect();
let enums: Vec<tera::Value> = module
.enums
.iter()
.map(|e| {
let variants: Vec<tera::Value> = e
.variants
.iter()
.map(|v| {
serde_json::json!({
"name": v.name,
"value": v.value,
})
})
.collect();
serde_json::json!({
"name": e.name,
"variants": variants,
})
})
.collect();
serde_json::json!({
"name": module.name,
"functions": functions,
"structs": structs,
"enums": enums,
})
})
.collect();
ctx.insert("modules", &modules);
ctx
}
#[cfg(test)]
mod tests {
use super::*;
use weaveffi_ir::ir::{Function, Module, Param, StructDef, StructField};
#[test]
fn api_context_has_modules() {
let api = Api {
version: "0.1.0".into(),
modules: vec![Module {
name: "math".into(),
functions: vec![Function {
name: "add".into(),
params: vec![
Param {
name: "a".into(),
ty: TypeRef::I32,
mutable: false,
doc: None,
},
Param {
name: "b".into(),
ty: TypeRef::I32,
mutable: false,
doc: None,
},
],
returns: Some(TypeRef::I32),
doc: Some("Add two numbers".into()),
r#async: false,
cancellable: false,
deprecated: None,
since: None,
}],
structs: vec![StructDef {
name: "Point".into(),
doc: None,
fields: vec![StructField {
name: "x".into(),
ty: TypeRef::F64,
doc: None,
default: None,
}],
builder: false,
}],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}],
generators: None,
};
let ctx = api_to_context(&api);
let json = ctx.into_json();
assert_eq!(json["version"], "0.1.0");
let modules = json["modules"].as_array().unwrap();
assert_eq!(modules.len(), 1);
assert_eq!(modules[0]["name"], "math");
let funcs = modules[0]["functions"].as_array().unwrap();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0]["name"], "add");
assert_eq!(funcs[0]["doc"], "Add two numbers");
assert_eq!(funcs[0]["returns"]["kind"], "i32");
let params = funcs[0]["params"].as_array().unwrap();
assert_eq!(params.len(), 2);
assert_eq!(params[0]["name"], "a");
assert_eq!(params[0]["type"]["kind"], "i32");
let structs = modules[0]["structs"].as_array().unwrap();
assert_eq!(structs.len(), 1);
assert_eq!(structs[0]["name"], "Point");
let fields = structs[0]["fields"].as_array().unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0]["name"], "x");
assert_eq!(fields[0]["type"]["kind"], "f64");
}
#[test]
fn type_ref_context_struct() {
let map = type_ref_to_map(&TypeRef::Struct("Point".into()));
assert_eq!(map["kind"], "struct");
assert_eq!(map["name"], "Point");
assert_eq!(map.len(), 2);
}
#[test]
fn template_render_basic() {
let mut engine = TemplateEngine::new();
engine.load_builtin("greeting", "hello {{ name }}").unwrap();
let mut ctx = tera::Context::new();
ctx.insert("name", "world");
let output = engine.render("greeting", &ctx).unwrap();
assert_eq!(output, "hello world");
}
#[test]
fn load_dir_overrides_builtin() {
let mut engine = TemplateEngine::new();
engine
.load_builtin("test.tera", "original {{ val }}")
.unwrap();
let dir = tempfile::tempdir().unwrap();
let dir_path = Utf8Path::from_path(dir.path()).unwrap();
std::fs::write(dir_path.join("test.tera"), "override {{ val }}").unwrap();
engine.load_dir(dir_path).unwrap();
let mut ctx = tera::Context::new();
ctx.insert("val", "ok");
let output = engine.render("test.tera", &ctx).unwrap();
assert_eq!(output, "override ok");
}
}