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