use std::collections::BTreeSet;
use std::path::Path;
use crate::backends::kotlin::{
emit_enum_pub, emit_error_type_pub, emit_jni_bridge_object, emit_jni_client_class, emit_kdoc_pub,
emit_type_pub_with_defaults_sealed_and_constructible, kotlin_type_str_pub, to_lower_camel, to_pascal_case,
};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, TypeDef};
use crate::core::jni::bridge_class_name;
use crate::backends::kotlin_android::naming::kotlin_package;
use crate::backends::kotlin_android::template_env;
use crate::backends::kotlin_android::trait_bridge;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig, kotlin_source_dir: &Path) -> Vec<GeneratedFile> {
let package = kotlin_package(config);
let mut files = Vec::new();
let kotlin_android_excluded_function_names: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let mut effective_excluded_types: std::collections::HashSet<String> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
for bridge in &config.trait_bridges {
if bridge.exclude_languages.iter().any(|l| l == "kotlin_android") {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
if let Some(name) = bridge.param_name.as_deref() {
if kotlin_android_excluded_function_names.contains(name) {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
}
}
for (name, path) in &config.opaque_types {
if path.contains('<') {
effective_excluded_types.insert(name.clone());
}
}
let enum_defaults: std::collections::HashMap<String, String> = api
.enums
.iter()
.filter(|en| en.serde_tag.is_none() && !en.serde_untagged && en.variants.iter().all(|v| v.fields.is_empty()))
.map(|en| {
let default_variant = en
.variants
.iter()
.find(|v| v.is_default)
.map(|v| v.name.clone())
.unwrap_or_default();
(en.name.clone(), default_variant)
})
.collect();
let sealed_class_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|en| en.serde_tag.is_some() || en.serde_untagged)
.map(|en| en.name.clone())
.collect();
let default_constructible_types: std::collections::HashSet<String> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_default)
.map(|t| t.name.clone())
.collect();
let mut bridge_file = emit_jni_bridge_object(api, config);
bridge_file.path = kotlin_source_dir.join(bridge_file.path.file_name().expect("bridge file must have a filename"));
files.push(bridge_file);
let bridge = bridge_class_name(&config.name);
let exception_class = format!("{bridge}Exception");
let exception_content = format!(
"// Generated by alef. Do not edit by hand.\n\n\
package {package}\n\n\
class {exception_class}(message: String?, cause: Throwable?) : RuntimeException(message, cause) {{\n\
{}\n\
}}\n",
" constructor(message: String?) : this(message, null)"
);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{exception_class}.kt")),
content: exception_content,
generated_header: false,
});
if let Some(mut client_file) = emit_jni_client_class(api, config, Some(&package)) {
client_file.path = kotlin_source_dir.join("DefaultClient.kt");
files.push(client_file);
}
for ty in &api.types {
if ty.is_opaque || ty.is_trait || ty.binding_excluded {
continue;
}
if effective_excluded_types.contains(&ty.name) {
continue;
}
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
let needs_field_filter = ty
.fields
.iter()
.any(|f| effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
if needs_field_filter {
let mut filtered = ty.clone();
filtered
.fields
.retain(|f| !effective_excluded_types.iter().any(|name| f.ty.references_named(name)));
emit_type_pub_with_defaults_sealed_and_constructible(
&filtered,
&mut body,
&mut imports,
&enum_defaults,
&sealed_class_names,
&default_constructible_types,
);
} else {
emit_type_pub_with_defaults_sealed_and_constructible(
ty,
&mut body,
&mut imports,
&enum_defaults,
&sealed_class_names,
&default_constructible_types,
);
}
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", ty.name)),
content,
generated_header: false,
});
}
for en in &api.enums {
if en.binding_excluded {
continue;
}
let mut body = String::new();
emit_enum_pub(en, &mut body, &package);
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &BTreeSet::new(), &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", en.name)),
content,
generated_header: false,
});
}
for error in &api.errors {
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
emit_error_type_pub(error, &mut body, &mut imports);
if body.trim().is_empty() {
continue;
}
let content = assemble_kt_content(&package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{}.kt", error.name)),
content,
generated_header: false,
});
}
emit_trait_interfaces(api, config, kotlin_source_dir, &package, &mut files);
emit_module_kt(api, config, kotlin_source_dir, &package, &mut files);
files
}
fn emit_trait_interfaces(
api: &ApiSurface,
config: &ResolvedCrateConfig,
kotlin_source_dir: &Path,
package: &str,
files: &mut Vec<GeneratedFile>,
) {
let kotlin_android_excluded_function_names: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let mut effective_excluded_types: std::collections::HashSet<String> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
for bridge in &config.trait_bridges {
if bridge.exclude_languages.iter().any(|l| l == "kotlin_android") {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
if let Some(name) = bridge.param_name.as_deref() {
if kotlin_android_excluded_function_names.contains(name) {
if let Some(alias) = &bridge.type_alias {
effective_excluded_types.insert(alias.clone());
}
}
}
}
for name in api.excluded_type_paths.keys() {
effective_excluded_types.insert(name.clone());
}
for bridge in &config.trait_bridges {
if bridge
.exclude_languages
.iter()
.any(|language| language == "kotlin_android")
{
continue;
}
if let Some(param_name) = &bridge.param_name {
if kotlin_android_excluded_function_names.contains(param_name.as_str()) {
continue;
}
}
let Some(trait_def) = api
.types
.iter()
.find(|typ| typ.is_trait && typ.name == bridge.trait_name && !typ.binding_excluded)
else {
continue;
};
let interface_name = format!("I{}", trait_def.name);
let mut imports = BTreeSet::new();
let mut body = String::new();
emit_kdoc_pub(&mut body, &trait_def.doc, "");
body.push_str(&template_env::render(
"trait_interface_header.jinja",
minijinja::context! {
interface_name => interface_name,
},
));
if bridge.super_trait.is_some() {
body.push_str(" fun name(): String\n");
body.push_str(" fun version(): String\n");
body.push_str(" fun initialize() {}\n");
body.push_str(" fun shutdown() {}\n");
}
emit_trait_methods(
api,
bridge,
trait_def,
&effective_excluded_types,
&mut imports,
&mut body,
);
body.push_str("}\n");
let content = assemble_kt_content(package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{interface_name}.kt")),
content,
generated_header: false,
});
let bridge_class = bridge_class_name(&config.name);
if let Some((filename, bridge_content)) =
trait_bridge::gen_trait_bridge_object(package, &trait_def.name, bridge, trait_def, &bridge_class)
{
files.push(GeneratedFile {
path: kotlin_source_dir.join(filename),
content: bridge_content,
generated_header: false,
});
}
}
}
pub fn format_method_signature(suspend_keyword: &str, method_name: &str, params: &str, return_type: &str) -> String {
let base_sig = format!("{suspend_keyword}fun {method_name}(");
let indent = " ";
let full_sig_no_newline = format!(
"{indent}{base_sig}{params}{}{}",
if return_type == "Unit" { "" } else { "): " },
return_type
);
const THRESHOLD: usize = 110;
if params.is_empty() || full_sig_no_newline.len() < THRESHOLD {
if return_type == "Unit" {
format!("{indent}{base_sig}{params})\n")
} else {
format!("{indent}{base_sig}{params}): {return_type}\n")
}
} else {
let mut result = format!("{indent}{base_sig}\n");
for param in params.split(", ") {
result.push_str(" ");
result.push_str(param);
result.push_str(",\n");
}
if return_type == "Unit" {
result.push_str(" )\n");
} else {
result.push_str(&template_env::render(
"trait_method_return_line.jinja",
minijinja::context! {
return_type => return_type,
},
));
}
result
}
}
fn emit_trait_methods(
api: &ApiSurface,
bridge: &TraitBridgeConfig,
trait_def: &TypeDef,
excluded_types: &std::collections::HashSet<String>,
imports: &mut BTreeSet<String>,
body: &mut String,
) {
let visible_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| !t.binding_excluded && !excluded_types.contains(&t.name))
.map(|t| t.name.as_str())
.chain(api.enums.iter().map(|e| e.name.as_str()))
.collect();
for method in &trait_def.methods {
if method.sanitized || method.is_static {
continue;
}
emit_kdoc_pub(body, &method.doc, " ");
let suspend_keyword = if method.is_async { "suspend " } else { "" };
let method_name = to_lower_camel(&method.name);
let params = method
.params
.iter()
.map(|param| {
let name = to_lower_camel(¶m.name);
let ty_ref = substitute_trait_carrier_type(api, bridge, ¶m.ty);
let ty = kotlin_type_str_visible(&ty_ref, param.optional, &visible_type_names, imports);
format!("{name}: {ty}")
})
.collect::<Vec<_>>()
.join(", ");
let return_type_ref = substitute_trait_carrier_type(api, bridge, &method.return_type);
let return_type = kotlin_type_str_visible(&return_type_ref, false, &visible_type_names, imports);
body.push_str(&format_method_signature(
suspend_keyword,
&method_name,
¶ms,
&return_type,
));
}
}
fn kotlin_type_str_visible(
ty: &crate::core::ir::TypeRef,
optional: bool,
visible_type_names: &std::collections::HashSet<&str>,
imports: &mut BTreeSet<String>,
) -> String {
match ty {
crate::core::ir::TypeRef::Named(name) if !visible_type_names.contains(name.as_str()) => {
if optional {
"String?".to_string()
} else {
"String".to_string()
}
}
crate::core::ir::TypeRef::Optional(inner) => kotlin_type_str_visible(inner, true, visible_type_names, imports),
other => kotlin_type_str_pub(other, optional, imports),
}
}
fn substitute_trait_carrier_type(api: &ApiSurface, bridge: &TraitBridgeConfig, ty: &TypeRef) -> TypeRef {
match ty {
TypeRef::Named(name) if should_project_trait_carrier(api, bridge, name) => TypeRef::Named(
bridge
.result_type
.as_ref()
.expect("checked by should_project_trait_carrier")
.clone(),
),
TypeRef::Optional(inner) => TypeRef::Optional(Box::new(substitute_trait_carrier_type(api, bridge, inner))),
TypeRef::Vec(inner) => TypeRef::Vec(Box::new(substitute_trait_carrier_type(api, bridge, inner))),
TypeRef::Map(key, value) => TypeRef::Map(
Box::new(substitute_trait_carrier_type(api, bridge, key)),
Box::new(substitute_trait_carrier_type(api, bridge, value)),
),
other => other.clone(),
}
}
fn should_project_trait_carrier(api: &ApiSurface, bridge: &TraitBridgeConfig, type_name: &str) -> bool {
bridge.context_type.as_deref() == Some(type_name)
&& bridge.result_type.is_some()
&& (api.excluded_type_paths.contains_key(type_name)
|| api
.types
.iter()
.any(|typ| typ.name == type_name && (typ.binding_excluded || typ.is_opaque)))
}
fn emit_module_kt(
api: &ApiSurface,
config: &ResolvedCrateConfig,
kotlin_source_dir: &Path,
package: &str,
files: &mut Vec<GeneratedFile>,
) {
use crate::backends::kotlin::to_lower_camel;
let module_name = to_pascal_case(&config.name);
let bridge_name = format!("{module_name}Bridge");
let opaque_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait)
.map(|t| t.name.as_str())
.collect();
let client_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static))
.map(|t| t.name.as_str())
.collect();
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let visible_functions: Vec<_> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
let handle_only_types: std::collections::BTreeMap<&str, &crate::core::ir::TypeDef> = api
.types
.iter()
.filter(|t| t.is_opaque && !t.is_trait && !client_type_names.contains(t.name.as_str()))
.map(|t| (t.name.as_str(), t))
.collect();
for (type_name, type_def) in &handle_only_types {
let class_name = *type_name;
let free_name = format!("nativeFree{}", to_pascal_case(class_name));
let mut body = String::new();
let mut imports: BTreeSet<String> = BTreeSet::new();
if !type_def.doc.is_empty() {
emit_kdoc_pub(&mut body, &type_def.doc, "");
}
body.push_str(&template_env::render(
"handle_wrapper_header.jinja",
minijinja::context! {
class_name => class_name,
bridge_name => bridge_name,
free_name => free_name,
},
));
let streaming_adapters_for_type: Vec<&crate::core::config::AdapterConfig> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "kotlin_android"))
.filter(|a| {
a.owner_type
.as_deref()
.map(|owner| owner == class_name)
.unwrap_or(false)
})
.collect();
if !streaming_adapters_for_type.is_empty() {
imports.insert("import com.fasterxml.jackson.databind.ObjectMapper".to_string());
imports.insert("import com.fasterxml.jackson.datatype.jdk8.Jdk8Module".to_string());
imports.insert("import com.fasterxml.jackson.databind.PropertyNamingStrategies".to_string());
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.flow.Flow".to_string());
imports.insert("import kotlinx.coroutines.flow.callbackFlow".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
imports.insert("import kotlinx.coroutines.channels.awaitClose".to_string());
body.push_str("\n private val mapper = ObjectMapper()\n");
body.push_str(" .registerModule(Jdk8Module())\n");
body.push_str(" .findAndRegisterModules()\n");
body.push_str(" .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)\n\n");
for adapter in &streaming_adapters_for_type {
let method_name = to_lower_camel(&adapter.name);
let item_type = adapter.item_type.as_deref().unwrap_or("Any");
let owner_pascal = to_pascal_case(class_name);
let adapter_pascal = to_pascal_case(&adapter.name);
let jni_start = format!("native{owner_pascal}{adapter_pascal}Start");
let jni_next = format!("native{owner_pascal}{adapter_pascal}Next");
let jni_free = format!("native{owner_pascal}{adapter_pascal}Free");
let params: Vec<String> = adapter
.params
.iter()
.map(|p| {
let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
let param_name = to_lower_camel(&p.name);
format!("{param_name}: {simple_ty}")
})
.collect();
let first_param_name = adapter
.params
.first()
.map(|p| to_lower_camel(&p.name))
.unwrap_or_else(|| "request".to_string());
body.push_str(&template_env::render(
"android_streaming_method.jinja",
minijinja::context! {
method_name => method_name,
params => params.join(", "),
item_type => item_type,
bridge_name => bridge_name,
jni_start => jni_start,
jni_next => jni_next,
jni_free => jni_free,
first_param_name => first_param_name,
},
));
}
}
body.push_str("}\n");
let content = assemble_kt_content(package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{class_name}.kt")),
content,
generated_header: false,
});
}
if visible_functions.is_empty() {
return;
}
let is_dto_named = |ty: &crate::core::ir::TypeRef| -> bool {
match ty {
crate::core::ir::TypeRef::Named(n) => !opaque_type_names.contains(n.as_str()),
_ => false,
}
};
let facade_return_type = |ty: &crate::core::ir::TypeRef| -> String {
if let crate::core::ir::TypeRef::Named(n) = ty {
if opaque_type_names.contains(n.as_str()) {
return n.clone();
}
return n.clone();
}
if matches!(
unwrap_optional(ty),
crate::core::ir::TypeRef::Vec(_) | crate::core::ir::TypeRef::Map(_, _)
) {
return render_kotlin_type(ty, &opaque_type_names);
}
jni_return_type_str(ty).to_string()
};
let facade_param_type = |ty: &crate::core::ir::TypeRef| -> String {
let inner = unwrap_optional(ty);
if let crate::core::ir::TypeRef::Named(n) = inner {
if opaque_type_names.contains(n.as_str()) {
return n.clone();
}
return n.clone();
}
if matches!(
inner,
crate::core::ir::TypeRef::Vec(_) | crate::core::ir::TypeRef::Map(_, _)
) {
return render_kotlin_type(inner, &opaque_type_names);
}
jni_param_type_str(ty).to_string()
};
let is_vec_of_dtos = |ty: &crate::core::ir::TypeRef| -> bool {
if let crate::core::ir::TypeRef::Vec(inner) = ty {
if let crate::core::ir::TypeRef::Named(n) = inner.as_ref() {
return !opaque_type_names.contains(n.as_str());
}
}
false
};
let is_generic_container = |ty: &crate::core::ir::TypeRef| -> bool {
let base = unwrap_optional(ty);
matches!(
base,
crate::core::ir::TypeRef::Vec(_) | crate::core::ir::TypeRef::Map(_, _)
)
};
let needs_jackson = visible_functions.iter().any(|f| {
is_dto_named(&f.return_type)
|| is_generic_container(&f.return_type)
|| f.params
.iter()
.any(|p| is_dto_named(unwrap_optional(&p.ty)) || is_generic_container(unwrap_optional(&p.ty)))
});
let mut imports: BTreeSet<String> = BTreeSet::new();
if needs_jackson {
imports.insert("import com.fasterxml.jackson.annotation.JsonInclude".to_string());
imports.insert("import com.fasterxml.jackson.databind.DeserializationFeature".to_string());
imports.insert("import com.fasterxml.jackson.databind.PropertyNamingStrategies".to_string());
imports.insert("import com.fasterxml.jackson.datatype.jdk8.Jdk8Module".to_string());
imports.insert("import com.fasterxml.jackson.module.kotlin.KotlinFeature".to_string());
imports.insert("import com.fasterxml.jackson.module.kotlin.KotlinModule".to_string());
imports.insert("import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper".to_string());
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
}
let has_generic_container_return = visible_functions.iter().any(|f| is_generic_container(&f.return_type));
if has_generic_container_return {
imports.insert("import com.fasterxml.jackson.core.type.TypeReference".to_string());
}
let mut body = String::new();
body.push_str(&template_env::render(
"module_object_header.jinja",
minijinja::context! {
module_name => module_name,
},
));
if needs_jackson {
body.push_str(" /// Jackson module that marshals ByteArray as a JSON array of unsigned bytes,\n");
body.push_str(" /// matching how Rust serde encodes Vec<u8> on the wire.\n");
body.push_str(" /// Jackson's default writes ByteArray as a Base64 string, which Rust serde rejects\n");
body.push_str(" /// with \"invalid type: string, expected a sequence\".\n");
body.push_str(
" private val byteArrayModule = com.fasterxml.jackson.databind.module.SimpleModule().apply {\n",
);
body.push_str(" addSerializer(\n");
body.push_str(" ByteArray::class.java,\n");
body.push_str(" object : com.fasterxml.jackson.databind.ser.std.StdSerializer<ByteArray>(ByteArray::class.java) {\n");
body.push_str(" override fun serialize(\n");
body.push_str(" value: ByteArray,\n");
body.push_str(" gen: com.fasterxml.jackson.core.JsonGenerator,\n");
body.push_str(" provider: com.fasterxml.jackson.databind.SerializerProvider,\n");
body.push_str(" ) {\n");
body.push_str(" gen.writeStartArray()\n");
body.push_str(" for (b in value) gen.writeNumber(b.toInt() and 0xff)\n");
body.push_str(" gen.writeEndArray()\n");
body.push_str(" }\n");
body.push_str(" },\n");
body.push_str(" )\n");
body.push_str(" addDeserializer(\n");
body.push_str(" ByteArray::class.java,\n");
body.push_str(" object : com.fasterxml.jackson.databind.deser.std.StdDeserializer<ByteArray>(ByteArray::class.java) {\n");
body.push_str(" override fun deserialize(\n");
body.push_str(" parser: com.fasterxml.jackson.core.JsonParser,\n");
body.push_str(" ctx: com.fasterxml.jackson.databind.DeserializationContext,\n");
body.push_str(" ): ByteArray {\n");
body.push_str(
" val node = parser.codec.readTree<com.fasterxml.jackson.databind.JsonNode>(parser)\n",
);
body.push_str(" return when {\n");
body.push_str(
" node.isArray -> ByteArray(node.size()) { i -> node.get(i).asInt().toByte() }\n",
);
body.push_str(
" node.isTextual -> java.util.Base64.getDecoder().decode(node.asText())\n",
);
body.push_str(" else -> ByteArray(0)\n");
body.push_str(" }\n");
body.push_str(" }\n");
body.push_str(" },\n");
body.push_str(" )\n");
body.push_str(" }\n\n");
body.push_str(" private val mapper = jacksonObjectMapper()\n");
body.push_str(" .registerModule(com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
body.push_str(" .registerModule(byteArrayModule)\n");
body.push_str(" .registerModule(\n");
body.push_str(" com.fasterxml.jackson.module.kotlin.KotlinModule.Builder()\n");
body.push_str(
" .configure(com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault, true)\n",
);
body.push_str(" .configure(com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection, true)\n");
body.push_str(
" .configure(com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap, true)\n",
);
body.push_str(" .build(),\n");
body.push_str(" )\n");
body.push_str(" .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)\n");
body.push_str(
" .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY)\n",
);
body.push_str(" .configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n\n");
}
for f in &visible_functions {
emit_kdoc_pub(&mut body, &f.doc, " ");
let method_name = to_lower_camel(&f.name);
let native_name = format!("native{}", to_pascal_case(&f.name));
let return_ty = facade_return_type(&f.return_type);
let returns_dto = is_dto_named(&f.return_type);
let returns_vec_of_dtos = is_vec_of_dtos(&f.return_type);
let returns_generic_container = is_generic_container(&f.return_type);
let params: Vec<String> = f
.params
.iter()
.map(|p| {
let name = to_lower_camel(&p.name);
let inner = unwrap_optional(&p.ty);
let is_dto = is_dto_named(inner);
if p.optional {
if is_dto {
let ty_name = match inner {
crate::core::ir::TypeRef::Named(n) => n.clone(),
_ => unreachable!(),
};
format!("{name}: {ty_name}? = null")
} else if opaque_type_names.contains(match inner {
crate::core::ir::TypeRef::Named(n) => n.as_str(),
_ => "",
}) {
format!("{name}: {} = null", facade_param_type(&p.ty))
} else {
let ty = kotlin_nullable_type_for_optional(&p.ty);
format!("{name}: {ty} = null")
}
} else {
let ty = facade_param_type(&p.ty);
format!("{name}: {ty}")
}
})
.collect();
let bridge_args: Vec<String> = f
.params
.iter()
.map(|p| {
let name = to_lower_camel(&p.name);
let inner = unwrap_optional(&p.ty);
if let crate::core::ir::TypeRef::Named(n) = inner {
if opaque_type_names.contains(n.as_str()) {
return format!("{name}.handle");
}
if p.optional {
return format!("{name}?.let {{ mapper.writeValueAsString(it) }} ?: \"\"");
}
return format!("mapper.writeValueAsString({name})");
}
if matches!(
inner,
crate::core::ir::TypeRef::Vec(_) | crate::core::ir::TypeRef::Map(_, _)
) {
if p.optional {
return format!("{name}?.let {{ mapper.writeValueAsString(it) }} ?: \"\"");
}
return format!("mapper.writeValueAsString({name})");
}
if p.optional {
let zero = jni_zero_literal(inner);
return format!("{name} ?: {zero}");
}
name
})
.collect();
let bridge_call = format!("{bridge_name}.{native_name}({})", bridge_args.join(", "));
let call_args = f
.params
.iter()
.map(|p| to_lower_camel(&p.name))
.collect::<Vec<_>>()
.join(", ");
let params_str = params.join(", ");
let returns_opaque =
matches!(&f.return_type, crate::core::ir::TypeRef::Named(n) if opaque_type_names.contains(n.as_str()));
if returns_dto || returns_generic_container || returns_opaque || needs_jackson {
let _ = returns_vec_of_dtos;
if returns_dto {
let return_class = match &f.return_type {
crate::core::ir::TypeRef::Named(n) => n.clone(),
_ => unreachable!(),
};
body.push_str(&template_env::render(
"android_facade_dto_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
bridge_call => bridge_call,
return_class => return_class,
},
));
emit_kdoc_pub(&mut body, &f.doc, " ");
body.push_str(&template_env::render(
"android_facade_async_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
args => call_args,
},
));
} else if returns_generic_container {
let type_ref_body = render_kotlin_type(&f.return_type, &opaque_type_names);
body.push_str(&template_env::render(
"android_facade_generic_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
bridge_call => bridge_call,
type_ref_body => type_ref_body,
},
));
emit_kdoc_pub(&mut body, &f.doc, " ");
body.push_str(&template_env::render(
"android_facade_async_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
args => call_args,
},
));
} else if returns_opaque {
let opaque_class = match &f.return_type {
crate::core::ir::TypeRef::Named(n) => n.clone(),
_ => unreachable!(),
};
body.push_str(&template_env::render(
"android_facade_expr_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
expression => format!("{opaque_class}({bridge_call})"),
},
));
} else {
body.push_str(&template_env::render(
"android_facade_expr_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
expression => bridge_call,
},
));
}
} else {
body.push_str(&template_env::render(
"android_facade_expr_method.jinja",
minijinja::context! {
method_name => method_name,
params => params_str,
return_type => return_ty,
expression => bridge_call,
},
));
}
}
body.push_str("}\n");
let content = assemble_kt_content(package, &imports, &body);
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{module_name}.kt")),
content,
generated_header: false,
});
}
fn unwrap_optional(ty: &crate::core::ir::TypeRef) -> &crate::core::ir::TypeRef {
match ty {
crate::core::ir::TypeRef::Optional(inner) => inner.as_ref(),
other => other,
}
}
fn assemble_kt_content(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
let suppressions = vec![
"ktlint:standard:trailing-comma-on-call-site",
"ktlint:standard:trailing-comma-on-declaration-site",
"ktlint:standard:spacing-between-declarations-with-comments",
"ktlint:standard:spacing-between-declarations-with-annotations",
"ktlint:standard:when-entry-bracing",
"ktlint:standard:blank-line-between-when-conditions",
"ktlint:standard:blank-line-before-declaration",
"ktlint:standard:chain-method-continuation",
"ktlint:standard:annotation",
"ktlint:standard:max-line-length",
"ktlint:standard:no-semi",
"ktlint:standard:statement-wrapping",
"MaxLineLength",
"TooManyFunctions",
"FunctionParameterNaming",
"LongParameterList",
"CyclomaticComplexMethod",
"LongMethod",
"MagicNumber",
];
let imports = imports.iter().cloned().collect::<Vec<_>>();
template_env::render(
"kt_file.jinja",
minijinja::context! {
package => package,
imports => imports,
suppressions => suppressions,
body => body,
},
)
}
fn kotlin_nullable_type_for_optional(ty: &crate::core::ir::TypeRef) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
let base = match ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
let non_null = match base {
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "Boolean",
PrimitiveType::I8 | PrimitiveType::U8 => "Byte",
PrimitiveType::I16 | PrimitiveType::U16 => "Short",
PrimitiveType::I32 | PrimitiveType::U32 => "Int",
PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "Long",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
},
TypeRef::String => "String",
TypeRef::Named(n) => return format!("{n}?"),
_ => "String",
};
format!("{non_null}?")
}
fn jni_zero_literal(ty: &crate::core::ir::TypeRef) -> &'static str {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::String => "\"\"",
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "false",
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "0L",
_ => "0",
},
_ => "\"\"",
}
}
fn jni_return_type_str(ty: &crate::core::ir::TypeRef) -> &'static str {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Unit => "Unit",
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "Boolean",
PrimitiveType::I8 => "Byte",
PrimitiveType::I16 => "Short",
PrimitiveType::I32 => "Int",
PrimitiveType::I64 => "Long",
PrimitiveType::U8 => "Byte",
PrimitiveType::U16 => "Short",
PrimitiveType::U32 => "Int",
PrimitiveType::U64 => "Long",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
PrimitiveType::Usize | PrimitiveType::Isize => "Long",
},
TypeRef::String => "String",
TypeRef::Optional(_) => "String?",
TypeRef::Bytes => "ByteArray",
TypeRef::Vec(inner) => {
if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) {
"ByteArray"
} else {
"String"
}
}
TypeRef::Named(_) | TypeRef::Map(_, _) => "String",
_ => "Long",
}
}
fn render_kotlin_type(ty: &crate::core::ir::TypeRef, opaque_type_names: &std::collections::HashSet<&str>) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Unit => "Unit".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "Boolean".to_string(),
PrimitiveType::I8 | PrimitiveType::U8 => "Byte".to_string(),
PrimitiveType::I16 | PrimitiveType::U16 => "Short".to_string(),
PrimitiveType::I32 | PrimitiveType::U32 => "Int".to_string(),
PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "Long".to_string(),
PrimitiveType::F32 => "Float".to_string(),
PrimitiveType::F64 => "Double".to_string(),
},
TypeRef::String | TypeRef::Char => "String".to_string(),
TypeRef::Bytes => "ByteArray".to_string(),
TypeRef::Path => "String".to_string(),
TypeRef::Json => "Any".to_string(),
TypeRef::Duration => "Long".to_string(),
TypeRef::Named(n) => {
let _ = opaque_type_names;
n.clone()
}
TypeRef::Vec(inner) => format!("List<{}>", render_kotlin_type(inner, opaque_type_names)),
TypeRef::Map(k, v) => format!(
"Map<{}, {}>",
render_kotlin_type(k, opaque_type_names),
render_kotlin_type(v, opaque_type_names)
),
TypeRef::Optional(inner) => {
format!("{}?", render_kotlin_type(inner, opaque_type_names))
}
}
}
fn jni_param_type_str(ty: &crate::core::ir::TypeRef) -> &'static str {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "Boolean",
PrimitiveType::I8 => "Byte",
PrimitiveType::I16 => "Short",
PrimitiveType::I32 => "Int",
PrimitiveType::I64 => "Long",
PrimitiveType::U8 => "Byte",
PrimitiveType::U16 => "Short",
PrimitiveType::U32 => "Int",
PrimitiveType::U64 => "Long",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
PrimitiveType::Usize | PrimitiveType::Isize => "Long",
},
TypeRef::String => "String",
TypeRef::Bytes => "ByteArray",
TypeRef::Vec(inner) => {
if matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8)) {
"ByteArray"
} else {
"String"
}
}
_ => "String",
}
}