Skip to main content

weaveffi_core/
utils.rs

1/// Build the C symbol name for a function: `<prefix>_<module>_<func>`.
2///
3/// `prefix` is the configured ABI symbol prefix (default `"weaveffi"`).
4/// Every backend must route user-symbol construction through this (or the
5/// equivalent [`crate::model::BindingModel`] fields) so a non-default
6/// `c_prefix` is honored consistently across all eleven languages — not just
7/// the C and C++ headers.
8pub fn c_symbol_name(prefix: &str, module: &str, func: &str) -> String {
9    format!("{prefix}_{module}_{func}")
10}
11
12/// Comment syntax used to emit the standard prelude/trailer in generated files.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CommentStyle {
15    /// `// ...` line comments (C, C++, Swift, Kotlin, JS/TS, C#, Dart, Go).
16    DoubleSlash,
17    /// `# ...` line comments (Python, Ruby, YAML, TOML, CMake, GYP, gradle.properties).
18    Hash,
19    /// `<!-- ... -->` block comments (XML, HTML, Markdown).
20    Xml,
21}
22
23impl CommentStyle {
24    fn open(self) -> &'static str {
25        match self {
26            Self::DoubleSlash => "// ",
27            Self::Hash => "# ",
28            Self::Xml => "<!-- ",
29        }
30    }
31
32    fn close(self) -> &'static str {
33        match self {
34            Self::Xml => " -->",
35            _ => "",
36        }
37    }
38}
39
40/// Renders the standard `Generated by WeaveFFI {VERSION} from {input}` prelude
41/// followed by the `DO NOT EDIT` warning and a regenerate command. Trailing
42/// blank line included so generators can append their content directly.
43pub fn render_prelude(style: CommentStyle, input_basename: &str) -> String {
44    let version = env!("CARGO_PKG_VERSION");
45    let o = style.open();
46    let c = style.close();
47    format!(
48        "{o}Generated by WeaveFFI {version} from {input_basename}{c}\n\
49         {o}DO NOT EDIT — your changes will be overwritten.{c}\n\
50         {o}To regenerate: weaveffi generate {input_basename} -o <out>{c}\n\n"
51    )
52}
53
54/// Renders the closing `END {filename}` marker. Caller is responsible for any
55/// preceding newline; the returned string ends with `\n`.
56pub fn render_trailer(style: CommentStyle, filename: &str) -> String {
57    let o = style.open();
58    let c = style.close();
59    format!("{o}END {filename}{c}\n")
60}
61
62/// Renders the JSON-friendly prelude as `"//"` key/value pairs (recognised by
63/// npm). Each line is two-space-indented and comma-terminated so it can be
64/// embedded at the top of any JSON object literal that opens with `{` on the
65/// previous line.
66pub fn render_json_prelude(input_basename: &str) -> String {
67    let version = env!("CARGO_PKG_VERSION");
68    format!(
69        "  \"//\": \"Generated by WeaveFFI {version} from {input_basename}\",\n  \
70         \"//warning\": \"DO NOT EDIT — your changes will be overwritten.\",\n  \
71         \"//regenerate\": \"To regenerate: weaveffi generate {input_basename} -o <out>\",\n"
72    )
73}
74
75/// Symbols (functions and types) exported by the `weaveffi-abi` runtime crate.
76///
77/// Generators that emit C/C++ headers use this list to produce
78/// `#define {prefix}_{name} weaveffi_{name}` aliases at the top of the header
79/// when a non-default `c_prefix` is configured, so consumer code can refer to
80/// runtime helpers by the prefixed name while still linking against the
81/// canonical `weaveffi_*` symbols supplied by `weaveffi-abi`.
82pub const ABI_RUNTIME_SYMBOLS: &[&str] = &[
83    "error",
84    "handle_t",
85    "error_set",
86    "error_clear",
87    "free_string",
88    "free_bytes",
89    "arena_create",
90    "arena_destroy",
91    "arena_register",
92    "cancel_token",
93    "cancel_token_create",
94    "cancel_token_cancel",
95    "cancel_token_is_cancelled",
96    "cancel_token_destroy",
97];
98
99/// Render a `#define {prefix}_{name} weaveffi_{name}` block for runtime ABI
100/// symbols. Returns an empty string when `prefix == "weaveffi"`.
101pub fn render_abi_prefix_aliases(prefix: &str) -> String {
102    if prefix == "weaveffi" {
103        return String::new();
104    }
105    let mut out = String::new();
106    out.push_str("/* Aliases for weaveffi-abi runtime symbols */\n");
107    for sym in ABI_RUNTIME_SYMBOLS {
108        out.push_str(&format!("#define {prefix}_{sym} weaveffi_{sym}\n"));
109    }
110    out.push('\n');
111    out
112}
113
114/// Build the wrapper function name exposed to the foreign language.
115///
116/// When `strip_module_prefix` is `true`, returns just `func`.
117/// When `false`, returns `{module}_{func}`.
118pub fn wrapper_name(module: &str, func: &str, strip_module_prefix: bool) -> String {
119    if strip_module_prefix {
120        func.to_string()
121    } else {
122        format!("{module}_{func}")
123    }
124}
125
126/// Extract the local type name from a potentially qualified `module.TypeName`.
127///
128/// Uses `rsplit_once` so that *multi-level* module paths keep working: only the
129/// final dotted segment is the type name.
130///
131/// `"other.Contact"` → `"Contact"`, `"a.b.Widget"` → `"Widget"`,
132/// `"Contact"` → `"Contact"`.
133pub fn local_type_name(name: &str) -> &str {
134    name.rsplit_once('.').map_or(name, |(_, local)| local)
135}
136
137/// Build the C ABI struct name, resolving cross-module qualified references.
138///
139/// Qualified references use dot-separated module paths; the C ABI flattens
140/// those to underscore-joined symbol prefixes, so `rsplit_once` peels off the
141/// type name and the remaining dotted path becomes underscores.
142///
143/// `"other.Contact"` with any current module → `"{prefix}_other_Contact"`.
144/// `"a.b.Widget"` with any current module → `"{prefix}_a_b_Widget"`.
145/// `"Contact"` with current module `"math"` → `"{prefix}_math_Contact"`.
146pub fn c_abi_struct_name(name: &str, current_module: &str, prefix: &str) -> String {
147    if let Some((module_path, type_name)) = name.rsplit_once('.') {
148        let module_path = module_path.replace('.', "_");
149        format!("{prefix}_{module_path}_{type_name}")
150    } else {
151        format!("{prefix}_{current_module}_{name}")
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn c_symbol_name_uses_prefix() {
161        assert_eq!(
162            c_symbol_name("weaveffi", "calc", "add"),
163            "weaveffi_calc_add"
164        );
165        assert_eq!(c_symbol_name("myffi", "calc", "add"), "myffi_calc_add");
166    }
167
168    #[test]
169    fn local_type_name_unqualified() {
170        assert_eq!(local_type_name("Contact"), "Contact");
171    }
172
173    #[test]
174    fn local_type_name_qualified() {
175        assert_eq!(local_type_name("other.Contact"), "Contact");
176    }
177
178    #[test]
179    fn local_type_name_multi_level() {
180        assert_eq!(local_type_name("a.b.Widget"), "Widget");
181    }
182
183    #[test]
184    fn c_abi_struct_name_unqualified() {
185        assert_eq!(
186            c_abi_struct_name("Contact", "math", "weaveffi"),
187            "weaveffi_math_Contact"
188        );
189    }
190
191    #[test]
192    fn c_abi_struct_name_qualified() {
193        assert_eq!(
194            c_abi_struct_name("types.Name", "ops", "weaveffi"),
195            "weaveffi_types_Name"
196        );
197    }
198
199    #[test]
200    fn c_abi_struct_name_multi_level_flattens_path() {
201        assert_eq!(
202            c_abi_struct_name("a.b.Widget", "ops", "weaveffi"),
203            "weaveffi_a_b_Widget"
204        );
205    }
206
207    #[test]
208    fn abi_prefix_aliases_default_is_empty() {
209        assert!(render_abi_prefix_aliases("weaveffi").is_empty());
210    }
211
212    #[test]
213    fn abi_prefix_aliases_custom_lists_every_symbol() {
214        let out = render_abi_prefix_aliases("myffi");
215        for sym in ABI_RUNTIME_SYMBOLS {
216            let line = format!("#define myffi_{sym} weaveffi_{sym}");
217            assert!(out.contains(&line), "missing alias `{line}` in:\n{out}");
218        }
219    }
220
221    #[test]
222    fn prelude_double_slash_carries_required_phrases() {
223        let p = render_prelude(CommentStyle::DoubleSlash, "calc.yml");
224        assert!(p.starts_with("// Generated by WeaveFFI "));
225        assert!(p.contains(" from calc.yml\n"));
226        assert!(p.contains("// DO NOT EDIT"));
227        assert!(p.contains("// To regenerate: weaveffi generate calc.yml -o <out>"));
228        assert!(p.ends_with("\n\n"));
229    }
230
231    #[test]
232    fn prelude_hash_uses_hash_marker() {
233        let p = render_prelude(CommentStyle::Hash, "calc.yml");
234        assert!(p.starts_with("# Generated by WeaveFFI "));
235        assert!(p.contains("# DO NOT EDIT"));
236        assert!(p.contains("# To regenerate: weaveffi generate calc.yml -o <out>"));
237    }
238
239    #[test]
240    fn prelude_xml_wraps_lines_in_brackets() {
241        let p = render_prelude(CommentStyle::Xml, "calc.yml");
242        assert!(p.starts_with("<!-- Generated by WeaveFFI "));
243        assert!(p.contains("from calc.yml -->"));
244        assert!(p.contains("<!-- DO NOT EDIT"));
245        assert!(p.contains("your changes will be overwritten. -->"));
246    }
247
248    #[test]
249    fn trailer_has_correct_marker_per_style() {
250        assert_eq!(
251            render_trailer(CommentStyle::DoubleSlash, "lib.rs"),
252            "// END lib.rs\n"
253        );
254        assert_eq!(
255            render_trailer(CommentStyle::Hash, "build.toml"),
256            "# END build.toml\n"
257        );
258        assert_eq!(
259            render_trailer(CommentStyle::Xml, "package.csproj"),
260            "<!-- END package.csproj -->\n"
261        );
262    }
263
264    #[test]
265    fn json_prelude_contains_required_phrases_in_first_lines() {
266        let p = render_json_prelude("calc.yml");
267        let lines: Vec<&str> = p.lines().collect();
268        assert!(lines[0].contains("Generated by WeaveFFI"));
269        assert!(lines[0].contains("from calc.yml"));
270        assert!(lines[1].contains("DO NOT EDIT"));
271        assert!(lines[2].contains("To regenerate"));
272        for line in &lines {
273            assert!(line.starts_with("  "));
274            assert!(line.ends_with(','));
275        }
276    }
277}