use crate::type_map::{java_boxed_type, java_type};
use ahash::AHashSet;
use alef_codegen::naming::to_class_name;
use alef_core::config::{AdapterConfig, AdapterPattern};
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::{DefaultValue, EnumDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
use heck::{ToLowerCamelCase, ToSnakeCase};
use super::helpers::{
RECORD_LINE_WRAP_THRESHOLD, emit_javadoc, escape_javadoc_line, format_optional_value, is_tuple_field_name,
java_apply_rename_all, safe_java_field_name,
};
pub(crate) fn gen_record_type(
package: &str,
typ: &TypeDef,
complex_enums: &AHashSet<String>,
sealed_unions_with_unwrapped: &AHashSet<String>,
lang_rename_all: &str,
has_visitor_pattern: bool,
) -> String {
let mut fields_joined = String::with_capacity(typ.fields.len().saturating_mul(42));
let mut field_decls: Vec<String> = Vec::with_capacity(typ.fields.len());
for (i, f) in typ.fields.iter().enumerate() {
let is_complex = matches!(&f.ty, TypeRef::Named(n) if complex_enums.contains(n.as_str()));
let is_visitor_field = has_visitor_pattern && typ.name == "ConversionOptions" && f.name == "visitor";
let ftype = if is_visitor_field {
"Visitor".to_string()
} else if is_complex {
"Object".to_string()
} else if f.optional {
java_boxed_type(&f.ty).to_string()
} else {
java_type(&f.ty).to_string()
};
let jname = safe_java_field_name(&f.name);
let needs_non_null = !f.optional && matches!(&f.ty, TypeRef::Vec(_));
let needs_bytes_int_serialize = !f.optional && matches!(&f.ty, TypeRef::Bytes);
let has_json_property = (lang_rename_all == "camelCase" && f.name.contains('_')) || f.serde_rename.is_some();
let json_property_name = f.serde_rename.clone().unwrap_or_else(|| f.name.clone());
let has_nullable = f.optional;
let mut decl = String::new();
let field_type_name = match &f.ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
Some(n.as_str())
} else {
None
}
}
_ => None,
};
if let Some(type_name) = field_type_name {
if sealed_unions_with_unwrapped.contains(type_name) {
decl.push_str("@JsonDeserialize(using = ");
decl.push_str(type_name);
decl.push_str("Deserializer.class) ");
}
}
if is_visitor_field {
decl.push_str("@JsonIgnore ");
}
if needs_bytes_int_serialize {
decl.push_str("@JsonSerialize(using = ByteArrayToIntArraySerializer.class) ");
}
let nullable_at_leading_pos = has_nullable && !ftype.contains('.');
if nullable_at_leading_pos {
decl.push_str("@Nullable ");
}
if needs_non_null {
decl.push_str("@JsonInclude(JsonInclude.Include.NON_NULL) ");
}
if has_json_property && !is_visitor_field {
decl.push_str("@JsonProperty(\"");
decl.push_str(&json_property_name);
decl.push_str("\") ");
}
if has_nullable && !nullable_at_leading_pos {
if let Some(idx) = ftype.rfind('.') {
let (pkg, simple) = ftype.split_at(idx);
let simple = simple.trim_start_matches('.');
decl.push_str(pkg);
decl.push_str(".@Nullable ");
decl.push_str(simple);
decl.push(' ');
decl.push_str(&jname);
} else {
decl.push_str("@Nullable ");
decl.push_str(&ftype);
decl.push(' ');
decl.push_str(&jname);
}
} else {
decl.push_str(&ftype);
decl.push(' ');
decl.push_str(&jname);
}
if i > 0 {
fields_joined.push_str(", ");
}
fields_joined.push_str(&decl);
field_decls.push(decl);
}
let single_line_len = "public record ".len() + typ.name.len() + 1 + fields_joined.len() + ") { }".len();
let mut record_block = String::new();
emit_javadoc(&mut record_block, &typ.doc, "");
if typ.has_serde {
record_block.push_str("@JsonInclude(JsonInclude.Include.NON_ABSENT)\n");
}
if typ.has_default {
record_block.push_str("@JsonDeserialize(builder = ");
record_block.push_str(&typ.name);
record_block.push_str("Builder.class)\n");
}
if single_line_len > RECORD_LINE_WRAP_THRESHOLD && typ.fields.len() > 1 {
record_block.push_str("public record ");
record_block.push_str(&typ.name);
record_block.push_str("(\n");
for (i, decl) in field_decls.iter().enumerate() {
let comma = if i < field_decls.len() - 1 { "," } else { "" };
record_block.push_str(" ");
record_block.push_str(decl);
record_block.push_str(comma);
record_block.push('\n');
}
record_block.push_str(") {\n");
} else {
record_block.push_str("public record ");
record_block.push_str(&typ.name);
record_block.push('(');
record_block.push_str(&fields_joined);
record_block.push_str(") {\n");
}
if typ.has_default {
record_block.push_str(" public static ");
record_block.push_str(&typ.name);
record_block.push_str("Builder builder() {\n");
record_block.push_str(" return new ");
record_block.push_str(&typ.name);
record_block.push_str("Builder();\n");
record_block.push_str(" }\n");
}
record_block.push_str("\n /**\n");
record_block.push_str(" * Parse a {@code ");
record_block.push_str(&typ.name);
record_block.push_str("} from a JSON string.\n");
record_block.push_str(" *\n");
record_block.push_str(" * @param json JSON serialisation matching the Rust-side field names (snake_case).\n");
record_block.push_str(" * @throws RuntimeException if the JSON cannot be deserialised.\n");
record_block.push_str(" */\n");
record_block.push_str(" public static ");
record_block.push_str(&typ.name);
record_block.push_str(" fromJson(String json) {\n");
record_block.push_str(" try {\n");
record_block.push_str(" return new com.fasterxml.jackson.databind.ObjectMapper()\n");
record_block.push_str(" .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
record_block.push_str(" .findAndRegisterModules()\n");
record_block.push_str(
" .setPropertyNamingStrategy(com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE)\n",
);
record_block.push_str(
" .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)\n",
);
record_block.push_str(
" .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)\n",
);
record_block.push_str(" .readValue(json, ");
record_block.push_str(&typ.name);
record_block.push_str(".class);\n");
record_block.push_str(" } catch (Exception e) {\n");
record_block.push_str(" throw new RuntimeException(\"Failed to parse ");
record_block.push_str(&typ.name);
record_block.push_str(" from JSON\", e);\n");
record_block.push_str(" }\n");
record_block.push_str(" }\n");
let compact_ctor_lines: Vec<String> = typ
.fields
.iter()
.filter(|f| !f.optional)
.filter_map(|f| {
let jname = safe_java_field_name(&f.name);
match &f.typed_default {
Some(DefaultValue::IntLiteral(n)) if *n != 0 => {
let suffix = if matches!(f.ty, TypeRef::Duration) { "L" } else { "" };
Some(format!(" if ({jname} == 0) {jname} = {n}{suffix};"))
}
_ => None,
}
})
.collect();
if !compact_ctor_lines.is_empty() {
record_block.push_str(" public ");
record_block.push_str(&typ.name);
record_block.push_str("{\n");
for line in &compact_ctor_lines {
record_block.push_str(line);
record_block.push('\n');
}
record_block.push_str(" }\n");
}
record_block.push_str("}\n");
let needs_json_property = fields_joined.contains("@JsonProperty(");
let needs_json_include = fields_joined.contains("@JsonInclude(") || record_block.contains("@JsonInclude(");
let needs_json_deserialize =
record_block.contains("@JsonDeserialize(") || fields_joined.contains("@JsonDeserialize(");
let needs_json_serialize = fields_joined.contains("@JsonSerialize(");
let needs_json_ignore = fields_joined.contains("@JsonIgnore");
let needs_nullable = fields_joined.contains("@Nullable");
let _needs_transient = fields_joined.contains("@Transient");
let needs_optional = fields_joined.contains("Optional<");
let mut imports: Vec<&str> = vec![];
if fields_joined.contains("List<") {
imports.push("java.util.List");
}
if fields_joined.contains("Map<") {
imports.push("java.util.Map");
}
if needs_optional {
imports.push("java.util.Optional");
}
if needs_json_property {
imports.push("com.fasterxml.jackson.annotation.JsonProperty");
}
if needs_json_include {
imports.push("com.fasterxml.jackson.annotation.JsonInclude");
}
if needs_json_deserialize {
imports.push("com.fasterxml.jackson.databind.annotation.JsonDeserialize");
}
if needs_json_serialize {
imports.push("com.fasterxml.jackson.databind.annotation.JsonSerialize");
}
if needs_json_ignore {
imports.push("com.fasterxml.jackson.annotation.JsonIgnore");
}
if needs_nullable {
imports.push("org.jspecify.annotations.Nullable");
}
let header = hash::header(CommentStyle::DoubleSlash);
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
out.push_str(&record_block);
out
}
pub(crate) fn gen_enum_class(package: &str, enum_def: &EnumDef) -> String {
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
if enum_def.serde_tag.is_some() && has_data_variants {
return gen_java_tagged_union(package, enum_def);
}
if enum_def.serde_untagged && has_data_variants {
return gen_java_untagged_wrapper(package, enum_def);
}
let header = hash::header(CommentStyle::DoubleSlash);
let imports = [
"com.fasterxml.jackson.annotation.JsonCreator",
"com.fasterxml.jackson.annotation.JsonValue",
];
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
emit_javadoc(&mut out, &enum_def.doc, "");
out.push_str("public enum ");
out.push_str(&enum_def.name);
out.push_str(" {\n");
for (i, variant) in enum_def.variants.iter().enumerate() {
let comma = if i < enum_def.variants.len() - 1 { "," } else { ";" };
let json_name = variant
.serde_rename
.clone()
.unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
if !variant.doc.is_empty() {
let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
if doc_summary.len() + 11 > 80 {
out.push_str(" /**\n");
out.push_str(" * ");
out.push_str(&doc_summary);
out.push('\n');
out.push_str(" */\n");
} else {
out.push_str(" /** ");
out.push_str(&doc_summary);
out.push_str(" */\n");
}
}
out.push_str(" ");
out.push_str(&variant.name);
out.push_str("(\"");
out.push_str(&json_name);
out.push_str("\")");
out.push_str(comma);
out.push('\n');
}
out.push('\n');
out.push_str(" /** The string value. */\n");
out.push_str(" private final String value;\n");
out.push('\n');
out.push_str(" ");
out.push_str(&enum_def.name);
out.push_str("(final String value) {\n");
out.push_str(" this.value = value;\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" /** Returns the string value. */\n");
out.push_str(" @JsonValue\n");
out.push_str(" public String getValue() {\n");
out.push_str(" return value;\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" /** Creates an instance from a string value. */\n");
out.push_str(" @JsonCreator\n");
out.push_str(" public static ");
out.push_str(&enum_def.name);
out.push_str(" fromValue(final String value) {\n");
out.push_str(" for (");
out.push_str(&enum_def.name);
out.push_str(" e : values()) {\n");
out.push_str(" if (e.value.equalsIgnoreCase(value)) {\n");
out.push_str(" return e;\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" throw new IllegalArgumentException(\"Unknown value: \" + value);\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_java_untagged_wrapper(package: &str, enum_def: &EnumDef) -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let doc = enum_def
.doc
.lines()
.next()
.map(|line| escape_javadoc_line(line.trim()))
.unwrap_or_default();
crate::template_env::render(
"untagged_union_wrapper.jinja",
minijinja::context! {
header => header,
package => package,
class_name => &enum_def.name,
doc => doc,
},
)
}
pub(crate) fn gen_java_tagged_union(package: &str, enum_def: &EnumDef) -> String {
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let variant_names: std::collections::HashSet<&str> = enum_def.variants.iter().map(|v| v.name.as_str()).collect();
let optional_type = if variant_names.contains("Optional") {
"java.util.Optional"
} else {
"Optional"
};
let needs_json_property = enum_def
.variants
.iter()
.any(|v| v.fields.iter().any(|f| !is_tuple_field_name(&f.name)));
let has_data_variants = enum_def
.variants
.iter()
.any(|v| !v.fields.is_empty() && is_tuple_field_name(&v.fields[0].name));
let needs_list = !variant_names.contains("List")
&& enum_def
.variants
.iter()
.any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Vec(_))));
let needs_map = !variant_names.contains("Map")
&& enum_def
.variants
.iter()
.any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))));
let needs_optional =
!variant_names.contains("Optional") && enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.optional));
let needs_unwrapped = enum_def
.variants
.iter()
.any(|v| v.fields.len() == 1 && is_tuple_field_name(&v.fields[0].name));
let mut imports: Vec<&str> = vec![];
if needs_json_property {
imports.push("com.fasterxml.jackson.annotation.JsonProperty");
}
if !needs_unwrapped {
imports.push("com.fasterxml.jackson.annotation.JsonSubTypes");
imports.push("com.fasterxml.jackson.annotation.JsonTypeInfo");
}
if needs_list {
imports.push("java.util.List");
}
if needs_map {
imports.push("java.util.Map");
}
if needs_optional {
imports.push("java.util.Optional");
}
if needs_unwrapped {
imports.push("com.fasterxml.jackson.databind.deser.std.StdDeserializer");
imports.push("com.fasterxml.jackson.databind.ser.std.StdSerializer");
imports.push("com.fasterxml.jackson.core.JsonParser");
imports.push("com.fasterxml.jackson.core.JsonGenerator");
imports.push("com.fasterxml.jackson.databind.DeserializationContext");
imports.push("com.fasterxml.jackson.databind.SerializerProvider");
imports.push("com.fasterxml.jackson.databind.node.ObjectNode");
imports.push("com.fasterxml.jackson.databind.annotation.JsonDeserialize");
imports.push("com.fasterxml.jackson.databind.annotation.JsonSerialize");
}
if has_data_variants {
imports.push("org.jspecify.annotations.Nullable");
}
let header = hash::header(CommentStyle::DoubleSlash);
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
emit_javadoc(&mut out, &enum_def.doc, "");
if !needs_unwrapped {
out.push_str("@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"");
out.push_str(tag_field);
out.push_str("\", visible = false)\n");
out.push_str("@JsonSubTypes({\n");
for (i, variant) in enum_def.variants.iter().enumerate() {
let discriminator = variant
.serde_rename
.clone()
.unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
let comma = if i < enum_def.variants.len() - 1 { "," } else { "" };
out.push_str(" @JsonSubTypes.Type(value = ");
out.push_str(&enum_def.name);
out.push('.');
out.push_str(&variant.name);
out.push_str(".class, name = \"");
out.push_str(&discriminator);
out.push_str("\")");
out.push_str(comma);
out.push('\n');
}
out.push_str("})\n");
}
out.push_str("@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)\n");
if needs_unwrapped {
out.push_str("@JsonDeserialize(using = ");
out.push_str(&enum_def.name);
out.push_str("Deserializer.class)\n");
out.push_str("@JsonSerialize(using = ");
out.push_str(&enum_def.name);
out.push_str("Serializer.class)\n");
}
out.push_str("public sealed interface ");
out.push_str(&enum_def.name);
out.push_str(" {\n");
for variant in &enum_def.variants {
out.push('\n');
if variant.fields.is_empty() {
if !variant.doc.is_empty() {
let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
out.push_str(" /** ");
out.push_str(&doc_summary);
out.push_str(" */\n");
}
out.push_str(" record ");
out.push_str(&variant.name);
out.push_str("() implements ");
out.push_str(&enum_def.name);
out.push_str(" {\n");
out.push_str(" }\n");
} else {
let field_parts: Vec<String> = variant
.fields
.iter()
.map(|f| {
let ftype = if f.optional {
let inner = java_boxed_type(&f.ty);
let inner_str = inner.as_ref();
let inner_qualified = if inner_str.starts_with("List<") && variant_names.contains("List") {
inner_str.replacen("List<", "java.util.List<", 1)
} else if inner_str.starts_with("Map<") && variant_names.contains("Map") {
inner_str.replacen("Map<", "java.util.Map<", 1)
} else {
inner_str.to_string()
};
format!("{optional_type}<{inner_qualified}>")
} else {
let t = java_type(&f.ty);
let t_str = t.as_ref();
if t_str.starts_with("List<") && variant_names.contains("List") {
t_str.replacen("List<", "java.util.List<", 1)
} else if t_str.starts_with("Map<") && variant_names.contains("Map") {
t_str.replacen("Map<", "java.util.Map<", 1)
} else {
t_str.to_string()
}
};
if is_tuple_field_name(&f.name) {
format!("{ftype} value")
} else {
let json_name = f.name.trim_start_matches('_');
let jname = safe_java_field_name(json_name);
format!("@JsonProperty(\"{json_name}\") {ftype} {jname}")
}
})
.collect();
let fields_joined: String = field_parts.join(", ");
let single_len = " record ".len()
+ variant.name.len()
+ 1
+ fields_joined.len()
+ ") implements ".len()
+ enum_def.name.len()
+ " { }".len();
if !variant.doc.is_empty() {
let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
out.push_str(" /** ");
out.push_str(&doc_summary);
out.push_str(" */\n");
}
if single_len > RECORD_LINE_WRAP_THRESHOLD && field_parts.len() > 1 {
out.push_str(" record ");
out.push_str(&variant.name);
out.push_str("(\n");
for (i, fp) in field_parts.iter().enumerate() {
let comma = if i < field_parts.len() - 1 { "," } else { "" };
out.push_str(" ");
out.push_str(fp);
out.push_str(comma);
out.push('\n');
}
out.push_str(" ) implements ");
out.push_str(&enum_def.name);
out.push_str(" {\n");
out.push_str(" }\n");
} else {
out.push_str(" record ");
out.push_str(&variant.name);
out.push('(');
out.push_str(&fields_joined);
out.push_str(") implements ");
out.push_str(&enum_def.name);
out.push_str(" { }\n");
}
}
}
if has_data_variants {
out.push('\n');
for variant in &enum_def.variants {
if variant.fields.is_empty() || !is_tuple_field_name(&variant.fields[0].name) {
continue;
}
let method_name = variant.name.to_lower_camel_case();
let return_type = java_boxed_type(&variant.fields[0].ty);
let variant_name = &variant.name;
out.push_str(" /** Returns the ");
out.push_str(variant_name);
out.push_str(" data if this is a ");
out.push_str(variant_name);
out.push_str(" variant, otherwise null. */\n");
out.push_str(" default @Nullable ");
out.push_str(return_type.as_ref());
out.push(' ');
out.push_str(&method_name);
out.push_str("() {\n");
out.push_str(" return this instanceof ");
out.push_str(variant_name);
out.push_str(" e ? e.value() : null;\n");
out.push_str(" }\n");
out.push('\n');
}
}
out.push_str("}\n");
if needs_unwrapped {
out.push('\n');
gen_sealed_union_deserializer(&mut out, package, enum_def, tag_field);
out.push('\n');
gen_sealed_union_serializer(&mut out, package, enum_def, tag_field);
}
out
}
pub(crate) fn gen_opaque_handle_class(
package: &str,
typ: &TypeDef,
prefix: &str,
adapters: &[AdapterConfig],
main_class: &str,
) -> String {
let class_name = &typ.name;
let type_snake = class_name.to_snake_case();
let header = hash::header(CommentStyle::DoubleSlash);
let streaming_adapters: Vec<&AdapterConfig> = adapters
.iter()
.filter(|a| {
matches!(a.pattern, AdapterPattern::Streaming)
&& a.owner_type.as_deref() == Some(class_name.as_str())
&& a.item_type.is_some()
&& a.params.first().is_some_and(|p| !p.ty.is_empty())
})
.collect();
let has_streaming = !streaming_adapters.is_empty();
let streaming_method_names: AHashSet<String> = streaming_adapters.iter().map(|a| a.name.to_snake_case()).collect();
let instance_methods: Vec<&MethodDef> = typ
.methods
.iter()
.filter(|m| !m.is_static)
.filter(|m| !streaming_method_names.contains(&m.name.to_snake_case()))
.collect();
let has_instance_methods = !instance_methods.is_empty();
let needs_helpers = has_streaming || has_instance_methods;
let mut imports: Vec<&str> = vec!["java.lang.foreign.MemorySegment"];
if needs_helpers {
imports.push("java.lang.foreign.Arena");
imports.push("java.lang.foreign.ValueLayout");
imports.push("com.fasterxml.jackson.databind.ObjectMapper");
}
if has_streaming {
imports.push("java.util.Iterator");
imports.push("java.util.NoSuchElementException");
}
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
emit_javadoc(&mut out, &typ.doc, "");
out.push_str("public class ");
out.push_str(class_name);
out.push_str(" implements AutoCloseable {\n");
out.push_str(" private final MemorySegment handle;\n");
out.push('\n');
out.push_str(" ");
out.push_str(class_name);
out.push_str("(MemorySegment handle) {\n");
out.push_str(" this.handle = handle;\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" MemorySegment handle() {\n");
out.push_str(" return this.handle;\n");
out.push_str(" }\n");
out.push('\n');
for adapter in &streaming_adapters {
gen_streaming_method(&mut out, adapter, prefix, &type_snake, main_class);
}
for method in &instance_methods {
gen_instance_method(&mut out, method, prefix, &type_snake, main_class);
}
out.push_str(" @Override\n");
out.push_str(" public void close() {\n");
out.push_str(" if (handle != null && !handle.equals(MemorySegment.NULL)) {\n");
out.push_str(" try {\n");
out.push_str(" NativeLib.");
out.push_str(&prefix.to_uppercase());
out.push('_');
out.push_str(&type_snake.to_uppercase());
out.push_str("_FREE.invoke(handle);\n");
out.push_str(" } catch (Throwable e) {\n");
out.push_str(" throw new RuntimeException(\"Failed to free ");
out.push_str(class_name);
out.push_str(": \" + e.getMessage(), e);\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" }\n");
if needs_helpers {
gen_streaming_helpers(&mut out, prefix, main_class);
}
out.push_str("}\n");
out
}
fn gen_instance_method(out: &mut String, method: &MethodDef, prefix: &str, owner_snake: &str, main_class: &str) {
let method_name = method.name.to_lower_camel_case();
let prefix_upper = prefix.to_uppercase();
let owner_upper = owner_snake.to_uppercase();
let method_upper = method.name.to_snake_case().to_uppercase();
let exception_class = format!("{main_class}Exception");
let ffi_handle = format!("NativeLib.{prefix_upper}_{owner_upper}_{method_upper}");
let params_sig: Vec<String> = method
.params
.iter()
.map(|p| {
let ptype = if p.optional {
java_boxed_type(&p.ty).to_string()
} else {
java_type(&p.ty).to_string()
};
format!("final {} {}", ptype, p.name.to_lower_camel_case())
})
.collect();
let is_bytes_result = method.error_type.is_some()
&& (matches!(method.return_type, TypeRef::Bytes)
|| matches!(&method.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Bytes)));
let (is_optional_return, dispatch_return) = match &method.return_type {
TypeRef::Optional(inner) => (true, (**inner).clone()),
other => (false, other.clone()),
};
let return_type_java = if is_bytes_result {
if is_optional_return {
"java.util.Optional<byte[]>"
} else {
"byte[]"
}
.to_string()
} else {
java_type(&method.return_type).to_string()
};
out.push_str(" public ");
out.push_str(&return_type_java);
out.push(' ');
out.push_str(&method_name);
out.push('(');
out.push_str(¶ms_sig.join(", "));
out.push_str(") throws ");
out.push_str(&exception_class);
out.push_str(" {\n");
for p in &method.params {
if !p.optional && param_needs_null_check(&p.ty) {
let pname = p.name.to_lower_camel_case();
out.push_str(&format!(
" java.util.Objects.requireNonNull({pname}, \"{pname} must not be null\");\n"
));
}
}
out.push_str(" try (var arena = Arena.ofConfined()) {\n");
let mut named_ptr_frees: Vec<(String, String)> = Vec::new();
let mut call_args: Vec<String> = Vec::new();
for p in &method.params {
let pname = p.name.to_lower_camel_case();
let cname = format!("c{}", to_class_name(&p.name));
match &p.ty {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
out.push_str(&format!(" var {cname} = arena.allocateFrom({pname});\n"));
call_args.push(cname);
}
TypeRef::Optional(inner)
if matches!(
inner.as_ref(),
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json
) =>
{
out.push_str(&format!(
" MemorySegment {cname} = ({pname} == null) ? MemorySegment.NULL : arena.allocateFrom({pname});\n"
));
call_args.push(cname);
}
TypeRef::Named(type_name) => {
let req_snake = type_name.to_snake_case();
let req_upper = req_snake.to_uppercase();
let from_json = format!("NativeLib.{prefix_upper}_{req_upper}_FROM_JSON");
let req_free = format!("NativeLib.{prefix_upper}_{req_upper}_FREE");
out.push_str(&format!(
" String {cname}Json = STREAM_MAPPER.writeValueAsString({pname});\n"
));
out.push_str(&format!(
" var {cname}JsonSeg = arena.allocateFrom({cname}Json);\n"
));
out.push_str(&format!(
" MemorySegment {cname} = (MemorySegment) {from_json}.invoke({cname}JsonSeg);\n"
));
out.push_str(&format!(
" if ({cname}.equals(MemorySegment.NULL)) {{ checkLastFfiError(); throw new {exception_class}(\"{method_name}: failed to marshal {pname}\", (Throwable) null); }}\n"
));
named_ptr_frees.push((cname.clone(), req_free));
call_args.push(cname);
}
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
let type_name = match inner.as_ref() {
TypeRef::Named(n) => n,
_ => unreachable!(),
};
let req_snake = type_name.to_snake_case();
let req_upper = req_snake.to_uppercase();
let from_json = format!("NativeLib.{prefix_upper}_{req_upper}_FROM_JSON");
let req_free = format!("NativeLib.{prefix_upper}_{req_upper}_FREE");
out.push_str(&format!(" MemorySegment {cname};\n"));
out.push_str(&format!(
" if ({pname} == null) {{ {cname} = MemorySegment.NULL; }} else {{\n"
));
out.push_str(&format!(
" String {cname}Json = STREAM_MAPPER.writeValueAsString({pname});\n"
));
out.push_str(&format!(
" var {cname}JsonSeg = arena.allocateFrom({cname}Json);\n"
));
out.push_str(&format!(
" {cname} = (MemorySegment) {from_json}.invoke({cname}JsonSeg);\n"
));
out.push_str(&format!(
" if ({cname}.equals(MemorySegment.NULL)) {{ checkLastFfiError(); throw new {exception_class}(\"{method_name}: failed to marshal {pname}\", (Throwable) null); }}\n"
));
out.push_str(" }\n");
named_ptr_frees.push((cname.clone(), req_free));
call_args.push(cname);
}
TypeRef::Primitive(_) | TypeRef::Duration => {
call_args.push(pname);
}
_ => {
out.push_str(&format!(" // TODO unsupported parameter type for {pname}\n"));
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: unsupported parameter shape\", (Throwable) null);\n"
));
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" if (e instanceof {exception_class} ex) {{ throw ex; }}\n"
));
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: failed\", e);\n"
));
out.push_str(" }\n");
out.push_str(" }\n\n");
return;
}
}
}
let emit_named_frees = |out: &mut String, indent: &str| {
for (cname, free_handle) in &named_ptr_frees {
out.push_str(&format!(
"{indent}if (!{cname}.equals(MemorySegment.NULL)) {{ try {{ {free_handle}.invoke({cname}); }} catch (Throwable ignore) {{}} }}\n"
));
}
};
let mut call_args_full = vec!["this.handle".to_string()];
call_args_full.extend(call_args);
let args_joined = call_args_full.join(", ");
if is_bytes_result {
let free_bytes = format!("NativeLib.{prefix_upper}_FREE_BYTES");
out.push_str(" var outPtrHolder = arena.allocate(ValueLayout.ADDRESS);\n");
out.push_str(" var outLenHolder = arena.allocate(ValueLayout.JAVA_LONG);\n");
out.push_str(" var outCapHolder = arena.allocate(ValueLayout.JAVA_LONG);\n");
out.push_str(&format!(
" int rc = (int) {ffi_handle}.invoke({args_joined}, outPtrHolder, outLenHolder, outCapHolder);\n"
));
emit_named_frees(out, " ");
out.push_str(" if (rc != 0) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(&format!(
" {}\n",
if is_optional_return {
"return java.util.Optional.empty();"
} else {
"return null;"
}
));
out.push_str(" }\n");
out.push_str(" var outPtr = outPtrHolder.get(ValueLayout.ADDRESS, 0);\n");
out.push_str(" long outLen = outLenHolder.get(ValueLayout.JAVA_LONG, 0);\n");
out.push_str(" long outCap = outCapHolder.get(ValueLayout.JAVA_LONG, 0);\n");
out.push_str(" if (outPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(&format!(
" {}\n",
if is_optional_return {
"return java.util.Optional.empty();"
} else {
"return null;"
}
));
out.push_str(" }\n");
out.push_str(" byte[] result = outPtr.reinterpret(outLen).toArray(ValueLayout.JAVA_BYTE);\n");
out.push_str(&format!(" {free_bytes}.invoke(outPtr, outLen, outCap);\n"));
out.push_str(&format!(
" return {};\n",
if is_optional_return {
"java.util.Optional.of(result)"
} else {
"result"
}
));
} else if matches!(dispatch_return, TypeRef::Named(_)) {
let return_type_name = match &dispatch_return {
TypeRef::Named(n) => n.clone(),
_ => unreachable!(),
};
let ret_snake = return_type_name.to_snake_case();
let ret_upper = ret_snake.to_uppercase();
let ret_free = format!("NativeLib.{prefix_upper}_{ret_upper}_FREE");
let ret_to_json = format!("NativeLib.{prefix_upper}_{ret_upper}_TO_JSON");
out.push_str(&format!(
" MemorySegment resultPtr = (MemorySegment) {ffi_handle}.invoke({args_joined});\n"
));
emit_named_frees(out, " ");
out.push_str(" if (resultPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(" return null;\n");
out.push_str(" }\n");
out.push_str(" try {\n");
out.push_str(&format!(
" MemorySegment jsonPtr = (MemorySegment) {ret_to_json}.invoke(resultPtr);\n"
));
out.push_str(" if (jsonPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: failed to serialize response\", (Throwable) null);\n"
));
out.push_str(" }\n");
out.push_str(" String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);\n");
out.push_str(&format!(
" NativeLib.{prefix_upper}_FREE_STRING.invoke(jsonPtr);\n"
));
out.push_str(&format!(
" return STREAM_MAPPER.readValue(json, {return_type_name}.class);\n"
));
out.push_str(" } finally {\n");
out.push_str(&format!(" {ret_free}.invoke(resultPtr);\n"));
out.push_str(" }\n");
} else if matches!(dispatch_return, TypeRef::Unit) {
out.push_str(&format!(" {ffi_handle}.invoke({args_joined});\n"));
emit_named_frees(out, " ");
out.push_str(" checkLastFfiError();\n");
} else {
emit_named_frees(out, " ");
out.push_str(&format!(
" // TODO unsupported return shape for {method_name}\n"
));
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: unsupported return shape\", (Throwable) null);\n"
));
}
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" if (e instanceof {exception_class} ex) {{ throw ex; }}\n"
));
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: failed\", e);\n"
));
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn param_needs_null_check(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::String
| TypeRef::Char
| TypeRef::Path
| TypeRef::Json
| TypeRef::Named(_)
| TypeRef::Bytes
| TypeRef::Vec(_)
| TypeRef::Map(_, _)
)
}
fn gen_streaming_method(out: &mut String, adapter: &AdapterConfig, prefix: &str, owner_snake: &str, main_class: &str) {
let method_name = adapter.name.to_lower_camel_case();
let item_type = adapter.item_type.as_deref().unwrap_or("Object");
let request_type_full = adapter.params[0].ty.as_str();
let request_type = request_type_full.rsplit("::").next().unwrap_or(request_type_full);
let request_snake = request_type.to_snake_case();
let prefix_upper = prefix.to_uppercase();
let owner_upper = owner_snake.to_uppercase();
let adapter_upper = adapter.name.to_snake_case().to_uppercase();
let request_upper = request_snake.to_uppercase();
let item_snake = item_type.to_snake_case();
let item_upper = item_snake.to_uppercase();
let exception_class = format!("{main_class}Exception");
let request_param = adapter.params[0].name.to_lower_camel_case();
let request_param = if request_param.is_empty() {
"request".to_string()
} else {
request_param
};
let start_handle = format!("{prefix_upper}_{owner_upper}_{adapter_upper}_START");
let next_handle = format!("{prefix_upper}_{owner_upper}_{adapter_upper}_NEXT");
let free_handle = format!("{prefix_upper}_{owner_upper}_{adapter_upper}_FREE");
let req_from_json = format!("{prefix_upper}_{request_upper}_FROM_JSON");
let req_free = format!("{prefix_upper}_{request_upper}_FREE");
let item_to_json = format!("{prefix_upper}_{item_upper}_TO_JSON");
let item_free = format!("{prefix_upper}_{item_upper}_FREE");
out.push_str(&format!(
" public Iterator<{item_type}> {method_name}(final {request_type} {request_param}) throws {exception_class} {{\n"
));
out.push_str(&format!(
" java.util.Objects.requireNonNull({request_param}, \"{request_param} must not be null\");\n"
));
out.push_str(" final MemorySegment streamHandle;\n");
out.push_str(" try (var arena = Arena.ofConfined()) {\n");
out.push_str(&format!(
" String requestJson = STREAM_MAPPER.writeValueAsString({request_param});\n"
));
out.push_str(" var cRequestJson = arena.allocateFrom(requestJson);\n");
out.push_str(&format!(
" MemorySegment requestPtr = (MemorySegment) NativeLib.{req_from_json}.invoke(cRequestJson);\n"
));
out.push_str(" if (requestPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: failed to marshal request\", (Throwable) null);\n"
));
out.push_str(" }\n");
out.push_str(" try {\n");
out.push_str(&format!(
" streamHandle = (MemorySegment) NativeLib.{start_handle}.invoke(this.handle, requestPtr);\n"
));
out.push_str(" } finally {\n");
out.push_str(&format!(" NativeLib.{req_free}.invoke(requestPtr);\n"));
out.push_str(" }\n");
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" if (e instanceof {exception_class} ex) {{ throw ex; }}\n"
));
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: failed to start stream\", e);\n"
));
out.push_str(" }\n");
out.push_str(" if (streamHandle == null || streamHandle.equals(MemorySegment.NULL)) {\n");
out.push_str(" checkLastFfiError();\n");
out.push_str(&format!(
" throw new {exception_class}(\"{method_name}: stream handle was null\", (Throwable) null);\n"
));
out.push_str(" }\n");
out.push_str(" final MemorySegment finalStreamHandle = streamHandle;\n");
out.push_str(&format!(" return new Iterator<{item_type}>() {{\n"));
out.push_str(&format!(" private {item_type} pending = pull();\n"));
out.push_str(" private boolean closed = false;\n");
out.push('\n');
out.push_str(&format!(" private {item_type} pull() {{\n"));
out.push_str(" if (closed) { return null; }\n");
out.push_str(" MemorySegment chunkPtr;\n");
out.push_str(" try {\n");
out.push_str(&format!(
" chunkPtr = (MemorySegment) NativeLib.{next_handle}.invoke(finalStreamHandle);\n"
));
out.push_str(" } catch (Throwable e) {\n");
out.push_str(" closeStream();\n");
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(\"{method_name}: stream advance failed\", e));\n"
));
out.push_str(" }\n");
out.push_str(" if (chunkPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(" closeStream();\n");
out.push_str(" int code;\n");
out.push_str(" try {\n");
out.push_str(&format!(
" code = (int) NativeLib.{prefix_upper}_LAST_ERROR_CODE.invoke();\n"
));
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(\"{method_name}: failed to read last_error_code\", e));\n"
));
out.push_str(" }\n");
out.push_str(" if (code != 0) {\n");
out.push_str(" String msg;\n");
out.push_str(" try {\n");
out.push_str(&format!(
" MemorySegment ctxPtr = (MemorySegment) NativeLib.{prefix_upper}_LAST_ERROR_CONTEXT.invoke();\n"
));
out.push_str(
" msg = ctxPtr.equals(MemorySegment.NULL) ? \"unknown\" : ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);\n",
);
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(code, \"{method_name}: failed to read error context\"));\n"
));
out.push_str(" }\n");
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(code, msg));\n"
));
out.push_str(" }\n");
out.push_str(" return null;\n");
out.push_str(" }\n");
out.push_str(" try {\n");
out.push_str(&format!(
" MemorySegment jsonPtr = (MemorySegment) NativeLib.{item_to_json}.invoke(chunkPtr);\n"
));
out.push_str(" if (jsonPtr.equals(MemorySegment.NULL)) {\n");
out.push_str(&format!(
" NativeLib.{item_free}.invoke(chunkPtr);\n"
));
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(\"{method_name}: failed to serialize chunk\", (Throwable) null));\n"
));
out.push_str(" }\n");
out.push_str(" String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);\n");
out.push_str(&format!(
" NativeLib.{prefix_upper}_FREE_STRING.invoke(jsonPtr);\n"
));
out.push_str(&format!(
" NativeLib.{item_free}.invoke(chunkPtr);\n"
));
out.push_str(&format!(
" return STREAM_MAPPER.readValue(json, {item_type}.class);\n"
));
out.push_str(" } catch (Throwable e) {\n");
out.push_str(" closeStream();\n");
out.push_str(&format!(
" throw new RuntimeException(new {exception_class}(\"{method_name}: failed to deserialize chunk\", e));\n"
));
out.push_str(" }\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" private void closeStream() {\n");
out.push_str(" if (closed) { return; }\n");
out.push_str(" closed = true;\n");
out.push_str(" try {\n");
out.push_str(&format!(
" NativeLib.{free_handle}.invoke(finalStreamHandle);\n"
));
out.push_str(" } catch (Throwable ignore) {\n");
out.push_str(" // best-effort cleanup\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" @Override\n");
out.push_str(" public boolean hasNext() {\n");
out.push_str(" return pending != null;\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" @Override\n");
out.push_str(&format!(" public {item_type} next() {{\n"));
out.push_str(" if (pending == null) {\n");
out.push_str(" throw new NoSuchElementException();\n");
out.push_str(" }\n");
out.push_str(&format!(" {item_type} current = pending;\n"));
out.push_str(" pending = pull();\n");
out.push_str(" return current;\n");
out.push_str(" }\n");
out.push_str(" };\n");
out.push_str(" }\n");
out.push('\n');
}
fn gen_streaming_helpers(out: &mut String, prefix: &str, main_class: &str) {
let prefix_upper = prefix.to_uppercase();
let exception_class = format!("{main_class}Exception");
out.push('\n');
out.push_str(&format!(
" private void checkLastFfiError() throws {exception_class} {{\n"
));
out.push_str(" try {\n");
out.push_str(&format!(
" int code = (int) NativeLib.{prefix_upper}_LAST_ERROR_CODE.invoke();\n"
));
out.push_str(" if (code == 0) { return; }\n");
out.push_str(&format!(
" MemorySegment ctxPtr = (MemorySegment) NativeLib.{prefix_upper}_LAST_ERROR_CONTEXT.invoke();\n"
));
out.push_str(
" String msg = ctxPtr.equals(MemorySegment.NULL) ? \"unknown\" : ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);\n",
);
out.push_str(&format!(" throw new {exception_class}(code, msg);\n"));
out.push_str(" } catch (Throwable e) {\n");
out.push_str(&format!(
" if (e instanceof {exception_class} ex) {{ throw ex; }}\n"
));
out.push_str(&format!(
" throw new {exception_class}(\"failed to read last error\", e);\n"
));
out.push_str(" }\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" private static final ObjectMapper STREAM_MAPPER = new ObjectMapper()\n");
out.push_str(" .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
out.push_str(" .findAndRegisterModules()\n");
out.push_str(
" .setPropertyNamingStrategy(com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE)\n",
);
out.push_str(" .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)\n");
out.push_str(
" .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);\n",
);
}
pub(crate) fn gen_builder_class(package: &str, typ: &TypeDef, has_visitor_pattern: bool) -> String {
let mut body = String::with_capacity(2048);
emit_javadoc(&mut body, &typ.doc, "");
body.push_str("@JsonPOJOBuilder(withPrefix = \"with\")\n");
body.push_str("public class ");
body.push_str(&typ.name);
body.push_str("Builder {\n");
body.push('\n');
for field in &typ.fields {
let field_name = safe_java_field_name(&field.name);
if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
|| field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
{
continue;
}
let is_visitor_field = has_visitor_pattern && typ.name == "ConversionOptions" && field.name == "visitor";
let field_type = if is_visitor_field {
"Optional<Visitor>".to_string()
} else if field.optional {
format!("Optional<{}>", java_boxed_type(&field.ty))
} else if matches!(field.ty, TypeRef::Duration) {
java_boxed_type(&field.ty).to_string()
} else {
java_type(&field.ty).to_string()
};
let default_value = if is_visitor_field {
"Optional.empty()".to_string()
} else if field.optional {
if let Some(default) = &field.default {
format_optional_value(&field.ty, default)
} else {
"Optional.empty()".to_string()
}
} else {
if let Some(default) = &field.default {
default.clone()
} else {
match &field.ty {
TypeRef::String | TypeRef::Char | TypeRef::Path => {
match &field.typed_default {
Some(DefaultValue::StringLiteral(s)) => {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
format!("\"{escaped}\"")
}
_ => "\"\"".to_string(),
}
}
TypeRef::Json => "null".to_string(),
TypeRef::Bytes => "new byte[0]".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => {
match &field.typed_default {
Some(DefaultValue::BoolLiteral(b)) => b.to_string(),
_ => "false".to_string(),
}
}
PrimitiveType::F32 => "0.0f".to_string(),
PrimitiveType::F64 => "0.0".to_string(),
_ => "0".to_string(),
},
TypeRef::Vec(_) => "List.of()".to_string(),
TypeRef::Map(_, _) => "Map.of()".to_string(),
TypeRef::Optional(_) => "Optional.empty()".to_string(),
TypeRef::Duration => "null".to_string(),
_ => "null".to_string(),
}
}
};
body.push_str(" private ");
body.push_str(&field_type);
body.push(' ');
body.push_str(&field_name);
body.push_str(" = ");
body.push_str(&default_value);
body.push_str(";\n");
}
body.push('\n');
for field in &typ.fields {
if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
|| field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
{
continue;
}
let field_name = safe_java_field_name(&field.name);
let field_name_pascal = to_class_name(&field.name);
let is_visitor_field = has_visitor_pattern && typ.name == "ConversionOptions" && field.name == "visitor";
let field_type = if is_visitor_field {
"Visitor".to_string()
} else if field.optional {
format!("Optional<{}>", java_boxed_type(&field.ty))
} else if matches!(field.ty, TypeRef::Duration) {
java_boxed_type(&field.ty).to_string()
} else {
java_type(&field.ty).to_string()
};
body.push_str(" /** Sets the ");
body.push_str(&field_name);
body.push_str(" field. */\n");
body.push_str(" public ");
body.push_str(&typ.name);
body.push_str("Builder with");
body.push_str(&field_name_pascal);
body.push_str("(final ");
body.push_str(&field_type);
body.push_str(" value) {\n");
if is_visitor_field {
body.push_str(" this.");
body.push_str(&field_name);
body.push_str(" = Optional.ofNullable(value);\n");
} else {
body.push_str(" this.");
body.push_str(&field_name);
body.push_str(" = value;\n");
}
body.push_str(" return this;\n");
body.push_str(" }\n");
body.push('\n');
}
body.push_str(" /** Builds the ");
body.push_str(&typ.name);
body.push_str(" instance. */\n");
body.push_str(" public ");
body.push_str(&typ.name);
body.push_str(" build() {\n");
body.push_str(" return new ");
body.push_str(&typ.name);
body.push_str("(\n");
let non_tuple_fields: Vec<_> = typ
.fields
.iter()
.filter(|f| {
!(f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit())
|| f.name.chars().next().is_none_or(|c| c.is_ascii_digit()))
})
.collect();
for (i, field) in non_tuple_fields.iter().enumerate() {
let field_name = safe_java_field_name(&field.name);
let comma = if i < non_tuple_fields.len() - 1 { "," } else { "" };
let is_visitor_field = has_visitor_pattern && typ.name == "ConversionOptions" && field.name == "visitor";
if field.optional || is_visitor_field {
body.push_str(" ");
body.push_str(&field_name);
body.push_str(".orElse(null)");
body.push_str(comma);
body.push('\n');
} else {
body.push_str(" ");
body.push_str(&field_name);
body.push_str(comma);
body.push('\n');
}
}
body.push_str(" );\n");
body.push_str(" }\n");
body.push_str("}\n");
let mut imports: Vec<&str> = vec![];
if body.contains("List<") {
imports.push("java.util.List");
}
if body.contains("Map<") {
imports.push("java.util.Map");
}
if body.contains("Optional<") {
imports.push("java.util.Optional");
}
if body.contains("@JsonPOJOBuilder") {
imports.push("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder");
}
let header = hash::header(CommentStyle::DoubleSlash);
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
out.push_str(&body);
out
}
pub(crate) fn gen_byte_array_serializer(package: &str) -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let imports = [
"com.fasterxml.jackson.core.JsonGenerator",
"com.fasterxml.jackson.databind.SerializerProvider",
"com.fasterxml.jackson.databind.ser.std.StdSerializer",
];
let mut out = crate::template_env::render(
"java_file_header.jinja",
minijinja::context! { header => header, package => package, imports => &imports },
);
out.push('\n');
out.push_str("/**\n");
out.push_str(" * Serialises {@code byte[]} as a JSON array of integers.\n");
out.push_str(" *\n");
out.push_str(" * <p>Jackson's default serialiser encodes {@code byte[]} as a base64 string, but\n");
out.push_str(" * Rust's {@code serde} for {@code Vec<u8>} expects {@code [72, 101, 108, ...]}.\n");
out.push_str(" * Annotate any {@code byte[]} field sent to the FFI layer with\n");
out.push_str(" * {@code @JsonSerialize(using = ByteArrayToIntArraySerializer.class)}.\n");
out.push_str(" */\n");
out.push_str("public class ByteArrayToIntArraySerializer extends StdSerializer<byte[]> {\n");
out.push_str(" /** Default constructor required by Jackson. */\n");
out.push_str(" public ByteArrayToIntArraySerializer() {\n");
out.push_str(" super(byte[].class);\n");
out.push_str(" }\n\n");
out.push_str(" @Override\n");
out.push_str(" public void serialize(final byte[] value, final JsonGenerator gen,\n");
out.push_str(" final SerializerProvider provider) throws java.io.IOException {\n");
out.push_str(" gen.writeStartArray();\n");
out.push_str(" for (byte b : value) {\n");
out.push_str(" gen.writeNumber(b & 0xFF);\n");
out.push_str(" }\n");
out.push_str(" gen.writeEndArray();\n");
out.push_str(" }\n");
out.push_str("}\n");
out
}
fn gen_sealed_union_deserializer(out: &mut String, _package: &str, enum_def: &EnumDef, tag_field: &str) {
out.push_str("// Custom deserializer for sealed interface with unwrapped variants\n");
out.push_str("class ");
out.push_str(&enum_def.name);
out.push_str("Deserializer extends StdDeserializer<");
out.push_str(&enum_def.name);
out.push_str("> {\n");
out.push_str(" ");
out.push_str(&enum_def.name);
out.push_str("Deserializer() {\n");
out.push_str(" super(");
out.push_str(&enum_def.name);
out.push_str(".class);\n");
out.push_str(" }\n\n");
out.push_str(" @Override\n");
out.push_str(" public ");
out.push_str(&enum_def.name);
out.push_str(" deserialize(JsonParser parser, DeserializationContext ctx)\n");
out.push_str(" throws java.io.IOException {\n");
out.push_str(" ObjectNode node = parser.getCodec().readTree(parser);\n");
out.push_str(" com.fasterxml.jackson.databind.JsonNode tagNode = node.get(\"");
out.push_str(tag_field);
out.push_str("\");\n");
out.push_str(" if (tagNode == null || tagNode.isNull()) {\n");
out.push_str(" throw new com.fasterxml.jackson.databind.JsonMappingException(\n");
out.push_str(" parser, \"Missing discriminator field: ");
out.push_str(tag_field);
out.push_str("\");\n");
out.push_str(" }\n");
out.push_str(" String tagValue = tagNode.asText();\n");
out.push_str(" node.remove(\"");
out.push_str(tag_field);
out.push_str("\");\n\n");
out.push_str(" return switch (tagValue) {\n");
for variant in &enum_def.variants {
let discriminator = variant.serde_rename.clone().unwrap_or_else(|| {
let name = &variant.name;
enum_def
.serde_rename_all
.as_deref()
.map(|strategy| java_apply_rename_all(name, Some(strategy)))
.unwrap_or_else(|| java_apply_rename_all(name, None))
});
out.push_str(" case \"");
out.push_str(&discriminator);
out.push_str("\" -> ");
if variant.fields.is_empty() {
out.push_str("new ");
out.push_str(&enum_def.name);
out.push('.');
out.push_str(&variant.name);
out.push_str("();\n");
} else if variant.fields.len() == 1 && is_tuple_field_name(&variant.fields[0].name) {
let field = &variant.fields[0];
let inner_type = java_type(&field.ty);
out.push_str("new ");
out.push_str(&enum_def.name);
out.push('.');
out.push_str(&variant.name);
out.push_str("(ctx.readTreeAsValue(node, ");
out.push_str(inner_type.as_ref());
out.push_str(".class));\n");
} else {
out.push_str("ctx.readTreeAsValue(node, ");
out.push_str(&enum_def.name);
out.push('.');
out.push_str(&variant.name);
out.push_str(".class);\n");
}
}
out.push_str(" default -> throw new com.fasterxml.jackson.databind.JsonMappingException(\n");
out.push_str(" parser, \"Unknown ");
out.push_str(&enum_def.name);
out.push_str(" discriminator: \" + tagValue);\n");
out.push_str(" };\n");
out.push_str(" }\n");
out.push_str("}\n");
}
fn gen_sealed_union_serializer(out: &mut String, _package: &str, enum_def: &EnumDef, tag_field: &str) {
let variants: Vec<minijinja::Value> = enum_def
.variants
.iter()
.map(|v| {
let discriminator = v.serde_rename.clone().unwrap_or_else(|| {
let name = &v.name;
enum_def
.serde_rename_all
.as_deref()
.map(|strategy| java_apply_rename_all(name, Some(strategy)))
.unwrap_or_else(|| java_apply_rename_all(name, None))
});
let is_unit = v.fields.is_empty();
let is_tuple = !is_unit && v.fields.len() == 1 && is_tuple_field_name(&v.fields[0].name);
minijinja::context! {
name => &v.name,
discriminator => discriminator,
is_unit => is_unit,
is_tuple => is_tuple,
}
})
.collect();
out.push_str(&crate::template_env::render(
"sealed_union_serializer.jinja",
minijinja::context! {
class_name => &enum_def.name,
tag_field => tag_field,
variants => variants,
},
));
}