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::{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#[derive(Clone, Debug)]
28pub(crate) struct OptionsFieldBridgeInfo {
29 pub options_type: String,
31 pub field_name: String,
33 pub bridge_java_type: String,
36}
37
38fn 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 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 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 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 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 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
143
144 let options_field_bridges = collect_options_field_bridges(config);
146
147 let mut files = Vec::new();
148
149 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 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 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 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 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 let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
215
216 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 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 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 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 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 for enum_def in &api.enums {
266 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 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 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, });
296 }
297 }
298
299 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 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 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 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 let has_visitor_pattern = config.ffi.as_ref().map(|f| f.visitor_callbacks).unwrap_or(false);
368
369 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}