weaveffi-core 0.4.0

Generator trait, orchestrator, validation, and shared utilities for WeaveFFI
Documentation
/// Build the C symbol name for a function: `weaveffi_<module>_<func>`.
pub fn c_symbol_name(module: &str, func: &str) -> String {
    format!("weaveffi_{}_{}", module, func)
}

/// Comment syntax used to emit the standard prelude/trailer in generated files.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommentStyle {
    /// `// ...` line comments (C, C++, Swift, Kotlin, JS/TS, C#, Dart, Go).
    DoubleSlash,
    /// `# ...` line comments (Python, Ruby, YAML, TOML, CMake, GYP, gradle.properties).
    Hash,
    /// `<!-- ... -->` block comments (XML, HTML, Markdown).
    Xml,
}

impl CommentStyle {
    fn open(self) -> &'static str {
        match self {
            Self::DoubleSlash => "// ",
            Self::Hash => "# ",
            Self::Xml => "<!-- ",
        }
    }

    fn close(self) -> &'static str {
        match self {
            Self::Xml => " -->",
            _ => "",
        }
    }
}

/// Renders the standard `Generated by WeaveFFI {VERSION} from {input}` prelude
/// followed by the `DO NOT EDIT` warning and a regenerate command. Trailing
/// blank line included so generators can append their content directly.
pub fn render_prelude(style: CommentStyle, input_basename: &str) -> String {
    let version = env!("CARGO_PKG_VERSION");
    let o = style.open();
    let c = style.close();
    format!(
        "{o}Generated by WeaveFFI {version} from {input_basename}{c}\n\
         {o}DO NOT EDIT — your changes will be overwritten.{c}\n\
         {o}To regenerate: weaveffi generate {input_basename} -o <out>{c}\n\n"
    )
}

/// Renders the closing `END {filename}` marker. Caller is responsible for any
/// preceding newline; the returned string ends with `\n`.
pub fn render_trailer(style: CommentStyle, filename: &str) -> String {
    let o = style.open();
    let c = style.close();
    format!("{o}END {filename}{c}\n")
}

/// Renders the JSON-friendly prelude as `"//"` key/value pairs (recognised by
/// npm). Each line is two-space-indented and comma-terminated so it can be
/// embedded at the top of any JSON object literal that opens with `{` on the
/// previous line.
pub fn render_json_prelude(input_basename: &str) -> String {
    let version = env!("CARGO_PKG_VERSION");
    format!(
        "  \"//\": \"Generated by WeaveFFI {version} from {input_basename}\",\n  \
         \"//warning\": \"DO NOT EDIT — your changes will be overwritten.\",\n  \
         \"//regenerate\": \"To regenerate: weaveffi generate {input_basename} -o <out>\",\n"
    )
}

/// Symbols (functions and types) exported by the `weaveffi-abi` runtime crate.
///
/// Generators that emit C/C++ headers use this list to produce
/// `#define {prefix}_{name} weaveffi_{name}` aliases at the top of the header
/// when a non-default `c_prefix` is configured, so consumer code can refer to
/// runtime helpers by the prefixed name while still linking against the
/// canonical `weaveffi_*` symbols supplied by `weaveffi-abi`.
pub const ABI_RUNTIME_SYMBOLS: &[&str] = &[
    "error",
    "handle_t",
    "error_set",
    "error_clear",
    "free_string",
    "free_bytes",
    "arena_create",
    "arena_destroy",
    "arena_register",
    "cancel_token",
    "cancel_token_create",
    "cancel_token_cancel",
    "cancel_token_is_cancelled",
    "cancel_token_destroy",
];

/// Render a `#define {prefix}_{name} weaveffi_{name}` block for runtime ABI
/// symbols. Returns an empty string when `prefix == "weaveffi"`.
pub fn render_abi_prefix_aliases(prefix: &str) -> String {
    if prefix == "weaveffi" {
        return String::new();
    }
    let mut out = String::new();
    out.push_str("/* Aliases for weaveffi-abi runtime symbols */\n");
    for sym in ABI_RUNTIME_SYMBOLS {
        out.push_str(&format!("#define {prefix}_{sym} weaveffi_{sym}\n"));
    }
    out.push('\n');
    out
}

/// Build the wrapper function name exposed to the foreign language.
///
/// When `strip_module_prefix` is `true`, returns just `func`.
/// When `false`, returns `{module}_{func}`.
pub fn wrapper_name(module: &str, func: &str, strip_module_prefix: bool) -> String {
    if strip_module_prefix {
        func.to_string()
    } else {
        format!("{module}_{func}")
    }
}

/// Extract the local type name from a potentially qualified `module.TypeName`.
///
/// `"other.Contact"` → `"Contact"`, `"Contact"` → `"Contact"`.
pub fn local_type_name(name: &str) -> &str {
    name.split_once('.').map_or(name, |(_, local)| local)
}

/// Build the C ABI struct name, resolving cross-module qualified references.
///
/// `"other.Contact"` with any current module → `"{prefix}_other_Contact"`.
/// `"Contact"` with current module `"math"` → `"{prefix}_math_Contact"`.
pub fn c_abi_struct_name(name: &str, current_module: &str, prefix: &str) -> String {
    if let Some((mod_name, type_name)) = name.split_once('.') {
        format!("{prefix}_{mod_name}_{type_name}")
    } else {
        format!("{prefix}_{current_module}_{name}")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn local_type_name_unqualified() {
        assert_eq!(local_type_name("Contact"), "Contact");
    }

    #[test]
    fn local_type_name_qualified() {
        assert_eq!(local_type_name("other.Contact"), "Contact");
    }

    #[test]
    fn c_abi_struct_name_unqualified() {
        assert_eq!(
            c_abi_struct_name("Contact", "math", "weaveffi"),
            "weaveffi_math_Contact"
        );
    }

    #[test]
    fn c_abi_struct_name_qualified() {
        assert_eq!(
            c_abi_struct_name("types.Name", "ops", "weaveffi"),
            "weaveffi_types_Name"
        );
    }

    #[test]
    fn abi_prefix_aliases_default_is_empty() {
        assert!(render_abi_prefix_aliases("weaveffi").is_empty());
    }

    #[test]
    fn abi_prefix_aliases_custom_lists_every_symbol() {
        let out = render_abi_prefix_aliases("myffi");
        for sym in ABI_RUNTIME_SYMBOLS {
            let line = format!("#define myffi_{sym} weaveffi_{sym}");
            assert!(out.contains(&line), "missing alias `{line}` in:\n{out}");
        }
    }

    #[test]
    fn prelude_double_slash_carries_required_phrases() {
        let p = render_prelude(CommentStyle::DoubleSlash, "calc.yml");
        assert!(p.starts_with("// Generated by WeaveFFI "));
        assert!(p.contains(" from calc.yml\n"));
        assert!(p.contains("// DO NOT EDIT"));
        assert!(p.contains("// To regenerate: weaveffi generate calc.yml -o <out>"));
        assert!(p.ends_with("\n\n"));
    }

    #[test]
    fn prelude_hash_uses_hash_marker() {
        let p = render_prelude(CommentStyle::Hash, "calc.yml");
        assert!(p.starts_with("# Generated by WeaveFFI "));
        assert!(p.contains("# DO NOT EDIT"));
        assert!(p.contains("# To regenerate: weaveffi generate calc.yml -o <out>"));
    }

    #[test]
    fn prelude_xml_wraps_lines_in_brackets() {
        let p = render_prelude(CommentStyle::Xml, "calc.yml");
        assert!(p.starts_with("<!-- Generated by WeaveFFI "));
        assert!(p.contains("from calc.yml -->"));
        assert!(p.contains("<!-- DO NOT EDIT"));
        assert!(p.contains("your changes will be overwritten. -->"));
    }

    #[test]
    fn trailer_has_correct_marker_per_style() {
        assert_eq!(
            render_trailer(CommentStyle::DoubleSlash, "lib.rs"),
            "// END lib.rs\n"
        );
        assert_eq!(
            render_trailer(CommentStyle::Hash, "build.toml"),
            "# END build.toml\n"
        );
        assert_eq!(
            render_trailer(CommentStyle::Xml, "package.csproj"),
            "<!-- END package.csproj -->\n"
        );
    }

    #[test]
    fn json_prelude_contains_required_phrases_in_first_lines() {
        let p = render_json_prelude("calc.yml");
        let lines: Vec<&str> = p.lines().collect();
        assert!(lines[0].contains("Generated by WeaveFFI"));
        assert!(lines[0].contains("from calc.yml"));
        assert!(lines[1].contains("DO NOT EDIT"));
        assert!(lines[2].contains("To regenerate"));
        for line in &lines {
            assert!(line.starts_with("  "));
            assert!(line.ends_with(','));
        }
    }
}