mod module_facade;
mod trait_interfaces;
use crate::core::config::Language;
pub use trait_interfaces::format_method_signature;
use std::collections::BTreeSet;
use std::path::Path;
use crate::backends::kotlin::{
ValueMethodBridge, default_constructible_type_names, emit_enum_pub, emit_error_type_pub, emit_jni_bridge_object,
emit_jni_client_class, emit_type_pub_with_defaults_sealed_and_constructible,
};
use crate::backends::kotlin_android::naming;
use crate::backends::kotlin_android::naming::kotlin_package;
use crate::backends::kotlin_android::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use crate::core::jni::bridge_class_name;
pub(super) fn effective_excluded_type_names(config: &ResolvedCrateConfig) -> std::collections::HashSet<String> {
let kotlin_android_excluded_function_names = naming::excluded_function_names(config);
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_for(Language::KotlinAndroid) {
if bridge.exclude_languages.iter().any(|l| l == "kotlin_android")
&& let Some(alias) = &bridge.type_alias
{
effective_excluded_types.insert(alias.clone());
}
if let Some(name) = bridge.param_name.as_deref()
&& kotlin_android_excluded_function_names.contains(name)
&& 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());
}
}
effective_excluded_types
}
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig, kotlin_source_dir: &Path) -> Vec<GeneratedFile> {
let package = kotlin_package(config);
let mut files = Vec::new();
let effective_excluded_types = effective_excluded_type_names(config);
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)
.filter(|en| !crate::backends::kotlin::enum_uses_declarative_polymorphism_pub(en))
.map(|en| en.name.clone())
.collect();
let default_constructible_types = default_constructible_type_names(&api.types, &enum_defaults);
let serde_type_names = crate::core::jni::value_bridge_serde_type_names(api);
let bridge_class = bridge_class_name(&config.name);
let value_method_bridge = ValueMethodBridge {
bridge_class: &bridge_class,
serde_type_names: &serde_type_names,
};
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 exception_class = format!("{bridge_class}Exception");
let marker = crate::core::hash::SELF_MARKING_HEADER_LINE;
let exception_content = format!(
"{marker}\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,
None,
);
} else {
emit_type_pub_with_defaults_sealed_and_constructible(
ty,
&mut body,
&mut imports,
&enum_defaults,
&sealed_class_names,
&default_constructible_types,
Some(value_method_bridge),
);
}
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,
});
}
let text_types = &config.untagged_union_text_types;
for en in &api.enums {
if en.binding_excluded {
continue;
}
let mut body = String::new();
emit_enum_pub(en, &mut body, &package, text_types);
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,
});
}
trait_interfaces::emit_trait_interfaces(api, config, kotlin_source_dir, &package, &mut files);
module_facade::emit_module_kt(api, config, kotlin_source_dir, &package, &mut files);
if !config.components.is_empty() {
files.push(emit_unsupported_components(kotlin_source_dir, &package));
}
files
}
fn emit_unsupported_components(kotlin_source_dir: &Path, package: &str) -> GeneratedFile {
let message = "Downloadable native components are unsupported by this framework on Android because downloaded executable code conflicts with application packaging, store, and platform security policies. Bundle the required feature profile in the AAR instead."
.to_string();
let content = format!(
"// Generated by alef. Do not edit by hand.\n\n\
package {package}\n\n\
/** Explicit unsupported surface for Alef downloadable components on Android. */\n\
object Components {{\n\
private const val UNSUPPORTED_MESSAGE = \"{message}\"\n\n\
fun componentLoad(component: String): Nothing = unsupported(component)\n\n\
fun componentPrefetch(components: List<String>? = null): Nothing = unsupported(components)\n\n\
fun componentStatus(component: String): Nothing = unsupported(component)\n\n\
fun componentCachePath(component: String): Nothing = unsupported(component)\n\n\
private fun unsupported(@Suppress(\"UNUSED_PARAMETER\") value: Any?): Nothing =\n\
throw UnsupportedOperationException(UNSUPPORTED_MESSAGE)\n\
}}\n"
);
GeneratedFile {
path: kotlin_source_dir.join("Components.kt"),
content,
generated_header: false,
}
}
pub(super) 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",
"NestedBlockDepth",
"ReturnCount",
"UnusedParameter",
];
let imports = imports.iter().cloned().collect::<Vec<_>>();
template_env::render(
"kt_file.jinja",
crate::alef_context! {
package => package,
imports => imports,
suppressions => suppressions,
body => body,
},
)
}
#[cfg(test)]
mod component_tests {
use super::*;
#[test]
fn android_component_api_fails_explicitly() {
let generated = emit_unsupported_components(Path::new("packages/android/src"), "com.example.demo");
assert!(generated.path.ends_with("Components.kt"));
for operation in [
"componentLoad",
"componentPrefetch",
"componentStatus",
"componentCachePath",
] {
assert!(generated.content.contains(operation), "missing {operation}");
}
assert!(generated.content.contains("UnsupportedOperationException"));
assert!(generated.content.contains("unsupported by this framework on Android"));
assert!(
generated
.content
.contains("packaging, store, and platform security policies")
);
}
}