Skip to main content

alef_backend_java/gen_bindings/
mod.rs

1use ahash::AHashSet;
2use alef_codegen::naming::to_class_name;
3use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
4use alef_core::config::{AlefConfig, Language, resolve_output_dir};
5use alef_core::ir::ApiSurface;
6use std::collections::HashSet;
7use std::path::PathBuf;
8
9mod facade;
10mod ffi_class;
11mod helpers;
12mod marshal;
13mod native_lib;
14mod types;
15
16use facade::gen_facade_class;
17use ffi_class::gen_main_class;
18use helpers::gen_exception_class;
19use native_lib::gen_native_lib;
20use types::{gen_builder_class, gen_enum_class, gen_opaque_handle_class, gen_record_type};
21
22pub struct JavaBackend;
23
24impl JavaBackend {
25    /// Convert crate name to main class name (PascalCase + "Rs" suffix).
26    ///
27    /// The "Rs" suffix ensures the raw FFI wrapper class has a distinct name from
28    /// the public facade class (which strips the "Rs" suffix). Without this, the
29    /// facade would delegate to itself, causing infinite recursion.
30    fn resolve_main_class(api: &ApiSurface) -> String {
31        let base = to_class_name(&api.crate_name.replace('-', "_"));
32        if base.ends_with("Rs") {
33            base
34        } else {
35            format!("{}Rs", base)
36        }
37    }
38}
39
40impl Backend for JavaBackend {
41    fn name(&self) -> &str {
42        "java"
43    }
44
45    fn language(&self) -> Language {
46        Language::Java
47    }
48
49    fn capabilities(&self) -> Capabilities {
50        Capabilities {
51            supports_async: true,
52            supports_classes: true,
53            supports_enums: true,
54            supports_option: true,
55            supports_result: true,
56            ..Capabilities::default()
57        }
58    }
59
60    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
61        let package = config.java_package();
62        let prefix = config.ffi_prefix();
63        let main_class = Self::resolve_main_class(api);
64        let package_path = package.replace('.', "/");
65
66        let output_dir = resolve_output_dir(
67            config.output.java.as_ref(),
68            &config.crate_config.name,
69            "packages/java/src/main/java/",
70        );
71
72        let base_path = PathBuf::from(&output_dir).join(&package_path);
73
74        // Collect bridge param names and type aliases so we can strip them from generated
75        // function signatures and emit convertWithVisitor instead.
76        let bridge_param_names: HashSet<String> = config
77            .trait_bridges
78            .iter()
79            .filter_map(|b| b.param_name.clone())
80            .collect();
81        let bridge_type_aliases: HashSet<String> = config
82            .trait_bridges
83            .iter()
84            .filter_map(|b| b.type_alias.clone())
85            .collect();
86        let has_visitor_bridge = !config.trait_bridges.is_empty();
87
88        let mut files = Vec::new();
89
90        // 0. package-info.java - required by Checkstyle
91        let description = config
92            .scaffold
93            .as_ref()
94            .and_then(|s| s.description.as_deref())
95            .unwrap_or("High-performance HTML to Markdown converter.");
96        files.push(GeneratedFile {
97            path: base_path.join("package-info.java"),
98            content: format!(
99                "/**\n * {description}\n */\npackage {package};\n",
100                description = description,
101                package = package,
102            ),
103            generated_header: true,
104        });
105
106        // 1. NativeLib.java - FFI method handles
107        files.push(GeneratedFile {
108            path: base_path.join("NativeLib.java"),
109            content: gen_native_lib(api, config, &package, &prefix, has_visitor_bridge),
110            generated_header: true,
111        });
112
113        // 2. Main wrapper class
114        files.push(GeneratedFile {
115            path: base_path.join(format!("{}.java", main_class)),
116            content: gen_main_class(
117                api,
118                config,
119                &package,
120                &main_class,
121                &prefix,
122                &bridge_param_names,
123                &bridge_type_aliases,
124                has_visitor_bridge,
125            ),
126            generated_header: true,
127        });
128
129        // 3. Exception class
130        files.push(GeneratedFile {
131            path: base_path.join(format!("{}Exception.java", main_class)),
132            content: gen_exception_class(&package, &main_class),
133            generated_header: true,
134        });
135
136        // Collect complex enums (enums with data variants and no serde tag) — use Object for these fields.
137        // Tagged unions (serde_tag is set) are now generated as proper sealed interfaces
138        // and can be deserialized as their concrete types, so they are NOT complex_enums.
139        let complex_enums: AHashSet<String> = api
140            .enums
141            .iter()
142            .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
143            .map(|e| e.name.clone())
144            .collect();
145
146        // Resolve language-level serde rename strategy (always wins over IR type-level).
147        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
148
149        // 4. Record types
150        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
151            if !typ.is_opaque && !typ.fields.is_empty() {
152                // Skip types that gen_visitor handles with richer visitor-specific versions
153                if has_visitor_bridge && (typ.name == "NodeContext" || typ.name == "VisitResult") {
154                    continue;
155                }
156                files.push(GeneratedFile {
157                    path: base_path.join(format!("{}.java", typ.name)),
158                    content: gen_record_type(&package, typ, &complex_enums, &lang_rename_all),
159                    generated_header: true,
160                });
161                // Generate builder class for types with defaults
162                if typ.has_default {
163                    files.push(GeneratedFile {
164                        path: base_path.join(format!("{}Builder.java", typ.name)),
165                        content: gen_builder_class(&package, typ),
166                        generated_header: true,
167                    });
168                }
169            }
170        }
171
172        // Collect builder class names generated from record types with defaults,
173        // so we can skip opaque types that would collide with them.
174        let builder_class_names: AHashSet<String> = api
175            .types
176            .iter()
177            .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
178            .map(|t| format!("{}Builder", t.name))
179            .collect();
180
181        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
182        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
183            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
184                files.push(GeneratedFile {
185                    path: base_path.join(format!("{}.java", typ.name)),
186                    content: gen_opaque_handle_class(&package, typ, &prefix),
187                    generated_header: true,
188                });
189            }
190        }
191
192        // 5. Enums
193        for enum_def in &api.enums {
194            // Skip enums that gen_visitor handles with richer visitor-specific versions
195            if has_visitor_bridge && enum_def.name == "VisitResult" {
196                continue;
197            }
198            files.push(GeneratedFile {
199                path: base_path.join(format!("{}.java", enum_def.name)),
200                content: gen_enum_class(&package, enum_def),
201                generated_header: true,
202            });
203        }
204
205        // 6. Error exception classes
206        for error in &api.errors {
207            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
208                files.push(GeneratedFile {
209                    path: base_path.join(format!("{}.java", class_name)),
210                    content,
211                    generated_header: true,
212                });
213            }
214        }
215
216        // 7. Visitor support files (when a trait bridge is configured)
217        if has_visitor_bridge {
218            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
219                files.push(GeneratedFile {
220                    path: base_path.join(filename),
221                    content,
222                    generated_header: false, // already has header comment
223                });
224            }
225        }
226
227        // Build adapter body map (consumed by generators via body substitution)
228        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Java)?;
229
230        Ok(files)
231    }
232
233    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
234        let package = config.java_package();
235        let prefix = config.ffi_prefix();
236        let main_class = Self::resolve_main_class(api);
237        let package_path = package.replace('.', "/");
238
239        let output_dir = resolve_output_dir(
240            config.output.java.as_ref(),
241            &config.crate_config.name,
242            "packages/java/src/main/java/",
243        );
244
245        let base_path = PathBuf::from(&output_dir).join(&package_path);
246
247        // Collect bridge param names/aliases to strip from the public facade.
248        let bridge_param_names: HashSet<String> = config
249            .trait_bridges
250            .iter()
251            .filter_map(|b| b.param_name.clone())
252            .collect();
253        let bridge_type_aliases: HashSet<String> = config
254            .trait_bridges
255            .iter()
256            .filter_map(|b| b.type_alias.clone())
257            .collect();
258        let has_visitor_bridge = !config.trait_bridges.is_empty();
259
260        // Generate a high-level public API class that wraps the raw FFI class.
261        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
262        let public_class = main_class.trim_end_matches("Rs").to_string();
263        let facade_content = gen_facade_class(
264            api,
265            &package,
266            &public_class,
267            &main_class,
268            &prefix,
269            &bridge_param_names,
270            &bridge_type_aliases,
271            has_visitor_bridge,
272        );
273
274        Ok(vec![GeneratedFile {
275            path: base_path.join(format!("{}.java", public_class)),
276            content: facade_content,
277            generated_header: true,
278        }])
279    }
280
281    fn build_config(&self) -> Option<BuildConfig> {
282        Some(BuildConfig {
283            tool: "mvn",
284            crate_suffix: "",
285            depends_on_ffi: true,
286            post_build: vec![],
287        })
288    }
289}