use crate::backends::kotlin_android::naming::bridge_object_name;
use crate::backends::kotlin_android::template_env;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeDef;
use heck::ToUpperCamelCase;
use std::collections::BTreeSet;
pub fn gen_trait_bridge_files(
package: &str,
trait_name: &str,
bridge_cfg: &TraitBridgeConfig,
trait_def: &TypeDef,
bridge_class_name: &str,
api: &crate::core::ir::ApiSurface,
effective_excluded_types: &std::collections::HashSet<String>,
) -> Vec<(String, String)> {
let interface_name = format!("I{trait_name}");
let bridge_obj = bridge_object_name(trait_name);
let mut files = Vec::new();
{
let has_super_trait = bridge_cfg.super_trait.is_some();
let register_native_fn = format!("nativeRegister{}", trait_name.to_upper_camel_case());
let unregister_native_fn = bridge_cfg
.unregister_fn
.as_ref()
.map(|_| format!("nativeUnregister{}", trait_name.to_upper_camel_case()));
let clear_native_fn = bridge_cfg
.clear_fn
.as_ref()
.map(|_| format!("nativeClear{}s", trait_name.to_upper_camel_case()));
let body = template_env::render(
"trait_bridge_object.jinja",
minijinja::context! {
bridge_obj => bridge_obj,
interface_name => interface_name,
bridge_class_name => bridge_class_name,
has_super_trait => has_super_trait,
register_native_fn => register_native_fn,
unregister_native_fn => unregister_native_fn,
clear_native_fn => clear_native_fn,
dispatcher_class => format!("{trait_name}JniDispatcher"),
},
);
let content = assemble_kt_content(package, &BTreeSet::new(), &body);
files.push((format!("{bridge_obj}.kt"), content));
}
files.push(gen_jni_dispatcher_file(
package,
trait_name,
bridge_cfg,
trait_def,
api,
effective_excluded_types,
));
files
}
pub fn gen_jni_dispatcher_file(
package: &str,
trait_name: &str,
bridge_cfg: &TraitBridgeConfig,
trait_def: &TypeDef,
api: &crate::core::ir::ApiSurface,
effective_excluded_types: &std::collections::HashSet<String>,
) -> (String, String) {
use heck::ToLowerCamelCase;
let visible_type_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| !t.binding_excluded && !effective_excluded_types.contains(&t.name))
.map(|t| t.name.as_str())
.chain(api.enums.iter().map(|e| e.name.as_str()))
.collect();
let mut imports = BTreeSet::new();
imports.insert("com.fasterxml.jackson.module.kotlin.jacksonObjectMapper".to_string());
let mut any_async = false;
let own_methods: Vec<&crate::core::ir::MethodDef> = trait_def
.methods
.iter()
.filter(|m| !m.sanitized && !m.is_static)
.collect();
let methods: Vec<minijinja::Value> = own_methods
.iter()
.map(|method| {
let method_camel = method.name.to_lower_camel_case();
let args = method
.params
.iter()
.map(|p| {
let ty_ref = substitute_trait_carrier_type(api, bridge_cfg, &p.ty);
let ty = kotlin_type_str_visible(&ty_ref, p.optional, &visible_type_names, &mut imports);
format!(
"mapper.convertValue(args.get(\"{}\"), object : com.fasterxml.jackson.core.type.TypeReference<{}>() {{}})",
p.name, ty
)
})
.collect::<Vec<_>>()
.join(", ");
let call = format!("impl.{method_camel}({args})");
let call = if method.is_async {
any_async = true;
format!("runBlocking {{ {call} }}")
} else {
call
};
let call_expr = if matches!(method.return_type, crate::core::ir::TypeRef::Unit) {
format!("{{\n {call}\n null\n }}")
} else {
call
};
minijinja::context! {
rust_name => &method.name,
call_expr => call_expr,
}
})
.collect();
if any_async {
imports.insert("kotlinx.coroutines.runBlocking".to_string());
}
let mut implemented: Vec<&str> = own_methods.iter().map(|m| m.name.as_str()).collect();
if bridge_cfg.super_trait.is_some() {
implemented.extend(["name", "version", "initialize", "shutdown"]);
}
let body = template_env::render(
"trait_bridge_dispatcher.jinja",
minijinja::context! {
trait_name => trait_name,
has_super_trait => bridge_cfg.super_trait.is_some(),
methods => methods,
implemented => implemented,
},
);
let mut final_imports = BTreeSet::new();
for import_line in &imports {
if let Some(stripped) = import_line.strip_prefix("import ") {
final_imports.insert(stripped.to_string());
} else {
final_imports.insert(import_line.clone());
}
}
let content = assemble_kt_content(package, &final_imports, &body);
(format!("{trait_name}JniDispatcher.kt"), content)
}
pub fn gen_trait_bridge_object(
package: &str,
trait_name: &str,
bridge_cfg: &TraitBridgeConfig,
trait_def: &TypeDef,
bridge_class_name: &str,
) -> Option<(String, String)> {
use crate::core::ir::ApiSurface;
let api = ApiSurface {
crate_name: "test_crate".to_string(),
version: "0.0.0".to_string(),
types: vec![],
enums: vec![],
functions: vec![],
errors: vec![],
excluded_type_paths: std::collections::HashMap::new(),
excluded_trait_names: std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: vec![],
};
let excluded_types = std::collections::HashSet::new();
let files = gen_trait_bridge_files(
package,
trait_name,
bridge_cfg,
trait_def,
bridge_class_name,
&api,
&excluded_types,
);
files.first().map(|(name, content)| (name.clone(), content.clone()))
}
fn assemble_kt_content(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
let mut out = String::new();
let imports = imports
.iter()
.map(|import| format!("import {import}"))
.collect::<Vec<_>>();
out.push_str(&template_env::render(
"kt_file.jinja",
minijinja::context! {
package => package,
imports => imports,
suppressions => Vec::<String>::new(),
body => body,
},
));
out
}
fn kotlin_type_str_visible(
ty: &crate::core::ir::TypeRef,
optional: bool,
visible_type_names: &std::collections::HashSet<&str>,
imports: &mut BTreeSet<String>,
) -> String {
use crate::backends::kotlin::kotlin_type_str_pub;
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: &crate::core::ir::ApiSurface,
bridge: &TraitBridgeConfig,
ty: &crate::core::ir::TypeRef,
) -> crate::core::ir::TypeRef {
use crate::core::ir::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: &crate::core::ir::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)))
}
#[cfg(test)]
fn tests_support_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
use heck::ToSnakeCase;
TraitBridgeConfig {
trait_name: trait_name.to_string(),
super_trait: super_trait.map(|s| s.to_string()),
register_fn: Some(format!("register_{}", trait_name.to_snake_case())),
unregister_fn: Some(format!("unregister_{}", trait_name.to_snake_case())),
clear_fn: Some(format!("clear_{}", trait_name.to_snake_case())),
bind_via: crate::core::config::BridgeBinding::FunctionParam,
..TraitBridgeConfig::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use heck::ToSnakeCase;
fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: trait_name.to_string(),
param_name: None,
type_alias: None,
exclude_languages: vec![],
super_trait: super_trait.map(|s| s.to_string()),
registry_getter: None,
register_fn: Some(format!("register_{}", trait_name.to_snake_case())),
unregister_fn: Some(format!("unregister_{}", trait_name.to_snake_case())),
clear_fn: Some(format!("clear_{}", trait_name.to_snake_case())),
register_extra_args: None,
bind_via: crate::core::config::BridgeBinding::FunctionParam,
options_type: None,
options_field: None,
context_type: None,
result_type: None,
ffi_skip_methods: Vec::new(),
}
}
fn make_trait_def(name: &str) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("testcrate::{}", name),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: true,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
has_private_fields: false,
version: Default::default(),
}
}
#[test]
fn test_bridge_object_with_super_trait() {
let trait_def = make_trait_def("OcrBackend");
let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(content.contains("object OcrBackendBridge"));
assert!(content.contains("fun register(impl: IOcrBackend): Unit"));
assert!(content.contains("val name = impl.name()"));
assert!(content.contains("TestBridge.nativeRegisterOcrBackend(OcrBackendJniDispatcher(impl))"));
assert!(content.contains("nativeRegisterOcrBackend"));
}
#[test]
fn test_bridge_object_without_super_trait() {
let trait_def = make_trait_def("OcrBackend");
let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
bridge_cfg.super_trait = None;
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(content.contains("object OcrBackendBridge"));
assert!(content.contains("fun register(impl: IOcrBackend, name: String): Unit"));
assert!(!content.contains("val name = impl.name()"));
assert!(content.contains("TestBridge.nativeRegisterOcrBackend(OcrBackendJniDispatcher(impl), name)"));
}
#[test]
fn test_bridge_object_with_unregister() {
let trait_def = make_trait_def("OcrBackend");
let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(content.contains("fun unregister(name: String): Unit"));
assert!(content.contains("TestBridge.nativeUnregisterOcrBackend(name)"));
assert!(content.contains("nativeUnregisterOcrBackend"));
}
#[test]
fn test_bridge_object_with_clear() {
let trait_def = make_trait_def("OcrBackend");
let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(content.contains("fun clearAll(): Unit"));
assert!(content.contains("TestBridge.nativeClearOcrBackends()"));
assert!(content.contains("nativeClearOcrBackends"));
}
#[test]
fn test_bridge_object_without_unregister() {
let trait_def = make_trait_def("OcrBackend");
let mut bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
bridge_cfg.unregister_fn = None;
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(!content.contains("fun unregister"));
}
#[test]
fn test_bridge_object_without_clear() {
let trait_def = make_trait_def("OcrBackend");
let mut bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
bridge_cfg.clear_fn = None;
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(!content.contains("fun clearAll"));
}
#[test]
fn test_bridge_object_includes_getall() {
let trait_def = make_trait_def("OcrBackend");
let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
let (_filename, content) =
gen_trait_bridge_object("dev.sample_crate", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
.expect("should generate bridge");
assert!(content.contains("fun getAll(): Map<String, IOcrBackend>"));
}
}
#[cfg(test)]
mod dispatcher_tests {
use super::*;
use crate::core::ir::{MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
fn make_api(trait_def: &TypeDef) -> crate::core::ir::ApiSurface {
crate::core::ir::ApiSurface {
crate_name: "testcrate".into(),
version: "0.1.0".into(),
types: vec![trait_def.clone()],
..Default::default()
}
}
fn ocr_like_trait() -> TypeDef {
TypeDef {
name: "OcrBackend".into(),
rust_path: "testcrate::OcrBackend".into(),
is_trait: true,
is_opaque: true,
methods: vec![
MethodDef {
name: "process_image".into(),
params: vec![
ParamDef {
name: "image_bytes".into(),
ty: TypeRef::Bytes,
is_ref: true,
..Default::default()
},
ParamDef {
name: "config".into(),
ty: TypeRef::Named("OcrConfig".into()),
is_ref: true,
..Default::default()
},
],
return_type: TypeRef::Named("ExtractedDocument".into()),
is_async: true,
receiver: Some(ReceiverKind::Ref),
error_type: Some("Error".into()),
..Default::default()
},
MethodDef {
name: "supports_language".into(),
params: vec![ParamDef {
name: "lang".into(),
ty: TypeRef::String,
is_ref: true,
..Default::default()
}],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
receiver: Some(ReceiverKind::Ref),
..Default::default()
},
MethodDef {
name: "supports_tables".into(),
params: vec![],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
receiver: Some(ReceiverKind::Ref),
has_default_impl: true,
..Default::default()
},
],
..Default::default()
}
}
#[test]
fn dispatcher_file_emitted_with_json_dispatch_and_run_blocking() {
let trait_def = ocr_like_trait();
let bridge_cfg =
crate::backends::kotlin_android::trait_bridge::tests_support_bridge_cfg("OcrBackend", Some("Plugin"));
let api = make_api(&trait_def);
let files = gen_trait_bridge_files(
"dev.sample_crate",
"OcrBackend",
&bridge_cfg,
&trait_def,
"DemoBridge",
&api,
&std::collections::HashSet::new(),
);
let (_, dispatcher) = files
.iter()
.find(|(name, _)| name == "OcrBackendJniDispatcher.kt")
.expect("dispatcher file must be emitted");
assert!(
dispatcher.contains("fun dispatch(method: String, argsJson: String): String"),
"dispatcher must expose the JSON dispatch entry point:\n{dispatcher}"
);
assert!(
dispatcher.contains("runBlocking"),
"suspend trait methods must be bridged via runBlocking:\n{dispatcher}"
);
assert!(
dispatcher.contains("\"process_image\" ->") && dispatcher.contains("\"supports_language\" ->"),
"dispatch must switch on the Rust method names:\n{dispatcher}"
);
assert!(
dispatcher.contains("\"initialize\" ->") && dispatcher.contains("\"shutdown\" ->"),
"lifecycle hooks must be dispatchable:\n{dispatcher}"
);
assert!(
dispatcher.contains("fun implementedMethods(): String"),
"dispatcher must report the implemented method names:\n{dispatcher}"
);
}
#[test]
fn bridge_object_registers_dispatcher_wrapper() {
let trait_def = ocr_like_trait();
let bridge_cfg =
crate::backends::kotlin_android::trait_bridge::tests_support_bridge_cfg("OcrBackend", Some("Plugin"));
let api = make_api(&trait_def);
let files = gen_trait_bridge_files(
"dev.sample_crate",
"OcrBackend",
&bridge_cfg,
&trait_def,
"DemoBridge",
&api,
&std::collections::HashSet::new(),
);
let (_, bridge_obj) = files
.iter()
.find(|(name, _)| name.ends_with("Bridge.kt"))
.expect("bridge object file");
assert!(
bridge_obj.contains("nativeRegisterOcrBackend(OcrBackendJniDispatcher(impl))"),
"register must wrap the impl in the JNI dispatcher:\n{bridge_obj}"
);
}
}