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 line_wrap;
13mod marshal;
14mod native_lib;
15mod trait_bridge;
16mod types;
17
18use facade::gen_facade_class;
19use ffi_class::gen_main_class;
20use helpers::{gen_exception_class, gen_infrastructure_exception_class};
21use native_lib::gen_native_lib;
22use types::{gen_builder_class, gen_byte_array_serializer, gen_enum_class, gen_opaque_handle_class, gen_record_type};
23
24pub struct JavaBackend;
25
26impl JavaBackend {
27 fn resolve_main_class(api: &ApiSurface) -> String {
33 let base = to_class_name(&api.crate_name.replace('-', "_"));
34 if base.ends_with("Rs") {
35 base
36 } else {
37 format!("{}Rs", base)
38 }
39 }
40}
41
42impl Backend for JavaBackend {
43 fn name(&self) -> &str {
44 "java"
45 }
46
47 fn language(&self) -> Language {
48 Language::Java
49 }
50
51 fn capabilities(&self) -> Capabilities {
52 Capabilities {
53 supports_async: true,
54 supports_classes: true,
55 supports_enums: true,
56 supports_option: true,
57 supports_result: true,
58 ..Capabilities::default()
59 }
60 }
61
62 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
63 let package = config.java_package();
64 let prefix = config.ffi_prefix();
65 let main_class = Self::resolve_main_class(api);
66 let package_path = package.replace('.', "/");
67
68 let output_dir = config
69 .output_for("java")
70 .map(|p| p.to_string_lossy().into_owned())
71 .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
72
73 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 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 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
96 || config
97 .trait_bridges
98 .iter()
99 .any(|b| b.bind_via == BridgeBinding::OptionsField);
100 let bridge_associated_types = config.bridge_associated_types();
101
102 let mut files = Vec::new();
103
104 let description = config
106 .scaffold
107 .as_ref()
108 .and_then(|s| s.description.as_deref())
109 .unwrap_or("High-performance HTML to Markdown converter.");
110 files.push(GeneratedFile {
111 path: base_path.join("package-info.java"),
112 content: format!(
113 "/**\n * {description}\n */\npackage {package};\n",
114 description = description,
115 package = package,
116 ),
117 generated_header: true,
118 });
119
120 files.push(GeneratedFile {
122 path: base_path.join("NativeLib.java"),
123 content: gen_native_lib(api, config, &package, &prefix, has_visitor_pattern),
124 generated_header: true,
125 });
126
127 files.push(GeneratedFile {
129 path: base_path.join(format!("{}.java", main_class)),
130 content: gen_main_class(
131 api,
132 config,
133 &package,
134 &main_class,
135 &prefix,
136 &bridge_param_names,
137 &bridge_type_aliases,
138 has_visitor_pattern,
139 ),
140 generated_header: true,
141 });
142
143 files.push(GeneratedFile {
145 path: base_path.join(format!("{}Exception.java", main_class)),
146 content: gen_exception_class(&package, &main_class),
147 generated_header: true,
148 });
149
150 for (class_name, code, doc) in [
157 (
158 "InvalidInputException",
159 1i32,
160 "Exception thrown when input validation fails.",
161 ),
162 (
163 "ConversionErrorException",
164 2i32,
165 "Exception thrown when type conversion fails.",
166 ),
167 ] {
168 files.push(GeneratedFile {
169 path: base_path.join(format!("{}.java", class_name)),
170 content: gen_infrastructure_exception_class(&package, &main_class, class_name, code, doc),
171 generated_header: true,
172 });
173 }
174
175 let complex_enums: AHashSet<String> = AHashSet::new();
179
180 let sealed_unions_with_unwrapped: AHashSet<String> = api
184 .enums
185 .iter()
186 .filter(|e| {
187 e.serde_tag.is_some()
188 && e.variants
189 .iter()
190 .any(|v| v.fields.len() == 1 && helpers::is_tuple_field_name(&v.fields[0].name))
191 })
192 .map(|e| e.name.clone())
193 .collect();
194
195 let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
197
198 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
204 let is_unit_serde = !typ.is_opaque && typ.fields.is_empty() && typ.has_serde;
205 if !typ.is_opaque && (!typ.fields.is_empty() || is_unit_serde) {
206 if has_visitor_pattern && bridge_associated_types.contains(typ.name.as_str()) {
208 continue;
209 }
210 files.push(GeneratedFile {
211 path: base_path.join(format!("{}.java", typ.name)),
212 content: gen_record_type(
213 &package,
214 typ,
215 &complex_enums,
216 &sealed_unions_with_unwrapped,
217 &lang_rename_all,
218 has_visitor_pattern,
219 &main_class,
220 ),
221 generated_header: true,
222 });
223 if typ.has_default {
225 files.push(GeneratedFile {
226 path: base_path.join(format!("{}Builder.java", typ.name)),
227 content: gen_builder_class(&package, typ, has_visitor_pattern),
228 generated_header: true,
229 });
230 }
231 }
232 }
233
234 let needs_bytes_serializer = api
238 .types
239 .iter()
240 .any(|t| !t.is_opaque && t.fields.iter().any(|f| !f.optional && matches!(f.ty, TypeRef::Bytes)));
241 if needs_bytes_serializer {
242 files.push(GeneratedFile {
243 path: base_path.join("ByteArrayToIntArraySerializer.java"),
244 content: gen_byte_array_serializer(&package),
245 generated_header: true,
246 });
247 }
248
249 let builder_class_names: AHashSet<String> = api
252 .types
253 .iter()
254 .filter(|t| !t.is_opaque && (!t.fields.is_empty() || (t.has_serde && t.fields.is_empty())) && t.has_default)
255 .map(|t| format!("{}Builder", t.name))
256 .collect();
257
258 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
260 if typ.is_opaque && !builder_class_names.contains(&typ.name) {
261 files.push(GeneratedFile {
262 path: base_path.join(format!("{}.java", typ.name)),
263 content: gen_opaque_handle_class(&package, typ, &prefix, &config.adapters, &main_class),
264 generated_header: true,
265 });
266 }
267 }
268
269 for enum_def in &api.enums {
271 if has_visitor_pattern && bridge_associated_types.contains(enum_def.name.as_str()) {
273 continue;
274 }
275 files.push(GeneratedFile {
276 path: base_path.join(format!("{}.java", enum_def.name)),
277 content: gen_enum_class(&package, enum_def, &main_class),
278 generated_header: true,
279 });
280 }
281
282 let infrastructure_exception_names: AHashSet<&str> = ["InvalidInputException", "ConversionErrorException"]
290 .into_iter()
291 .collect();
292 for error in &api.errors {
293 for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
294 if infrastructure_exception_names.contains(class_name.as_str()) {
295 continue;
296 }
297 files.push(GeneratedFile {
298 path: base_path.join(format!("{}.java", class_name)),
299 content,
300 generated_header: true,
301 });
302 }
303 }
304
305 if has_visitor_pattern {
307 for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
308 files.push(GeneratedFile {
309 path: base_path.join(filename),
310 content,
311 generated_header: false, });
313 }
314 }
315
316 let visible_type_names: HashSet<&str> = api
324 .types
325 .iter()
326 .filter(|t| !t.is_trait)
327 .map(|t| t.name.as_str())
328 .chain(api.enums.iter().map(|e| e.name.as_str()))
329 .collect();
330 for bridge_cfg in &config.trait_bridges {
331 if bridge_cfg.exclude_languages.contains(&Language::Java.to_string()) {
332 continue;
333 }
334
335 if has_visitor_pattern && bridge_cfg.bind_via == BridgeBinding::OptionsField {
340 continue;
341 }
342
343 if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name && t.is_trait) {
344 let has_super_trait = bridge_cfg.super_trait.is_some();
345 let trait_bridge::BridgeFiles {
346 interface_content,
347 bridge_content,
348 } = trait_bridge::gen_trait_bridge_files(
349 trait_def,
350 &prefix,
351 &package,
352 has_super_trait,
353 bridge_cfg.unregister_fn.as_deref(),
354 bridge_cfg.clear_fn.as_deref(),
355 &visible_type_names,
356 );
357
358 files.push(GeneratedFile {
359 path: base_path.join(format!("I{}.java", trait_def.name)),
360 content: interface_content,
361 generated_header: true,
362 });
363 files.push(GeneratedFile {
364 path: base_path.join(format!("{}Bridge.java", trait_def.name)),
365 content: bridge_content,
366 generated_header: true,
367 });
368 }
369 }
370
371 for file in &mut files {
376 file.content = line_wrap::wrap_long_java_lines(&file.content);
377 }
378
379 Ok(files)
380 }
381
382 fn generate_public_api(
383 &self,
384 api: &ApiSurface,
385 config: &ResolvedCrateConfig,
386 ) -> anyhow::Result<Vec<GeneratedFile>> {
387 let package = config.java_package();
388 let prefix = config.ffi_prefix();
389 let main_class = Self::resolve_main_class(api);
390 let package_path = package.replace('.', "/");
391
392 let output_dir = config
393 .output_for("java")
394 .map(|p| p.to_string_lossy().into_owned())
395 .unwrap_or_else(|| "packages/java/src/main/java/".to_string());
396
397 let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
400 PathBuf::from(&output_dir)
401 } else {
402 PathBuf::from(&output_dir).join(&package_path)
403 };
404
405 let bridge_param_names: HashSet<String> = config
407 .trait_bridges
408 .iter()
409 .filter_map(|b| b.param_name.clone())
410 .collect();
411 let bridge_type_aliases: HashSet<String> = config
412 .trait_bridges
413 .iter()
414 .filter_map(|b| b.type_alias.clone())
415 .collect();
416 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false)
417 || config
418 .trait_bridges
419 .iter()
420 .any(|b| b.bind_via == BridgeBinding::OptionsField);
421 let public_class = main_class.trim_end_matches("Rs").to_string();
424 let facade_content = gen_facade_class(
425 api,
426 &package,
427 &public_class,
428 &main_class,
429 &prefix,
430 &bridge_param_names,
431 &bridge_type_aliases,
432 has_visitor_pattern,
433 );
434
435 Ok(vec![GeneratedFile {
436 path: base_path.join(format!("{}.java", public_class)),
437 content: line_wrap::wrap_long_java_lines(&facade_content),
438 generated_header: true,
439 }])
440 }
441
442 fn build_config(&self) -> Option<BuildConfig> {
443 Some(BuildConfig {
444 tool: "mvn",
445 crate_suffix: "",
446 build_dep: BuildDependency::Ffi,
447 post_build: vec![],
448 })
449 }
450}