use std::collections::HashSet;
use crate::codegen::naming::to_class_name;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, MethodDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef};
pub fn jni_package(config: &ResolvedCrateConfig) -> String {
config
.kotlin_android
.as_ref()
.and_then(|a| a.package.clone())
.or_else(|| config.kotlin.as_ref().and_then(|k| k.package.clone()))
.or_else(|| config.try_kotlin_package().ok())
.unwrap_or_else(|| {
let clean = config.name.replace(['-', '_'], "").to_lowercase();
format!("com.example.{clean}")
})
}
pub fn bridge_class_name(crate_name: &str) -> String {
format!("{}Bridge", to_class_name(crate_name))
}
pub fn service_bridge_class_name(service_name: &str) -> String {
format!("{}ServiceBridge", to_class_name(service_name))
}
pub fn bridge_method_name(owner: &str, method: &str) -> String {
let owner_pascal = to_class_name(owner);
let method_pascal = to_class_name(method);
if owner_pascal.is_empty() {
format!("native{method_pascal}")
} else {
format!("native{owner_pascal}{method_pascal}")
}
}
pub fn streaming_method_names(owner: &str, method: &str) -> (String, String, String) {
let owner_pascal = to_class_name(owner);
let method_pascal = to_class_name(method);
(
format!("native{owner_pascal}{method_pascal}Start"),
format!("native{owner_pascal}{method_pascal}Next"),
format!("native{owner_pascal}{method_pascal}Free"),
)
}
pub fn destructor_method_name(owner: &str) -> String {
let owner_pascal = to_class_name(owner);
format!("nativeFree{owner_pascal}")
}
pub fn jni_symbol(package: &str, class: &str, method: &str) -> String {
let encode = |s: &str| s.replace('_', "_1").replace('.', "_");
let pkg_encoded = encode(package);
let class_encoded = encode(class);
if method.is_empty() {
format!("Java_{pkg_encoded}_{class_encoded}")
} else {
let method_encoded = encode(method);
format!("Java_{pkg_encoded}_{class_encoded}_{method_encoded}")
}
}
pub fn value_bridge_serde_type_names(api: &ApiSurface) -> HashSet<&str> {
let mut names: HashSet<&str> = api
.types
.iter()
.filter(|t| !t.is_opaque && !t.is_trait && !t.binding_excluded && t.has_serde)
.map(|t| t.name.as_str())
.collect();
names.extend(
api.enums
.iter()
.filter(|e| !e.binding_excluded && e.has_serde)
.map(|e| e.name.as_str()),
);
names
}
pub fn is_functional_ref_mut_value_method(method: &MethodDef) -> bool {
matches!(method.receiver, Some(ReceiverKind::RefMut))
&& method.trait_source.is_none()
&& matches!(method.return_type, TypeRef::Unit)
}
pub fn value_method_return_type(owner_type_name: &str, method: &MethodDef) -> TypeRef {
if is_functional_ref_mut_value_method(method) {
TypeRef::Named(owner_type_name.to_string())
} else {
method.return_type.clone()
}
}
fn is_json_bridgeable(ty: &TypeRef, serde_type_names: &HashSet<&str>) -> bool {
match ty {
TypeRef::String | TypeRef::Primitive(_) | TypeRef::Path => true,
TypeRef::Named(name) => serde_type_names.contains(name.as_str()),
TypeRef::Vec(inner) => {
let is_bytes = matches!(inner.as_ref(), TypeRef::Primitive(PrimitiveType::U8));
!is_bytes && is_json_bridgeable(inner, serde_type_names)
}
TypeRef::Map(key, value) => {
is_json_bridgeable(key, serde_type_names) && is_json_bridgeable(value, serde_type_names)
}
_ => false,
}
}
pub fn value_method_is_bridgeable(owner: &TypeDef, method: &MethodDef, serde_type_names: &HashSet<&str>) -> bool {
if !serde_type_names.contains(owner.name.as_str()) {
return false;
}
if method.sanitized || method.binding_excluded || method.is_static || method.is_async {
return false;
}
if method.receiver.is_none() || method.trait_source.is_some() {
return false;
}
if !method
.params
.iter()
.all(|param| !param.sanitized && !param.optional && is_json_bridgeable(¶m.ty, serde_type_names))
{
return false;
}
if is_functional_ref_mut_value_method(method) && method.error_type.is_some() {
return false;
}
let return_type = value_method_return_type(&owner.name, method);
matches!(return_type, TypeRef::Unit)
|| (!matches!(return_type, TypeRef::Path) && is_json_bridgeable(&return_type, serde_type_names))
}
pub fn bridgeable_value_methods<'a>(owner: &'a TypeDef, serde_type_names: &HashSet<&str>) -> Vec<&'a MethodDef> {
owner
.methods
.iter()
.filter(|method| value_method_is_bridgeable(owner, method, serde_type_names))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bridge_class_name_basic() {
assert_eq!(bridge_class_name("demo"), "DemoBridge");
assert_eq!(bridge_class_name("my-lib"), "MyLibBridge");
assert_eq!(bridge_class_name("my_lib"), "MyLibBridge");
}
#[test]
fn bridge_method_name_with_owner() {
assert_eq!(bridge_method_name("DemoClient", "foo"), "nativeDemoClientFoo");
assert_eq!(bridge_method_name("demo_client", "bar_baz"), "nativeDemoClientBarBaz");
}
#[test]
fn bridge_method_name_no_owner() {
assert_eq!(bridge_method_name("", "createClient"), "nativeCreateClient");
assert_eq!(bridge_method_name("", "create_client"), "nativeCreateClient");
}
#[test]
fn streaming_method_names_basic() {
let (s, n, f) = streaming_method_names("DemoClient", "streamData");
assert_eq!(s, "nativeDemoClientStreamDataStart");
assert_eq!(n, "nativeDemoClientStreamDataNext");
assert_eq!(f, "nativeDemoClientStreamDataFree");
}
#[test]
fn destructor_method_name_basic() {
assert_eq!(destructor_method_name("DemoClient"), "nativeFreeDemoClient");
assert_eq!(destructor_method_name("demo_client"), "nativeFreeDemoClient");
}
#[test]
fn jni_symbol_basic() {
let sym = jni_symbol("dev.sample_crate.demo", "DemoBridge", "nativeFoo");
assert_eq!(sym, "Java_dev_sample_1crate_demo_DemoBridge_nativeFoo");
}
#[test]
fn jni_symbol_underscore_in_class_encoded() {
let sym = jni_symbol("dev.demo", "Demo_Bridge", "nativeBar");
assert_eq!(sym, "Java_dev_demo_Demo_1Bridge_nativeBar");
}
#[test]
fn jni_symbol_empty_method_gives_prefix() {
let prefix = jni_symbol("dev.sample_crate.demo", "DemoBridge", "");
assert_eq!(prefix, "Java_dev_sample_1crate_demo_DemoBridge");
}
#[test]
fn jni_package_prefers_kotlin_android() {
let config = ResolvedCrateConfig {
name: "test-lib".to_owned(),
..ResolvedCrateConfig::default()
};
assert_eq!(jni_package(&config), "com.example.testlib");
}
}