use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, ServiceDef, TypeRef};
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
fn kotlin_type_for_metadata(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char => "String".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "Boolean".to_owned(),
PrimitiveType::U8 | PrimitiveType::I8 => "Byte".to_owned(),
PrimitiveType::U16 | PrimitiveType::I16 => "Short".to_owned(),
PrimitiveType::U32 | PrimitiveType::I32 => "Int".to_owned(),
PrimitiveType::U64 | PrimitiveType::I64 => "Long".to_owned(),
PrimitiveType::F32 => "Float".to_owned(),
PrimitiveType::F64 => "Double".to_owned(),
PrimitiveType::Usize => "Long".to_owned(),
PrimitiveType::Isize => "Long".to_owned(),
}
}
TypeRef::Bytes => "ByteArray".to_owned(),
TypeRef::Unit => "Unit".to_owned(),
_ => "Any".to_owned(),
}
}
fn gen_service_kotlin(_api: &ApiSurface, service: &ServiceDef, package: &str) -> String {
let mut out = String::new();
out.push_str("// Auto-generated by alef — DO NOT EDIT\n");
out.push('\n');
out.push_str(&format!("package {}\n\n", package));
out.push_str("import java.io.Closeable\n\n");
let class_name = service.name.to_upper_camel_case();
out.push_str("/**\n");
out.push_str(&format!(" * Service wrapper for {}.\n", service.name));
out.push_str(" *\n");
out.push_str(" * Wraps an opaque native owner handle and provides type-safe registration\n");
out.push_str(" * and entrypoint methods.\n");
out.push_str(" */\n");
out.push_str(&format!("public class {}(\n", class_name));
out.push_str(" private var handle: Long = 0L,\n");
out.push_str(") : Closeable {\n\n");
out.push_str(" companion object {\n");
out.push_str(" init {\n");
out.push_str(" System.loadLibrary(\"spikard_jni\")\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
let service_snake = service.name.to_snake_case();
out.push_str(" /**\n");
out.push_str(&format!(" * Allocate a new {} instance.\n", service.name));
out.push_str(" */\n");
out.push_str(" init {\n");
out.push_str(&format!(
" handle = nativeConstructor_{service_snake}()\n"
));
out.push_str(" }\n\n");
out.push_str(" /**\n");
out.push_str(" * Free the native owner.\n");
out.push_str(" */\n");
out.push_str(" override fun close() {\n");
out.push_str(" if (handle != 0L) {\n");
out.push_str(&format!(
" nativeFree_{service_snake}(handle)\n"
));
out.push_str(" handle = 0L\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
for reg in &service.registrations {
let reg_method = ®.method;
let service_pascal = service.name.to_upper_camel_case();
let method_pascal = reg_method.to_upper_camel_case();
out.push_str(" /**\n");
out.push_str(&format!(" * Register a handler for {}.\n", reg_method));
out.push_str(" *\n");
out.push_str(" * @param handler A lambda accepting a request and returning a response\n");
for meta_param in ®.metadata_params {
out.push_str(&format!(" * @param {} Metadata: {}\n", meta_param.name, meta_param.name));
}
out.push_str(" * @return 0 on success, non-zero error code on failure\n");
out.push_str(" */\n");
out.push_str(&format!(" fun {}(\n", reg_method));
out.push_str(" handler: (String) -> String");
for meta_param in ®.metadata_params {
let kotlin_ty = kotlin_type_for_metadata(&meta_param.ty);
let param_name = meta_param.name.to_lower_camel_case();
out.push_str(&format!(",\n {}: {}", param_name, kotlin_ty));
}
out.push_str("\n ): Int {\n");
out.push_str(" return nativeRegister");
out.push_str(&service_pascal);
out.push_str(&method_pascal);
out.push_str("(\n");
out.push_str(" handle,\n");
out.push_str(" handler,\n");
let mut first = true;
for meta_param in ®.metadata_params {
if !first {
out.push_str(",\n");
}
let param_name = meta_param.name.to_lower_camel_case();
out.push_str(&format!(" {}", param_name));
first = false;
}
out.push_str("\n )\n");
out.push_str(" }\n\n");
}
for ep in &service.entrypoints {
let ep_method = &ep.method;
let service_pascal = service.name.to_upper_camel_case();
out.push_str(" /**\n");
out.push_str(&format!(" * {}.\n", ep_method));
out.push_str(" *\n");
for param in &ep.params {
out.push_str(&format!(" * @param {} {}\n", param.name, param.name));
}
match ep.kind {
EntrypointKind::Run => {
out.push_str(" */\n");
}
EntrypointKind::Finalize => {
out.push_str(" * @return Result from finalize\n");
out.push_str(" */\n");
}
}
let return_type = match ep.kind {
EntrypointKind::Run => "Unit".to_owned(),
EntrypointKind::Finalize => "Long".to_owned(),
};
let params_str = ep
.params
.iter()
.map(|param| {
format!(
"{}: {}",
param.name.to_lower_camel_case(),
kotlin_type_for_metadata(¶m.ty)
)
})
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(" fun {}({}): {} {{\n", ep_method, params_str, return_type));
match ep.kind {
EntrypointKind::Run => {
out.push_str(&format!(
" nativeRun{}(\n",
service_pascal
));
out.push_str(" handle");
for param in &ep.params {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(",\n {}", param_name));
}
out.push_str("\n )\n");
}
EntrypointKind::Finalize => {
out.push_str(&format!(
" return nativeFinalize{}(\n",
service_pascal
));
out.push_str(" handle");
for param in &ep.params {
let param_name = param.name.to_lower_camel_case();
out.push_str(&format!(",\n {}", param_name));
}
out.push_str("\n )\n");
}
}
out.push_str(" }\n\n");
}
out.push_str(" private companion object {\n");
out.push_str(" /**\n");
out.push_str(" * Allocate a new service instance via JNI.\n");
out.push_str(&format!(
" * Maps to: Java_com_example_constructor_{service_snake}()\n"
));
out.push_str(" */\n");
out.push_str(&format!(
" external fun nativeConstructor_{service_snake}(): Long\n"
));
out.push_str(" /**\n");
out.push_str(" * Free the service instance via JNI.\n");
out.push_str(&format!(
" * Maps to: Java_com_example_free_{service_snake}(env, class, handle)\n"
));
out.push_str(" */\n");
out.push_str(&format!(
" external fun nativeFree_{service_snake}(handle: Long)\n"
));
for reg in &service.registrations {
let service_pascal = service.name.to_upper_camel_case();
let method_pascal = reg.method.to_upper_camel_case();
out.push_str(" /**\n");
out.push_str(&format!(" * Register a handler for {} via JNI.\n", reg.method));
out.push_str(&format!(
" * Maps to: Java_com_example_register{}{}\n",
service_pascal, method_pascal
));
out.push_str(" */\n");
out.push_str(&format!(
" external fun nativeRegister{}{}(\n",
service_pascal, method_pascal
));
out.push_str(" handle: Long,\n");
out.push_str(" handler: (String) -> String");
for meta_param in ®.metadata_params {
let kotlin_ty = kotlin_type_for_metadata(&meta_param.ty);
out.push_str(&format!(",\n {}: {}", meta_param.name, kotlin_ty));
}
out.push_str("\n ): Int\n");
}
for ep in &service.entrypoints {
let service_pascal = service.name.to_upper_camel_case();
let return_type = match ep.kind {
EntrypointKind::Run => "Unit",
EntrypointKind::Finalize => "Long",
};
out.push_str(" /**\n");
out.push_str(&format!(" * {} the service via JNI.\n", ep.method));
match ep.kind {
EntrypointKind::Run => {
out.push_str(&format!(
" * Maps to: Java_com_example_run{}\n",
service_pascal
));
out.push_str(" */\n");
out.push_str(&format!(
" external fun nativeRun{}(\n",
service_pascal
));
}
EntrypointKind::Finalize => {
out.push_str(&format!(
" * Maps to: Java_com_example_finalize{}\n",
service_pascal
));
out.push_str(" */\n");
out.push_str(&format!(
" external fun nativeFinalize{}(\n",
service_pascal
));
}
}
out.push_str(" handle: Long");
for param in &ep.params {
let kotlin_ty = kotlin_type_for_metadata(¶m.ty);
out.push_str(&format!(",\n {}: {}", param.name, kotlin_ty));
}
out.push_str(&format!("\n ): {}\n", return_type));
}
out.push_str(" }\n");
out.push_str("}\n");
out
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let package = config.kotlin_package();
let output_dir = config
.output_paths
.get("kotlin")
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| "packages/kotlin/".to_owned());
let base_path = PathBuf::from(&output_dir).join(package.replace('.', "/"));
let mut files = Vec::new();
for service in &api.services {
let kotlin = gen_service_kotlin(api, service, &package);
let class_name = service.name.to_upper_camel_case();
files.push(GeneratedFile {
path: base_path.join(format!("{}.kt", class_name)),
content: kotlin,
generated_header: false,
});
}
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
};
fn make_fixture_surface() -> ApiSurface {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: None,
doc: "Register a handler.".to_owned(),
};
let run_ep = EntrypointDef {
method: "run".to_owned(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![ParamDef {
name: "addr".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Unit,
error_type: Some("ServiceError".to_owned()),
doc: "Run the service.".to_owned(),
};
let service = ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![run_ep],
doc: "A test service.".to_owned(),
cfg: None,
};
let dispatch_method = MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "request".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("ResponseData".to_owned()),
is_async: true,
is_static: false,
error_type: Some("HandlerError".to_owned()),
doc: "Dispatch a request.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: dispatch_method,
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("ResponseData".to_owned()),
doc: "Handler contract.".to_owned(),
};
ApiSurface {
crate_name: "my_crate".to_owned(),
version: "0.1.0".to_owned(),
services: vec![service],
handler_contracts: vec![contract],
..ApiSurface::default()
}
}
#[test]
fn gen_service_kotlin_contains_class() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("package com.example"));
assert!(kotlin.contains("class TestService("));
assert!(kotlin.contains("private var handle: Long"));
}
#[test]
fn gen_service_kotlin_declares_external_constructor() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("external fun nativeConstructor_test_service()"));
assert!(kotlin.contains(": Long"));
}
#[test]
fn gen_service_kotlin_declares_external_destructor() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("external fun nativeFree_test_service(handle: Long)"));
}
#[test]
fn gen_service_kotlin_implements_closeable() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains(": Closeable"));
assert!(kotlin.contains("override fun close()"));
assert!(kotlin.contains("nativeFree_test_service(handle)"));
}
#[test]
fn gen_service_kotlin_registration_method_accepts_lambda() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("fun add_handler("));
assert!(kotlin.contains("handler: (String) -> String"));
assert!(kotlin.contains("path: String"));
}
#[test]
fn gen_service_kotlin_registration_calls_native() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("nativeRegisterTestServiceAddHandler("));
assert!(kotlin.contains("handle,"));
assert!(kotlin.contains("handler,"));
assert!(kotlin.contains("path"));
}
#[test]
fn gen_service_kotlin_declares_external_register() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("external fun nativeRegisterTestServiceAddHandler("));
assert!(kotlin.contains("handle: Long"));
assert!(kotlin.contains("handler: (String) -> String"));
assert!(kotlin.contains("path: String"));
assert!(kotlin.contains("): Int"));
}
#[test]
fn gen_service_kotlin_entrypoint_method() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("fun run("));
assert!(kotlin.contains("addr: String"));
assert!(kotlin.contains("): Unit"));
}
#[test]
fn gen_service_kotlin_entrypoint_calls_native() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("nativeRunTestService("));
assert!(kotlin.contains("handle,"));
assert!(kotlin.contains("addr"));
}
#[test]
fn gen_service_kotlin_declares_external_run() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("external fun nativeRunTestService("));
assert!(kotlin.contains("handle: Long"));
assert!(kotlin.contains("addr: String"));
assert!(kotlin.contains("): Unit"));
}
#[test]
fn gen_service_kotlin_loads_native_library() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("System.loadLibrary"));
assert!(kotlin.contains("spikard_jni"));
}
#[test]
fn gen_service_kotlin_has_no_stubs() {
let api = make_fixture_surface();
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(!kotlin.contains("TODO"));
assert!(!kotlin.contains("stub"));
assert!(!kotlin.contains("placeholder"));
}
#[test]
fn generate_returns_files() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert!(!files.is_empty(), "expected at least one file");
let has_service_file = files.iter().any(|f| f.path.to_string_lossy().contains("TestService.kt"));
assert!(has_service_file, "expected TestService.kt in output");
}
#[test]
fn generate_returns_empty_for_no_services() {
let api = ApiSurface::default();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
#[test]
fn gen_service_kotlin_finalize_entrypoint() {
let mut api = make_fixture_surface();
let finalize_ep = EntrypointDef {
method: "shutdown".to_owned(),
kind: EntrypointKind::Finalize,
is_async: true,
params: vec![],
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::I32),
error_type: None,
doc: "Shutdown the service.".to_owned(),
};
api.services[0].entrypoints.push(finalize_ep);
let service = &api.services[0];
let kotlin = gen_service_kotlin(&api, service, "com.example");
assert!(kotlin.contains("fun shutdown()"));
assert!(kotlin.contains("): Long"));
assert!(kotlin.contains("nativeFinalizeTestService("));
assert!(kotlin.contains("external fun nativeFinalizeTestService("));
assert!(kotlin.contains("): Long"));
}
}