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