Skip to main content

weaveffi_core/
utils.rs

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