use heck::{ToSnakeCase, ToUpperCamelCase};
use minijinja::context;
use crate::backends::napi::template_env::render;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef};
use super::helpers::{find_contract, typeref_to_rust_type};
pub(in crate::backends::napi::gen_bindings) fn gen_service_rs(
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> String {
let core_import = config.core_import_name();
let mut out = String::new();
out.push_str(&render("service_rs_preamble.jinja", context! {}));
let referenced_contracts: Vec<&HandlerContractDef> = {
let mut names: Vec<&str> = api
.services
.iter()
.flat_map(|s| s.registrations.iter())
.map(|r| r.callback_contract.as_str())
.collect();
names.sort_unstable();
names.dedup();
names.iter().filter_map(|n| find_contract(api, n)).collect()
};
for contract in &referenced_contracts {
gen_handler_bridge(&mut out, contract, &core_import);
}
for service in &api.services {
for ep in &service.entrypoints {
gen_run_napi_function(&mut out, service, ep, api, &core_import);
}
}
let prefix = config.node_type_prefix();
for service in &api.services {
let has_variants = service.registrations.iter().any(|r| !r.variants.is_empty());
let has_entrypoints = !service.entrypoints.is_empty();
if !has_variants && !has_entrypoints {
continue;
}
let app_type_name = format!("{prefix}{}", service.name);
let mut impl_methods = String::new();
let mut wrapper_imports: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for reg in &service.registrations {
for p in ®.metadata_params {
let rust_ty = typeref_to_rust_type(&p.ty, &core_import);
let bare_name = rust_ty.rsplit("::").next().unwrap_or(&rust_ty);
wrapper_imports.insert(format!("{prefix}{bare_name}"));
}
}
let wrapper_use_items = wrapper_imports.into_iter().collect::<Vec<_>>().join(", ");
for reg in &service.registrations {
gen_base_registration_napi_method(&mut impl_methods, service, reg, api, &core_import, config);
}
for reg in &service.registrations {
for variant in ®.variants {
gen_variant_napi_method(&mut impl_methods, service, reg, variant, api, &core_import, config);
}
}
if has_entrypoints {
let has_accessor = config
.services
.iter()
.find(|sc| sc.owner_type == service.name)
.and_then(|sc| sc.host_app_inner_accessor.as_deref())
.is_some();
if has_accessor {
for ep in &service.entrypoints {
gen_entrypoint_napi_method(&mut impl_methods, service, ep, api, &core_import, config);
}
}
}
let indented: String = impl_methods
.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
if !indented.is_empty() {
let impl_methods = if indented.ends_with('\n') {
indented
} else {
format!("{indented}\n")
};
out.push_str(&render(
"service_rs_impl_block.jinja",
context! {
app_type_name,
impl_methods,
wrapper_use_items => wrapper_use_items.clone(),
has_wrapper_imports => !wrapper_use_items.is_empty(),
},
));
}
}
out
}
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
let trait_name = &contract.trait_name;
let bridge_name = format!("{}Bridge", trait_name.to_upper_camel_case());
let dispatch_name = &contract.dispatch.name;
let req_type = contract.wire_request_type.as_deref().unwrap_or("serde_json::Value");
let resp_type = contract.wire_response_type.as_deref().unwrap_or("serde_json::Value");
out.push_str(&render(
"service_rs_handler_bridge_header.jinja",
context! {
trait_name,
bridge_name,
},
));
let extra_param: String = contract
.dispatch_extra_params
.iter()
.map(|p| format!(", {p}"))
.collect();
let wire_name = contract.wire_param_name.as_deref().unwrap_or("request");
let req_path = if req_type == "Value" {
"serde_json::Value".to_string()
} else {
format!("{core_import}::{req_type}")
};
let resp_path = if resp_type == "Value" {
"serde_json::Value".to_string()
} else {
format!("{core_import}::{resp_type}")
};
let box_err = "Box<dyn std::error::Error + Send + Sync>";
let wire_output = format!("std::result::Result<{resp_path}, {box_err}>");
let output_type = contract
.dispatch_return_type
.clone()
.unwrap_or_else(|| wire_output.clone());
let tail = match &contract.response_adapter {
Some(adapter) => format!("{adapter}(outcome)"),
None => "outcome".to_string(),
};
out.push_str(&render(
"service_rs_handler_bridge_impl.jinja",
context! {
core_import,
trait_name,
bridge_name,
dispatch_name,
extra_param,
wire_name,
req_path,
output_type,
wire_output,
box_err,
tail,
},
));
}
fn gen_run_napi_function(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
_api: &ApiSurface,
core_import: &str,
) {
let service_snake = service.name.to_snake_case();
let fn_name = format!("{service_snake}_{}", ep.method);
let owner_path = &service.rust_path;
let ep_method = &ep.method;
let mut rust_params = vec![
"registrations: Vec<(String, Vec<serde_json::Value>, ThreadsafeFunction<serde_json::Value, serde_json::Value>)>".to_owned(),
];
for p in &ep.params {
let rust_ty = typeref_to_rust_type(&p.ty, core_import);
rust_params.push(format!("{}: {}", p.name, rust_ty));
}
let param_sig = rust_params.join(", ");
let return_ty = match ep.kind {
EntrypointKind::Run => "()".to_owned(),
EntrypointKind::Finalize => {
"()".to_owned()
}
};
out.push_str(&render(
"service_rs_run_function_header.jinja",
context! {
owner_path,
ep_method,
fn_name,
param_sig,
return_ty,
},
));
let ctor_call = build_ctor_call_napi(service, owner_path);
out.push_str(&render(
"service_rs_owner_ctor.jinja",
context! {
ctor_call,
},
));
let ep_call = build_ep_call_napi(ep, service, core_import);
out.push_str(&ep_call);
out.push_str(" Ok(())\n}\n\n");
}
fn build_ctor_call_napi(service: &ServiceDef, owner_path: &str) -> String {
if service.constructor.params.is_empty() {
format!("{owner_path}::{}()", service.constructor.name)
} else {
format!("{owner_path}::{}()", service.constructor.name)
}
}
fn build_ep_call_napi(ep: &crate::core::ir::EntrypointDef, _service: &ServiceDef, _core_import: &str) -> String {
let ep_method = &ep.method;
let ep_args: Vec<String> = ep.params.iter().map(|p| p.name.clone()).collect();
let args_str = ep_args.join(", ");
let bind = if matches!(ep.return_type, TypeRef::Unit) {
""
} else {
"let _ = "
};
if ep.is_async {
format!(
" {bind}owner.{ep_method}({args_str})\n \
.await\n \
.map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n"
)
} else if ep.error_type.is_some() {
format!(
" {bind}owner.{ep_method}({args_str})\n \
.map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n"
)
} else {
format!(" {bind}owner.{ep_method}({args_str});\n")
}
}
fn gen_base_registration_napi_method(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
core_import: &str,
config: &ResolvedCrateConfig,
) {
let base_method = ®.method;
let contract_name = ®.callback_contract;
let inner_accessor: String = config
.services
.iter()
.find(|sc| sc.owner_type == service.name)
.and_then(|sc| sc.host_app_inner_accessor.as_deref())
.map(|s| s.to_owned())
.unwrap_or_else(|| "self".to_owned());
let prefix = config.node_type_prefix();
let mut rust_params = vec!["&self".to_owned()];
let mut unwrap_lines = String::new();
for p in ®.metadata_params {
let rust_ty = typeref_to_rust_type(&p.ty, core_import);
let bare_name = rust_ty.rsplit("::").next().unwrap_or(&rust_ty);
let wrapper_ty = format!("{prefix}{bare_name}");
rust_params.push(format!("{}: &{wrapper_ty}", p.name));
unwrap_lines.push_str(&format!(" let {0} = (*{0}.inner).clone();\n", p.name));
}
rust_params.push("handler: ThreadsafeFunction<serde_json::Value, serde_json::Value>".to_string());
let param_sig = rust_params.join(", ");
let doc = reg
.doc
.trim()
.lines()
.map(|line| {
if line.is_empty() {
" ///".to_owned()
} else {
format!(" /// {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
out.push_str(&render(
"service_rs_base_registration_method_header.jinja",
context! {
base_method,
doc,
param_sig,
},
));
out.push_str(&unwrap_lines);
if let Some(contract) = find_contract(api, contract_name) {
let bridge_name = format!("{}Bridge", contract.trait_name.to_upper_camel_case());
out.push_str(&render(
"service_rs_handler_arc.jinja",
context! {
bridge_name,
core_import,
contract_name,
},
));
}
let meta_names: Vec<String> = reg.metadata_params.iter().map(|p| p.name.clone()).collect();
let meta_args = meta_names.join(", ");
if inner_accessor == "self" {
if !meta_names.is_empty() {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "self",
base_method,
meta_args,
has_meta => true,
},
));
} else {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "self",
base_method,
meta_args => "",
has_meta => false,
},
));
}
} else {
out.push_str(&render(
"service_rs_inner_accessor.jinja",
context! {
inner_accessor,
},
));
if !meta_names.is_empty() {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "inner",
base_method,
meta_args,
has_meta => true,
},
));
} else {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "inner",
base_method,
meta_args => "",
has_meta => false,
},
));
}
}
if reg.error_type.is_some() {
out.push_str(" .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n");
} else {
out.push_str(" ;\n");
}
out.push_str(&render("service_rs_unit_ok_footer.jinja", context! {}));
}
fn gen_variant_napi_method(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
variant: &crate::core::ir::RegistrationVariant,
api: &ApiSurface,
core_import: &str,
config: &ResolvedCrateConfig,
) {
let variant_name = &variant.name;
let base_method = ®.method;
let contract_name = ®.callback_contract;
let inner_accessor: String = config
.services
.iter()
.find(|sc| sc.owner_type == service.name)
.and_then(|sc| sc.host_app_inner_accessor.as_deref())
.map(|s| s.to_owned())
.unwrap_or_else(|| "self".to_owned());
let mut rust_params = vec!["&self".to_owned()];
for p in &variant.signature_params {
let rust_ty = typeref_to_rust_type(&p.ty, core_import);
rust_params.push(format!("{}: {}", p.name, rust_ty));
}
rust_params.push("handler: ThreadsafeFunction<serde_json::Value, serde_json::Value>".to_string());
let param_sig = rust_params.join(", ");
let doc = variant.doc.as_deref().unwrap_or("").trim();
out.push_str(&render(
"service_rs_variant_method_header.jinja",
context! {
variant_name,
doc,
param_sig,
},
));
if let Some(wrapper_call) = &variant.wrapper_call {
let wrapper_path = &wrapper_call.wrapper_type_path;
let constructor = &wrapper_call.constructor_method;
let mut ctor_args = Vec::new();
for arg in &wrapper_call.args {
match arg {
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} => {
ctor_args.push(value_expr.clone());
}
crate::core::ir::WrapperConstructorArg::Free { param } => {
ctor_args.push(param.name.clone());
}
}
}
let ctor_arg_str = ctor_args.join(", ");
let metadata_param = &wrapper_call.metadata_param;
out.push_str(&render(
"service_rs_wrapper_ctor.jinja",
context! {
metadata_param,
wrapper_path,
constructor,
ctor_arg_str,
},
));
}
let mut metadata_names: Vec<String> = Vec::new();
if let Some(wrapper_call) = &variant.wrapper_call {
metadata_names.push(wrapper_call.metadata_param.clone());
} else {
for p in &variant.signature_params {
metadata_names.push(p.name.clone());
}
}
if let Some(contract) = find_contract(api, contract_name) {
let bridge_name = format!("{}Bridge", contract.trait_name.to_upper_camel_case());
out.push_str(&render(
"service_rs_handler_arc.jinja",
context! {
bridge_name,
core_import,
contract_name,
},
));
}
let meta_args = metadata_names.join(", ");
if inner_accessor == "self" {
if !metadata_names.is_empty() {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "self",
base_method,
meta_args,
has_meta => true,
},
));
} else {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "self",
base_method,
meta_args => "",
has_meta => false,
},
));
}
} else {
out.push_str(&render(
"service_rs_inner_accessor.jinja",
context! {
inner_accessor,
},
));
if !metadata_names.is_empty() {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "inner",
base_method,
meta_args,
has_meta => true,
},
));
} else {
out.push_str(&render(
"service_rs_base_registration_call.jinja",
context! {
receiver => "inner",
base_method,
meta_args => "",
has_meta => false,
},
));
}
}
if reg.error_type.is_some() {
out.push_str(" .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n");
} else {
out.push_str(" ;\n");
}
out.push_str(&render("service_rs_unit_ok_footer.jinja", context! {}));
}
fn gen_entrypoint_napi_method(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
_api: &ApiSurface,
core_import: &str,
config: &ResolvedCrateConfig,
) {
let ep_method = &ep.method;
let js_name = format!("native{}", ep_method.to_upper_camel_case());
let inner_accessor: String = config
.services
.iter()
.find(|sc| sc.owner_type == service.name)
.and_then(|sc| sc.host_app_inner_accessor.as_deref())
.map(|s| s.to_owned())
.unwrap_or_else(|| "self".to_owned());
let mut rust_params = vec!["&self".to_owned()];
for p in &ep.params {
let rust_ty = typeref_to_rust_type(&p.ty, core_import);
rust_params.push(format!("{}: {}", p.name, rust_ty));
}
let param_sig = rust_params.join(", ");
let _ = EntrypointKind::Run; let return_ty = match ep.kind {
EntrypointKind::Run | EntrypointKind::Finalize => "()".to_owned(),
};
let doc = ep
.doc
.trim()
.lines()
.map(|line| {
if line.is_empty() {
"///".to_owned()
} else {
format!("/// {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
let async_kw = if ep.is_async { "async " } else { "" };
out.push_str(&render(
"service_rs_entrypoint_method_header.jinja",
context! {
ep_method,
doc,
js_name,
async_kw,
param_sig,
return_ty,
},
));
let ep_args: Vec<String> = ep.params.iter().map(|p| p.name.clone()).collect();
let args_str = ep_args.join(", ");
let bind = if matches!(ep.return_type, TypeRef::Unit) {
""
} else {
"let _ = "
};
if inner_accessor == "self" {
if ep.is_async {
out.push_str(&render(
"service_rs_entrypoint_call.jinja",
context! {
bind,
receiver => "self",
ep_method,
args_str,
is_async => true,
},
));
} else {
out.push_str(&render(
"service_rs_entrypoint_call.jinja",
context! {
bind,
receiver => "self",
ep_method,
args_str,
is_async => false,
},
));
}
} else {
out.push_str(&render(
"service_rs_take_owner.jinja",
context! {
inner_accessor,
},
));
if ep.is_async {
out.push_str(&render(
"service_rs_entrypoint_call.jinja",
context! {
bind,
receiver => "owner",
ep_method,
args_str,
is_async => true,
},
));
} else {
out.push_str(&render(
"service_rs_entrypoint_call.jinja",
context! {
bind,
receiver => "owner",
ep_method,
args_str,
is_async => false,
},
));
}
}
if ep.error_type.is_some() {
out.push_str(" .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n");
} else {
out.push_str(" ;\n");
}
out.push_str(&render("service_rs_unit_ok_footer.jinja", context! {}));
}