alef_backend_java/gen_bindings/
mod.rs1use 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 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 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 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 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 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 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 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 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 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 let complex_enums: AHashSet<String> = api
178 .enums
179 .iter()
180 .filter(|e| e.serde_untagged && e.variants.iter().any(|v| !v.fields.is_empty()))
181 .map(|e| e.name.clone())
182 .collect();
183
184 let sealed_unions_with_unwrapped: AHashSet<String> = api
188 .enums
189 .iter()
190 .filter(|e| {
191 e.serde_tag.is_some()
192 && e.variants
193 .iter()
194 .any(|v| v.fields.len() == 1 && helpers::is_tuple_field_name(&v.fields[0].name))
195 })
196 .map(|e| e.name.clone())
197 .collect();
198
199 let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
201
202 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
208 let is_unit_serde = !typ.is_opaque && typ.fields.is_empty() && typ.has_serde;
209 if !typ.is_opaque && (!typ.fields.is_empty() || is_unit_serde) {
210 if has_visitor_pattern && (typ.name == "NodeContext" || typ.name == "VisitResult") {
212 continue;
213 }
214 files.push(GeneratedFile {
215 path: base_path.join(format!("{}.java", typ.name)),
216 content: gen_record_type(
217 &package,
218 typ,
219 &complex_enums,
220 &sealed_unions_with_unwrapped,
221 &lang_rename_all,
222 has_visitor_pattern,
223 ),
224 generated_header: true,
225 });
226 if typ.has_default {
228 files.push(GeneratedFile {
229 path: base_path.join(format!("{}Builder.java", typ.name)),
230 content: gen_builder_class(&package, typ, has_visitor_pattern),
231 generated_header: true,
232 });
233 }
234 }
235 }
236
237 let needs_bytes_serializer = api
241 .types
242 .iter()
243 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
244 if needs_bytes_serializer {
245 files.push(GeneratedFile {
246 path: base_path.join("ByteArrayToIntArraySerializer.java"),
247 content: gen_byte_array_serializer(&package),
248 generated_header: true,
249 });
250 }
251
252 let builder_class_names: AHashSet<String> = api
255 .types
256 .iter()
257 .filter(|t| !t.is_opaque && (!t.fields.is_empty() || (t.has_serde && t.fields.is_empty())) && t.has_default)
258 .map(|t| format!("{}Builder", t.name))
259 .collect();
260
261 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
263 if typ.is_opaque && !builder_class_names.contains(&typ.name) {
264 files.push(GeneratedFile {
265 path: base_path.join(format!("{}.java", typ.name)),
266 content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
267 generated_header: true,
268 });
269 }
270 }
271
272 for enum_def in &api.enums {
274 if has_visitor_pattern && enum_def.name == "VisitResult" {
276 continue;
277 }
278 files.push(GeneratedFile {
279 path: base_path.join(format!("{}.java", enum_def.name)),
280 content: gen_enum_class(&package, enum_def),
281 generated_header: true,
282 });
283 }
284
285 for error in &api.errors {
287 for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
288 files.push(GeneratedFile {
289 path: base_path.join(format!("{}.java", class_name)),
290 content,
291 generated_header: true,
292 });
293 }
294 }
295
296 if has_visitor_pattern {
298 for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
299 files.push(GeneratedFile {
300 path: base_path.join(filename),
301 content,
302 generated_header: false, });
304 }
305 }
306
307 for bridge_cfg in &config.trait_bridges {
311 if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
312 continue;
313 }
314
315 if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
320 continue;
321 }
322
323 if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
324 let has_super_trait = bridge_cfg.super_trait.is_some();
325 let trait_bridge::BridgeFiles {
326 interface_content,
327 bridge_content,
328 } = trait_bridge::gen_trait_bridge_files(trait_def, &prefix, &package, has_super_trait);
329
330 files.push(GeneratedFile {
331 path: base_path.join(format!("I{}.java", trait_def.name)),
332 content: interface_content,
333 generated_header: true,
334 });
335 files.push(GeneratedFile {
336 path: base_path.join(format!("{}Bridge.java", trait_def.name)),
337 content: bridge_content,
338 generated_header: true,
339 });
340 }
341 }
342
343 Ok(files)
344 }
345
346 fn generate_public_api(
347 &self,
348 api: &ApiSurface,
349 config: &ResolvedCrateConfig,
350 ) -> anyhow::Result<Vec<GeneratedFile>> {
351 let package = config.java_package();
352 let prefix = config.ffi_prefix();
353 let main_class = Self::resolve_main_class(api);
354 let package_path = package.replace('.', "/");
355
356 let output_dir = config
357 .output_for("java")
358 .map(|p| p.to_string_lossy().into_owned())
359 .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
360
361 let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
364 PathBuf::from(&output_dir)
365 } else {
366 PathBuf::from(&output_dir).join(&package_path)
367 };
368
369 let bridge_param_names: HashSet<String> = config
371 .trait_bridges
372 .iter()
373 .filter_map(|b| b.param_name.clone())
374 .collect();
375 let bridge_type_aliases: HashSet<String> = config
376 .trait_bridges
377 .iter()
378 .filter_map(|b| b.type_alias.clone())
379 .collect();
380 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
381 || config
382 .trait_bridges
383 .iter()
384 .any(|b| b.bind_via == BridgeBinding::OptionsField);
385
386 let public_class = main_class.trim_end_matches("Rs").to_string();
389 let facade_content = gen_facade_class(
390 api,
391 &package,
392 &public_class,
393 &main_class,
394 &prefix,
395 &bridge_param_names,
396 &bridge_type_aliases,
397 has_visitor_pattern,
398 );
399
400 Ok(vec![GeneratedFile {
401 path: base_path.join(format!("{}.java", public_class)),
402 content: facade_content,
403 generated_header: true,
404 }])
405 }
406
407 fn build_config(&self) -> Option<BuildConfig> {
408 Some(BuildConfig {
409 tool: "mvn",
410 crate_suffix: "",
411 build_dep: BuildDependency::Ffi,
412 post_build: vec![],
413 })
414 }
415}