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::{BridgeBinding, Language, ResolvedCrateConfig};
5use alef_core::ir::{ApiSurface, TypeRef};
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, gen_infrastructure_exception_class};
20use native_lib::gen_native_lib;
21use types::{gen_builder_class, gen_byte_array_serializer, 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: &ResolvedCrateConfig) -> 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 = config
68            .output_for("java")
69            .map(|p| p.to_string_lossy().into_owned())
70            .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
71
72        // If output_dir already ends with the package path (user configured the full path),
73        // use it as-is. Otherwise, append the package path.
74        let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
75            PathBuf::from(&output_dir)
76        } else {
77            PathBuf::from(&output_dir).join(&package_path)
78        };
79
80        // Collect bridge param names and type aliases so we can strip them from generated
81        // function signatures and emit convertWithVisitor instead.
82        let bridge_param_names: HashSet<String> = config
83            .trait_bridges
84            .iter()
85            .filter_map(|b| b.param_name.clone())
86            .collect();
87        let bridge_type_aliases: HashSet<String> = config
88            .trait_bridges
89            .iter()
90            .filter_map(|b| b.type_alias.clone())
91            .collect();
92        // Generate visitor support when visitor_callbacks is enabled in FFI config (canonical check),
93        // OR when any trait bridge is bound via options_field (Java-specific activation path).
94        let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
95            || config
96                .trait_bridges
97                .iter()
98                .any(|b| b.bind_via == BridgeBinding::OptionsField);
99
100        let mut files = Vec::new();
101
102        // 0. package-info.java - required by Checkstyle
103        let description = config
104            .scaffold
105            .as_ref()
106            .and_then(|s| s.description.as_deref())
107            .unwrap_or("High-performance HTML to Markdown converter.");
108        files.push(GeneratedFile {
109            path: base_path.join("package-info.java"),
110            content: format!(
111                "/**\n * {description}\n */\npackage {package};\n",
112                description = description,
113                package = package,
114            ),
115            generated_header: true,
116        });
117
118        // 1. NativeLib.java - FFI method handles
119        files.push(GeneratedFile {
120            path: base_path.join("NativeLib.java"),
121            content: gen_native_lib(api, config, &package, &prefix, has_visitor_pattern),
122            generated_header: true,
123        });
124
125        // 2. Main wrapper class
126        files.push(GeneratedFile {
127            path: base_path.join(format!("{}.java", main_class)),
128            content: gen_main_class(
129                api,
130                config,
131                &package,
132                &main_class,
133                &prefix,
134                &bridge_param_names,
135                &bridge_type_aliases,
136                has_visitor_pattern,
137            ),
138            generated_header: true,
139        });
140
141        // 3. Exception class
142        files.push(GeneratedFile {
143            path: base_path.join(format!("{}Exception.java", main_class)),
144            content: gen_exception_class(&package, &main_class),
145            generated_header: true,
146        });
147
148        // 3b. Infrastructure exception classes for FFI error codes 1 and 2.
149        // These are always emitted because checkLastError() hardcodes:
150        //   case 1 -> throw new InvalidInputException(msg);
151        //   case 2 -> throw new ConversionErrorException(msg);
152        // Code 1 = null pointer / invalid UTF-8 in an input arg (invalid input).
153        // Code 2 = JSON serialisation/deserialisation failure (type conversion).
154        for (class_name, code, doc) in [
155            (
156                "InvalidInputException",
157                1i32,
158                "Exception thrown when input validation fails.",
159            ),
160            (
161                "ConversionErrorException",
162                2i32,
163                "Exception thrown when type conversion fails.",
164            ),
165        ] {
166            files.push(GeneratedFile {
167                path: base_path.join(format!("{}.java", class_name)),
168                content: gen_infrastructure_exception_class(&package, &main_class, class_name, code, doc),
169                generated_header: true,
170            });
171        }
172
173        // Untagged unions with data variants now emit as JsonNode-wrapper classes
174        // (see gen_java_untagged_wrapper). The set is intentionally empty so that
175        // record fields keep their wrapper type instead of being downcast to Object.
176        let complex_enums: AHashSet<String> = AHashSet::new();
177
178        // Collect sealed union types with unwrapped/tuple variants that need custom deserializers.
179        // When a record field references one of these types, we need to add a @JsonDeserialize
180        // annotation to the field so Jackson uses the custom deserializer.
181        let sealed_unions_with_unwrapped: AHashSet<String> = api
182            .enums
183            .iter()
184            .filter(|e| {
185                e.serde_tag.is_some()
186                    && e.variants
187                        .iter()
188                        .any(|v| v.fields.len() == 1 && helpers::is_tuple_field_name(&v.fields[0].name))
189            })
190            .map(|e| e.name.clone())
191            .collect();
192
193        // Resolve language-level serde rename strategy (always wins over IR type-level).
194        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
195
196        // 4. Record types
197        // Include non-opaque types that either have fields OR are serializable unit structs
198        // (has_serde + has_default, empty fields). Unit structs like `ExcelMetadata` need a
199        // concrete Java class so they can be referenced as record components in tagged-union
200        // variant records (e.g. FormatMetadata.Excel(@JsonUnwrapped ExcelMetadata value)).
201        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
202            let is_unit_serde = !typ.is_opaque && typ.fields.is_empty() && typ.has_serde;
203            if !typ.is_opaque && (!typ.fields.is_empty() || is_unit_serde) {
204                // Skip types that gen_visitor handles with richer visitor-specific versions
205                if has_visitor_pattern && (typ.name == "NodeContext" || typ.name == "VisitResult") {
206                    continue;
207                }
208                files.push(GeneratedFile {
209                    path: base_path.join(format!("{}.java", typ.name)),
210                    content: gen_record_type(
211                        &package,
212                        typ,
213                        &complex_enums,
214                        &sealed_unions_with_unwrapped,
215                        &lang_rename_all,
216                        has_visitor_pattern,
217                        &main_class,
218                    ),
219                    generated_header: true,
220                });
221                // Generate builder class for types with defaults
222                if typ.has_default {
223                    files.push(GeneratedFile {
224                        path: base_path.join(format!("{}Builder.java", typ.name)),
225                        content: gen_builder_class(&package, typ, has_visitor_pattern),
226                        generated_header: true,
227                    });
228                }
229            }
230        }
231
232        // 4a. Utility serializer for byte[] → JSON int-array (needed when any record
233        // has a non-optional Bytes field). Jackson's default byte[] serialiser emits
234        // base64, which Rust's serde Vec<u8> cannot accept. Emit the class once.
235        let needs_bytes_serializer = api
236            .types
237            .iter()
238            .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
239        if needs_bytes_serializer {
240            files.push(GeneratedFile {
241                path: base_path.join("ByteArrayToIntArraySerializer.java"),
242                content: gen_byte_array_serializer(&package),
243                generated_header: true,
244            });
245        }
246
247        // Collect builder class names generated from record types with defaults,
248        // so we can skip opaque types that would collide with them.
249        let builder_class_names: AHashSet<String> = api
250            .types
251            .iter()
252            .filter(|t| !t.is_opaque && (!t.fields.is_empty() || (t.has_serde && t.fields.is_empty())) && t.has_default)
253            .map(|t| format!("{}Builder", t.name))
254            .collect();
255
256        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
257        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
258            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
259                files.push(GeneratedFile {
260                    path: base_path.join(format!("{}.java", typ.name)),
261                    content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
262                    generated_header: true,
263                });
264            }
265        }
266
267        // 5. Enums
268        for enum_def in &api.enums {
269            // Skip enums that gen_visitor handles with richer visitor-specific versions
270            if has_visitor_pattern && enum_def.name == "VisitResult" {
271                continue;
272            }
273            files.push(GeneratedFile {
274                path: base_path.join(format!("{}.java", enum_def.name)),
275                content: gen_enum_class(&package, enum_def, &main_class),
276                generated_header: true,
277            });
278        }
279
280        // 6. Error exception classes
281        for error in &api.errors {
282            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
283                files.push(GeneratedFile {
284                    path: base_path.join(format!("{}.java", class_name)),
285                    content,
286                    generated_header: true,
287                });
288            }
289        }
290
291        // 7. Visitor support files (only when ConversionOptions/ConversionResult types exist)
292        if has_visitor_pattern {
293            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
294                files.push(GeneratedFile {
295                    path: base_path.join(filename),
296                    content,
297                    generated_header: false, // already has header comment
298                });
299            }
300        }
301
302        // 8. Trait bridge plugin registration files
303        // Emits two files per trait: I{Trait}.java (managed interface) and
304        // {Trait}Bridge.java (Panama upcall stubs + register/unregister helpers).
305        //
306        // Set of struct + enum names that get a generated companion Java class.
307        // Trait method signatures referencing types outside this set (e.g. excluded
308        // internal types like `InternalDocument`) are JSON-bridged as Strings.
309        let visible_type_names: HashSet<&str> = api
310            .types
311            .iter()
312            .filter(|t| !t.is_trait)
313            .map(|t| t.name.as_str())
314            .chain(api.enums.iter().map(|e| e.name.as_str()))
315            .collect();
316        for bridge_cfg in &config.trait_bridges {
317            if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
318                continue;
319            }
320
321            // When visitor_callbacks is active, visitor traits bound via options_field are
322            // surfaced through Visitor.java + VisitorBridge.java (generated by gen_visitor_files).
323            // The raw trait bridge I{Trait}.java emitted here would be an unreferenced orphan
324            // with snake_case method names. Suppress it for options_field-bound visitor traits.
325            if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
326                continue;
327            }
328
329            if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
330                let has_super_trait = bridge_cfg.super_trait.is_some();
331                let trait_bridge::BridgeFiles {
332                    interface_content,
333                    bridge_content,
334                } = trait_bridge::gen_trait_bridge_files(
335                    trait_def,
336                    &prefix,
337                    &package,
338                    has_super_trait,
339                    bridge_cfg.unregister_fn.as_deref(),
340                    bridge_cfg.clear_fn.as_deref(),
341                    &visible_type_names,
342                );
343
344                files.push(GeneratedFile {
345                    path: base_path.join(format!("I{}.java", trait_def.name)),
346                    content: interface_content,
347                    generated_header: true,
348                });
349                files.push(GeneratedFile {
350                    path: base_path.join(format!("{}Bridge.java", trait_def.name)),
351                    content: bridge_content,
352                    generated_header: true,
353                });
354            }
355        }
356
357        Ok(files)
358    }
359
360    fn generate_public_api(
361        &self,
362        api: &ApiSurface,
363        config: &ResolvedCrateConfig,
364    ) -> anyhow::Result<Vec<GeneratedFile>> {
365        let package = config.java_package();
366        let prefix = config.ffi_prefix();
367        let main_class = Self::resolve_main_class(api);
368        let package_path = package.replace('.', "/");
369
370        let output_dir = config
371            .output_for("java")
372            .map(|p| p.to_string_lossy().into_owned())
373            .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
374
375        // If output_dir already ends with the package path (user configured the full path),
376        // use it as-is. Otherwise, append the package path.
377        let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
378            PathBuf::from(&output_dir)
379        } else {
380            PathBuf::from(&output_dir).join(&package_path)
381        };
382
383        // Collect bridge param names/aliases to strip from the public facade.
384        let bridge_param_names: HashSet<String> = config
385            .trait_bridges
386            .iter()
387            .filter_map(|b| b.param_name.clone())
388            .collect();
389        let bridge_type_aliases: HashSet<String> = config
390            .trait_bridges
391            .iter()
392            .filter_map(|b| b.type_alias.clone())
393            .collect();
394        let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
395            || config
396                .trait_bridges
397                .iter()
398                .any(|b| b.bind_via == BridgeBinding::OptionsField);
399
400        // Generate a high-level public API class that wraps the raw FFI class.
401        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
402        let public_class = main_class.trim_end_matches("Rs").to_string();
403        let facade_content = gen_facade_class(
404            api,
405            &package,
406            &public_class,
407            &main_class,
408            &prefix,
409            &bridge_param_names,
410            &bridge_type_aliases,
411            has_visitor_pattern,
412        );
413
414        Ok(vec![GeneratedFile {
415            path: base_path.join(format!("{}.java", public_class)),
416            content: facade_content,
417            generated_header: true,
418        }])
419    }
420
421    fn build_config(&self) -> Option<BuildConfig> {
422        Some(BuildConfig {
423            tool: "mvn",
424            crate_suffix: "",
425            build_dep: BuildDependency::Ffi,
426            post_build: vec![],
427        })
428    }
429}