weaveffi-core 0.4.0

Generator trait, orchestrator, validation, and shared utilities for WeaveFFI
Documentation
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");
    }
}