pub fn emit_jni_client_class(
api: &ApiSurface,
config: &ResolvedCrateConfig,
package: Option<&str>,
) -> Option<GeneratedFile> {
let is_client_type = |t: &&crate::core::ir::TypeDef| {
t.is_opaque && !t.is_trait && t.methods.iter().any(|m| !m.sanitized && !m.is_static)
};
let client_types: Vec<_> = api.types.iter().filter(is_client_type).collect();
if client_types.is_empty() {
return None;
}
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.or_else(|| {
config
.kotlin
.as_ref()
.map(|k| k.exclude_functions.iter().map(String::as_str).collect())
})
.unwrap_or_default();
let module_name = to_pascal_case(&config.name);
let bridge_name = format!("{module_name}Bridge");
let pkg = package
.map(str::to_string)
.unwrap_or_else(|| jni_kotlin_package(config));
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
let has_async = client_types
.iter()
.any(|t| t.methods.iter().any(|m| !m.sanitized && m.is_async));
if has_async {
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
}
let streaming_adapters: Vec<_> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "kotlin"))
.filter(|a| {
a.owner_type
.as_deref()
.map(|owner| client_types.iter().any(|t| t.name == owner))
.unwrap_or(false)
})
.collect();
if !streaming_adapters.is_empty() {
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.withContext".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.channels.awaitClose".to_string());
}
for ty in &client_types {
let class_name = &ty.name;
for m in ty.methods.iter().filter(|m| !m.sanitized && !m.is_static) {
kotlin_type_with_string_imports(&m.return_type, false, &mut imports);
for p in &m.params {
format_param_with_imports(p, &mut imports);
}
}
for adapter in streaming_adapters
.iter()
.filter(|a| a.owner_type.as_deref() == Some(class_name.as_str()))
{
if let Some(item) = adapter.item_type.as_deref() {
let _ = item;
}
}
body.push_str(&template_env::render(
"jni_client_class_header.jinja",
minijinja::context! {
class_name => class_name,
},
));
let has_json_methods = ty
.methods
.iter()
.filter(|m| !m.sanitized && !m.is_static)
.any(|m| !m.params.is_empty() || needs_json_deserialize(&m.return_type));
let ctor_config = config.client_constructors.get(class_name.as_str());
let needs_companion = has_json_methods || ctor_config.is_some();
if needs_companion {
body.push_str(" companion object {\n");
if has_json_methods {
body.push_str(" private val MAPPER = com.fasterxml.jackson.databind.ObjectMapper()\n");
body.push_str(" .registerModule(com.fasterxml.jackson.datatype.jdk8.Jdk8Module())\n");
body.push_str(" .findAndRegisterModules()\n");
body.push_str(
" .setPropertyNamingStrategy(com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE)\n",
);
}
if let Some(ctor) = ctor_config {
emit_jni_client_factory(class_name, &bridge_name, ctor, api, &mut body);
}
body.push_str(" }\n\n");
}
for method in ty
.methods
.iter()
.filter(|m| !m.sanitized && !m.is_static && !exclude_functions.contains(m.name.as_str()))
{
emit_jni_client_method(method, class_name, &bridge_name, &mut body, &mut imports);
}
for adapter in streaming_adapters
.iter()
.filter(|a| a.owner_type.as_deref() == Some(class_name.as_str()))
{
emit_jni_streaming_client_method(adapter, class_name, &bridge_name, &mut body);
}
let free_name = format!("nativeFree{class_name}");
body.push_str(&template_env::render(
"jni_client_close_method.jinja",
minijinja::context! {
bridge_name => bridge_name,
free_name => free_name,
},
));
body.push_str("}\n");
}
let imports = imports.iter().cloned().collect::<Vec<_>>();
let content = template_env::render(
"jni_client_file.jinja",
minijinja::context! {
package => pkg,
imports => imports,
body => body,
},
);
let path = jni_output_path(config, "DefaultClient.kt");
Some(GeneratedFile {
path,
content,
generated_header: false,
})
}