use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
fn typescript_type_annotation(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::F32 | PrimitiveType::F64 => "number".to_owned(),
_ => "number".to_owned(),
}
}
TypeRef::Bytes => "Buffer".to_owned(),
TypeRef::Optional(inner) => format!("{} | undefined", typescript_type_annotation(inner)),
TypeRef::Vec(inner) => format!("{}[]", typescript_type_annotation(inner)),
TypeRef::Map(k, v) => format!(
"Record<{}, {}>",
typescript_type_annotation(k),
typescript_type_annotation(v)
),
TypeRef::Unit => "void".to_owned(),
TypeRef::Named(n) => n.clone(),
TypeRef::Json => "any".to_owned(),
TypeRef::Path => "string".to_owned(),
TypeRef::Duration => "number".to_owned(),
}
}
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}
pub(super) fn gen_service_ts(api: &ApiSurface, native_module: &str) -> String {
let mut out = String::new();
out.push_str("// Auto-generated service API class\n\n");
out.push_str("import type { ");
let mut imports = vec!["JsObject".to_owned()];
for contract in &api.handler_contracts {
if let Some(req_ty) = &contract.wire_request_type {
imports.push(req_ty.clone());
}
if let Some(resp_ty) = &contract.wire_response_type {
imports.push(resp_ty.clone());
}
}
if let Some(service) = api.services.first() {
for param in &service.constructor.params {
if let TypeRef::Named(name) = ¶m.ty {
imports.push(name.clone());
}
}
for method in &service.configurators {
for param in &method.params {
if let TypeRef::Named(name) = ¶m.ty {
imports.push(name.clone());
}
}
}
for reg in &service.registrations {
for variant in ®.variants {
if let Some(wrapper_call) = &variant.wrapper_call {
imports.push(wrapper_call.wrapper_type_name.clone());
for arg in &wrapper_call.args {
if let crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} = arg
{
let parts: Vec<&str> = value_expr.split("::").collect();
if parts.len() >= 2 {
imports.push(parts[parts.len() - 2].to_string());
}
}
}
}
for param in &variant.signature_params {
if let TypeRef::Named(name) = ¶m.ty {
imports.push(name.clone());
}
}
}
}
}
imports.sort();
imports.dedup();
out.push_str(&imports.join(", "));
out.push_str(" } from \"../index\";\n");
out.push_str(&format!(
"import {{ {}_run }} from \"../index\";\n\n",
api.services[0].name.to_snake_case()
));
for service in &api.services {
gen_service_class_ts(&mut out, service, api, native_module);
}
out
}
fn gen_service_class_ts(out: &mut String, service: &ServiceDef, api: &ApiSurface, _native_module: &str) {
let class_name = &service.name;
if !service.doc.is_empty() {
out.push_str(&format!("/**\n * {}\n */\n", service.doc.trim().replace('\n', "\n * ")));
}
out.push_str(&format!("export class {class_name} {{\n"));
out.push_str(" private _registrations: Array<[string, any[], (...args: any[]) => any]> = [];\n\n");
{
let ctor = &service.constructor;
let mut params = Vec::new();
for p in &ctor.params {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
params.push(format!("{}: {} = undefined", p.name, ty));
} else {
params.push(format!("{}: {}", p.name, ty));
}
}
let param_sig = params.join(", ");
out.push_str(" /**\n");
out.push_str(&format!(" * Create a new {class_name} instance.\n"));
out.push_str(" */\n");
if param_sig.is_empty() {
out.push_str(&format!(" static new(): {class_name} {{\n"));
out.push_str(&format!(" return new {class_name}();\n"));
} else {
out.push_str(&format!(" static new({param_sig}): {class_name} {{\n"));
let params_for_ctor: Vec<&str> = ctor.params.iter().map(|p| p.name.as_str()).collect();
out.push_str(&format!(
" return new {class_name}({});\n",
params_for_ctor.join(", ")
));
}
out.push_str(" }\n\n");
}
{
let ctor = &service.constructor;
let mut params = Vec::new();
for p in &ctor.params {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
params.push(format!("{}: {} = undefined", p.name, ty));
} else {
params.push(format!("{}: {}", p.name, ty));
}
}
out.push_str(" /**\n");
if !ctor.doc.is_empty() {
out.push_str(&format!(" * {}\n", ctor.doc.trim().replace('\n', "\n * ")));
}
out.push_str(" */\n");
let param_sig = params.join(", ");
out.push_str(&format!(" constructor({param_sig}) {{\n"));
out.push_str(" // Constructor initialization (parameters stored for future use)\n");
out.push_str(" }\n\n");
}
for method in &service.configurators {
let mut params = Vec::new();
for p in &method.params {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
params.push(format!("{}: {} = undefined", p.name, ty));
} else {
params.push(format!("{}: {}", p.name, ty));
}
}
out.push_str(" /**\n");
if !method.doc.is_empty() {
out.push_str(&format!(" * {}\n", method.doc.trim().replace('\n', "\n * ")));
}
out.push_str(" */\n");
let param_sig = params.join(", ");
let method_name = &method.name;
out.push_str(&format!(" {method_name}({param_sig}): this {{\n"));
out.push_str(" return this;\n");
out.push_str(" }\n\n");
}
for reg in &service.registrations {
gen_registration_method_ts(out, reg, service, api);
}
for ep in &service.entrypoints {
let mut params = Vec::new();
for p in &ep.params {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
params.push(format!("{}: {} = undefined", p.name, ty));
} else {
params.push(format!("{}: {}", p.name, ty));
}
}
let param_sig = params.join(", ");
let ep_name = &ep.method;
out.push_str(" /**\n");
if !ep.doc.is_empty() {
out.push_str(&format!(" * {}\n", ep.doc.trim().replace('\n', "\n * ")));
}
out.push_str(" */\n");
match ep.kind {
EntrypointKind::Run => {
out.push_str(&format!(" async {ep_name}({param_sig}): Promise<void> {{\n"));
let native_fn = format!("{}_{}", class_name.to_snake_case(), ep_name);
out.push_str(&format!(" return await {}(this._registrations", native_fn));
for p in &ep.params {
out.push_str(&format!(", {}", p.name));
}
out.push_str(");\n");
out.push_str(" }\n\n");
}
EntrypointKind::Finalize => {
let return_ty = typescript_type_annotation(&ep.return_type);
out.push_str(&format!(" {ep_name}({param_sig}): {return_ty} {{\n"));
let native_fn = format!("{}_{}", class_name.to_snake_case(), ep_name);
out.push_str(&format!(" return {}(this._registrations", native_fn));
for p in &ep.params {
out.push_str(&format!(", {}", p.name));
}
out.push_str(");\n");
out.push_str(" }\n\n");
}
}
}
while out.ends_with("\n\n") {
out.pop();
}
out.push_str("}\n");
}
fn gen_registration_method_ts(out: &mut String, reg: &RegistrationDef, service: &ServiceDef, _api: &ApiSurface) {
let method_name = ®.method;
let _class_name = &service.name;
let mut meta_params: Vec<String> = reg
.metadata_params
.iter()
.map(|p| {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
format!("{}: {} = undefined", p.name, ty)
} else {
format!("{}: {}", p.name, ty)
}
})
.collect();
let meta_sig = meta_params.join(", ");
out.push_str(" /**\n");
if !reg.doc.is_empty() {
out.push_str(&format!(" * {}\n", reg.doc.trim().replace('\n', "\n * ")));
}
out.push_str(" */\n");
out.push_str(&format!(
" {method_name}({meta_sig}): (fn: (...args: any[]) => any) => (...args: any[]) => any {{\n"
));
let meta_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
let meta_array = if meta_names.is_empty() {
"[]".to_owned()
} else {
format!("[{}]", meta_names.join(", "))
};
out.push_str(" return (fn: (...args: any[]) => any) => {\n");
out.push_str(&format!(
" this._registrations.push([\"{method_name}\", {meta_array}, fn]);\n"
));
out.push_str(" return fn;\n");
out.push_str(" };\n");
out.push_str(" }\n\n");
let direct_name = format!("register_{method_name}");
if direct_name != *method_name {
out.push_str(" /**\n");
out.push_str(&format!(" * Register a {method_name} callback directly.\n"));
out.push_str(" */\n");
meta_params.push("handler: (...args: any[]) => any".to_string());
let full_sig = meta_params.join(", ");
out.push_str(&format!(" {direct_name}({full_sig}): this {{\n"));
out.push_str(&format!(
" this._registrations.push([\"{method_name}\", {meta_array}, handler]);\n"
));
out.push_str(" return this;\n");
out.push_str(" }\n\n");
}
for variant in ®.variants {
gen_registration_variant_method_ts(out, variant, reg, service);
}
}
fn gen_registration_variant_method_ts(
out: &mut String,
variant: &crate::core::ir::RegistrationVariant,
reg: &RegistrationDef,
_service: &ServiceDef,
) {
use crate::core::ir::RegistrationVariantStyle;
let variant_name = &variant.name;
let base_method = ®.method;
let variant_params_no_handler: Vec<String> = variant
.signature_params
.iter()
.map(|p| {
let ty = typescript_type_annotation(&p.ty);
if p.optional {
format!("{}: {} = undefined", p.name, ty)
} else {
format!("{}: {}", p.name, ty)
}
})
.collect();
let metadata_array = if let Some(wrapper_call) = &variant.wrapper_call {
let wrapper_type = &wrapper_call.wrapper_type_name;
let mut ctor_args = Vec::new();
for arg in &wrapper_call.args {
match arg {
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} => {
let parts: Vec<&str> = value_expr.split("::").collect();
let ts_expr = if parts.len() >= 2 {
format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1])
} else {
value_expr.clone()
};
ctor_args.push(ts_expr);
}
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;
let wrapper_code = format!(" const {metadata_param} = new {wrapper_type}({ctor_arg_str});\n");
(wrapper_code, format!("[{metadata_param}]"))
} else {
let mut metadata_values = Vec::new();
for param in &variant.signature_params {
metadata_values.push(param.name.clone());
}
let metadata_expr = if metadata_values.is_empty() {
"[]".to_owned()
} else {
format!("[{}]", metadata_values.join(", "))
};
("".to_owned(), metadata_expr)
};
match variant.style {
RegistrationVariantStyle::VerbDecorator => {
emit_variant_direct_method(
out,
variant_name,
&variant_params_no_handler,
base_method,
&metadata_array.0,
&metadata_array.1,
variant,
);
}
RegistrationVariantStyle::Builder => {
emit_variant_decorator_factory(
out,
variant_name,
&variant_params_no_handler,
base_method,
&metadata_array.0,
&metadata_array.1,
variant,
);
}
RegistrationVariantStyle::Hybrid => {
emit_variant_direct_method(
out,
variant_name,
&variant_params_no_handler,
base_method,
&metadata_array.0,
&metadata_array.1,
variant,
);
emit_variant_decorator_factory(
out,
variant_name,
&variant_params_no_handler,
base_method,
&metadata_array.0,
&metadata_array.1,
variant,
);
}
}
}
fn emit_variant_direct_method(
out: &mut String,
variant_name: &str,
variant_params: &[String],
base_method: &str,
wrapper_code: &str,
metadata_array: &str,
variant: &crate::core::ir::RegistrationVariant,
) {
let mut full_params = variant_params.to_vec();
full_params.push("handler: (...args: any[]) => any".to_string());
let full_sig = full_params.join(", ");
out.push_str(" /**\n");
if let Some(doc) = &variant.doc {
out.push_str(&format!(" * {}\n", doc.trim().replace('\n', "\n * ")));
} else {
out.push_str(&format!(" * Register a {} callback directly.\n", variant_name));
}
out.push_str(" */\n");
out.push_str(&format!(" {variant_name}({full_sig}): this {{\n"));
out.push_str(wrapper_code);
out.push_str(&format!(
" this._registrations.push([\"{base_method}\", {metadata_array}, handler]);\n"
));
out.push_str(" return this;\n");
out.push_str(" }\n\n");
}
fn emit_variant_decorator_factory(
out: &mut String,
variant_name: &str,
variant_params: &[String],
base_method: &str,
wrapper_code: &str,
metadata_array: &str,
variant: &crate::core::ir::RegistrationVariant,
) {
let sig = variant_params.join(", ");
out.push_str(" /**\n");
if let Some(doc) = &variant.doc {
out.push_str(&format!(" * {}\n", doc.trim().replace('\n', "\n * ")));
} else {
out.push_str(&format!(
" * Register a {} callback via decorator factory.\n",
variant_name
));
}
out.push_str(" */\n");
out.push_str(&format!(
" {variant_name}({sig}): (fn: (...args: any[]) => any) => (...args: any[]) => any {{\n"
));
out.push_str(wrapper_code);
out.push_str(" return (fn: (...args: any[]) => any) => {\n");
out.push_str(&format!(
" this._registrations.push([\"{base_method}\", {metadata_array}, fn]);\n"
));
out.push_str(" return fn;\n");
out.push_str(" };\n");
out.push_str(" }\n\n");
}
pub(super) fn gen_service_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let core_import = config.core_import_name();
let mut out = String::new();
out.push_str("#![allow(clippy::too_many_arguments, clippy::unused_async)]\n\n");
out.push_str("use napi::bindgen_prelude::*;\n");
out.push_str("use napi::threadsafe_function::ThreadsafeFunction;\n");
out.push_str("use napi_derive::napi;\n");
out.push_str("use std::sync::Arc;\n\n");
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());
if !has_variants {
continue;
}
let app_type_name = format!("{prefix}{}", service.name);
let mut variant_methods = String::new();
for reg in &service.registrations {
for variant in ®.variants {
gen_variant_napi_method(&mut variant_methods, service, reg, variant, api, &core_import, config);
}
}
let indented: String = variant_methods
.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
out.push_str(&format!("use crate::{app_type_name};\n\n"));
out.push_str(&format!("#[napi]\nimpl {app_type_name} {{\n"));
out.push_str(&indented);
if !indented.ends_with('\n') {
out.push('\n');
}
out.push_str("}\n");
}
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(&format!(
"/// Generated NAPI bridge for the `{trait_name}` contract.\n\
///\n\
/// Wraps a JavaScript callable (async) via ThreadsafeFunction\n\
/// so it can be used as `Arc<dyn {trait_name}>` from Rust async code.\n\
pub struct {bridge_name} {{\n \
handler_fn: ThreadsafeFunction<serde_json::Value, serde_json::Value>,\n\
}}\n\n"
));
out.push_str(&format!(
"impl {bridge_name} {{\n \
/// Create a bridge from a JavaScript callable.\n \
pub fn new(handler_fn: ThreadsafeFunction<serde_json::Value, serde_json::Value>) -> Self {{\n \
Self {{ handler_fn }}\n \
}}\n\
}}\n\n"
));
out.push_str(&format!(
"// SAFETY: ThreadsafeFunction is Send+Sync. We call it only from async contexts\n\
// where the NAPI env is valid (within the async task spawned by call_async).\n\
unsafe impl Send for {bridge_name} {{}}\n\
unsafe impl Sync for {bridge_name} {{}}\n\n"
));
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!("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(&format!(
"impl {core_import}::{trait_name} for {bridge_name} {{\n \
fn {dispatch_name}(\n \
&self{extra_param},\n \
{wire_name}: {req_path},\n \
) -> std::pin::Pin<Box<dyn std::future::Future<Output = {output_type}> + Send + '_>> {{\n \
Box::pin(async move {{\n \
// Serialize request to JSON and call the ThreadsafeFunction\n \
let outcome: {wire_output} = async move {{\n \
let req_json = serde_json::to_value(&{wire_name})\n \
.map_err(|e| Box::new(e) as {box_err})?;\n \
let resp_json = self.handler_fn\n \
.call_async(Ok(req_json))\n \
.await\n \
.map_err(|e| Box::new(e) as {box_err})?;\n \
serde_json::from_value(resp_json)\n \
.map_err(|e| Box::new(e) as {box_err})\n \
}}\n \
.await;\n\n \
{tail}\n \
}})\n \
}}\n\
}}\n\n"
));
}
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(&format!(
"/// Drive `{owner_path}::{ep_method}` from JavaScript.\n\
///\n\
/// Each entry in `registrations` is a `[method_name, metadata, callback]` triple\n\
/// produced by the TypeScript service class.\n\
#[napi]\n\
pub async fn {fn_name}({param_sig}) -> napi::Result<{return_ty}> {{\n"
));
let ctor_call = build_ctor_call_napi(service, owner_path);
out.push_str(&format!(" let mut owner = {ctor_call};\n\n"));
out.push_str(" for (method_name, _metadata, handler_fn) in registrations {\n");
out.push_str(" match method_name.as_str() {\n");
for reg in &service.registrations {
let reg_method = ®.method;
let contract_name = ®.callback_contract;
if let Some(contract) = find_contract(api, contract_name) {
let has_opaque_metadata = reg.metadata_params.iter().any(|p| {
if let TypeRef::Named(n) = &p.ty {
api.types
.iter()
.find(|t| &t.name == n && !t.is_trait && t.is_opaque)
.is_some()
} else {
false
}
});
if has_opaque_metadata {
continue;
}
let bridge_name = format!("{}Bridge", contract.trait_name.to_upper_camel_case());
out.push_str(&format!(" \"{reg_method}\" => {{\n"));
out.push_str(&format!(
" let bridge = {bridge_name}::new(handler_fn);\n"
));
out.push_str(&format!(
" let handler: Arc<dyn {core_import}::{contract_name}> = Arc::new(bridge);\n"
));
if !reg.metadata_params.is_empty() {
for (idx, param) in reg.metadata_params.iter().enumerate() {
let param_name = ¶m.name;
let rust_ty = typeref_to_rust_type(¶m.ty, core_import);
out.push_str(&format!(" let {param_name}: {rust_ty} = {{\n"));
out.push_str(&format!(
" let val = _metadata.get({idx}).ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"missing metadata parameter at index {idx}\"))?;\n"
));
out.push_str(&format!(
" {}\n",
gen_metadata_extraction(¶m.ty, core_import, api)
));
out.push_str(" };\n");
}
let meta_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
out.push_str(&format!(
" owner.{reg_method}({}, handler)\n",
meta_names.join(", ")
));
} else {
out.push_str(&format!(" owner.{reg_method}(handler)\n"));
}
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(" }\n");
}
}
out.push_str(" _ => {}\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
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(", ");
if ep.is_async {
format!(
" 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!(
" owner.{ep_method}({args_str})\n \
.map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n"
)
} else {
format!(" owner.{ep_method}({args_str});\n")
}
}
}
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(", ");
out.push_str(&format!(
"/// Register a handler via the `{variant_name}` variant shortcut.\n"
));
if let Some(doc) = &variant.doc {
out.push_str(&format!("///\n/// {}\n", doc.trim()));
}
out.push_str(&format!(
"#[napi]\npub fn {variant_name}({param_sig}) -> napi::Result<()> {{\n"
));
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(", ");
out.push_str(&format!(
" let {} = {}::{}({});\n",
wrapper_call.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(&format!(" let bridge = {bridge_name}::new(handler);\n"));
out.push_str(&format!(
" let handler_arc: std::sync::Arc<dyn {core_import}::{contract_name}> = std::sync::Arc::new(bridge);\n"
));
}
let meta_args = metadata_names.join(", ");
if inner_accessor == "self" {
if !metadata_names.is_empty() {
out.push_str(&format!(" self.{base_method}({meta_args}, handler_arc)\n"));
} else {
out.push_str(&format!(" self.{base_method}(handler_arc)\n"));
}
} else {
out.push_str(&format!(" let mut inner = {inner_accessor};\n"));
if !metadata_names.is_empty() {
out.push_str(&format!(" inner.{base_method}({meta_args}, handler_arc)\n"));
} else {
out.push_str(&format!(" inner.{base_method}(handler_arc)\n"));
}
}
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(" Ok(())\n");
out.push_str("}\n\n");
}
#[allow(clippy::only_used_in_recursion)]
fn gen_metadata_extraction(ty: &TypeRef, core_import: &str, api: &ApiSurface) -> String {
match ty {
TypeRef::String | TypeRef::Char => {
"val.as_str().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected string metadata\"))?.to_owned()".to_owned()
}
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => {
"val.as_bool().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected bool metadata\"))?".to_owned()
}
PrimitiveType::F64 => {
"val.as_f64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))?".to_owned()
}
PrimitiveType::F32 => {
"val.as_f64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as f32".to_owned()
}
PrimitiveType::U8 => {
"val.as_u64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as u8".to_owned()
}
PrimitiveType::U16 => {
"val.as_u64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as u16".to_owned()
}
PrimitiveType::U32 => {
"val.as_u64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as u32".to_owned()
}
PrimitiveType::U64 | PrimitiveType::Usize => {
"val.as_u64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))?".to_owned()
}
PrimitiveType::I8 => {
"val.as_i64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as i8".to_owned()
}
PrimitiveType::I16 => {
"val.as_i64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as i16".to_owned()
}
PrimitiveType::I32 => {
"val.as_i64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))? as i32".to_owned()
}
PrimitiveType::I64 | PrimitiveType::Isize => {
"val.as_i64().ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"expected number metadata\"))?".to_owned()
}
}
}
TypeRef::Optional(inner) => {
let inner_extraction = gen_metadata_extraction(inner, core_import, api);
format!("if val.is_null() {{ None }} else {{ Some({{ {inner_extraction} }}) }}")
}
TypeRef::Named(n) => {
let is_opaque = api.types
.iter()
.find(|t| &t.name == n && !t.is_trait && t.is_opaque)
.is_some();
if is_opaque {
format!(
"{{ \
let binding = serde_json::from_value::<crate::{name}>(val.clone()) \
.map_err(|e| napi::Error::from_reason(format!(\"opaque type deserialization failed: {{}}\", e)))?; \
binding.inner.clone() \
}}",
name = n
)
} else {
"serde_json::from_value(val.clone())
.map_err(|e| napi::Error::from_reason(format!(\"metadata deserialization failed: {}\", e)))?".to_owned()
}
}
_ => {
"serde_json::from_value(val.clone())
.map_err(|e| napi::Error::from_reason(format!(\"metadata deserialization failed: {}\", e)))?".to_owned()
}
}
}
fn typeref_to_rust_type(ty: &TypeRef, core_import: &str) -> String {
match ty {
TypeRef::String | TypeRef::Char => "String".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "u8".to_owned(),
PrimitiveType::U16 => "u16".to_owned(),
PrimitiveType::U32 => "u32".to_owned(),
PrimitiveType::U64 => "u64".to_owned(),
PrimitiveType::I8 => "i8".to_owned(),
PrimitiveType::I16 => "i16".to_owned(),
PrimitiveType::I32 => "i32".to_owned(),
PrimitiveType::I64 => "i64".to_owned(),
PrimitiveType::F32 => "f32".to_owned(),
PrimitiveType::F64 => "f64".to_owned(),
PrimitiveType::Usize => "usize".to_owned(),
PrimitiveType::Isize => "isize".to_owned(),
}
}
TypeRef::Bytes => "Vec<u8>".to_owned(),
TypeRef::Optional(inner) => format!("Option<{}>", typeref_to_rust_type(inner, core_import)),
TypeRef::Vec(inner) => format!("Vec<{}>", typeref_to_rust_type(inner, core_import)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
typeref_to_rust_type(k, core_import),
typeref_to_rust_type(v, core_import)
),
TypeRef::Unit => "()".to_owned(),
TypeRef::Named(n) => format!("{core_import}::{n}"),
TypeRef::Json => "serde_json::Value".to_owned(),
TypeRef::Path => "std::path::PathBuf".to_owned(),
TypeRef::Duration => "std::time::Duration".to_owned(),
}
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
use crate::core::config::resolve_output_dir;
let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
let crate_root = {
let p = PathBuf::from(&output_dir);
match p.file_name().and_then(|n| n.to_str()) {
Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
_ => p,
}
};
let package_name = config.name.replace('-', "_");
let service_rs = gen_service_rs(api, config);
let service_ts = gen_service_ts(api, &package_name);
let _service_js = strip_typescript_annotations(&service_ts);
let output_base = config
.node
.as_ref()
.and_then(|n| n.package_name.as_ref())
.map(|p| PathBuf::from(format!("packages/node/{}", p)))
.unwrap_or_else(|| PathBuf::from(format!("packages/node/{}", package_name)));
Ok(vec![
GeneratedFile {
path: PathBuf::from(&output_dir).join("service.rs"),
content: service_rs,
generated_header: true,
},
GeneratedFile {
path: output_base.join("service.ts"),
content: service_ts.clone(),
generated_header: true,
},
GeneratedFile {
path: crate_root.join("service.ts"),
content: service_ts,
generated_header: true,
},
])
}
fn strip_typescript_annotations(ts_code: &str) -> String {
let mut result = String::new();
for line in ts_code.lines() {
let mut modified_line = line.to_string();
if modified_line.trim().starts_with("import type {") && modified_line.contains("from") {
if let Some(start_brace) = modified_line.find('{') {
if let Some(end_brace) = modified_line.rfind('}') {
if let Some(from_pos) = modified_line.find("from") {
let imports = modified_line[start_brace..=end_brace].to_string();
let module_part = modified_line[from_pos..].trim();
modified_line = format!("const {imports} = require({}", &module_part[5..]);
}
}
}
result.push_str(&modified_line);
result.push('\n');
continue;
}
if modified_line.trim().starts_with("import {") && modified_line.contains("from") {
if let Some(start_brace) = modified_line.find('{') {
if let Some(end_brace) = modified_line.rfind('}') {
if let Some(from_pos) = modified_line.find("from") {
let imports = modified_line[start_brace..=end_brace].to_string();
let module_part = modified_line[from_pos..].trim();
modified_line = format!("const {imports} = require({}", &module_part[5..]);
}
}
}
result.push_str(&modified_line);
result.push('\n');
continue;
}
if modified_line.trim().starts_with("export class") {
modified_line = modified_line.replace("export class", "class");
}
if modified_line.contains("private ") {
modified_line = modified_line.replace("private ", "");
}
let mut output = String::new();
let chars: Vec<char> = modified_line.chars().collect();
let mut i = 0;
while i < chars.len() {
if i < chars.len() - 1 && chars[i] == ':' && !modified_line[..i].ends_with("://") {
let mut j = i + 1;
while j < chars.len() && (chars[j] == ' ' || chars[j] == '\t') {
j += 1;
}
let mut paren_depth = 0;
let mut angle_depth = 0;
while j < chars.len() {
match chars[j] {
'(' => paren_depth += 1,
')' => {
if paren_depth == 0 {
break;
}
paren_depth -= 1;
}
'<' => angle_depth += 1,
'>' => angle_depth -= 1,
',' | '=' | '{' | ';' if paren_depth == 0 && angle_depth == 0 => {
break;
}
_ => {}
}
j += 1;
}
i = j;
while !output.is_empty() && output.ends_with(' ') {
output.pop();
}
if i < chars.len() && chars[i] != ',' && chars[i] != ')' && !output.is_empty() {
output.push(' ');
}
continue;
}
output.push(chars[i]);
i += 1;
}
modified_line = output;
result.push_str(&modified_line);
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind,
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 configurator = MethodDef {
name: "with_timeout".to_owned(),
params: vec![ParamDef {
name: "timeout_ms".to_owned(),
ty: TypeRef::Primitive(PrimitiveType::U64),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("TestService".to_owned()),
is_async: false,
is_static: false,
error_type: None,
doc: "Set request timeout.".to_owned(),
receiver: Some(ReceiverKind::RefMut),
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()
},
ParamDef {
name: "method".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
],
receiver: Some(ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: None,
doc: "Register a request handler for a path and method.".to_owned(),
variants: vec![],
};
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![configurator],
registrations: vec![registration],
entrypoints: vec![run_ep],
doc: "A test service owner.".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(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()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Async trait for handling requests.".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 typescript_output_contains_service_class() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("export class TestService"),
"expected `export class TestService` in output:\n{output}"
);
}
#[test]
fn typescript_output_contains_constructor() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("constructor()"),
"expected `constructor()` in output:\n{output}"
);
}
#[test]
fn typescript_output_contains_private_registrations() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("private _registrations"),
"expected `private _registrations` in output:\n{output}"
);
}
#[test]
fn typescript_output_contains_configurator() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("with_timeout(timeout_ms: number)"),
"expected `with_timeout` configurator:\n{output}"
);
assert!(
output.contains("return this;"),
"expected `return this;` in configurator:\n{output}"
);
}
#[test]
fn typescript_output_contains_registration_method() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("add_handler(path: string, method: string)"),
"expected `add_handler` registration method:\n{output}"
);
}
#[test]
fn typescript_output_contains_run_entrypoint() {
let surface = make_fixture_surface();
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("async run(addr: string)"),
"expected `async run` entrypoint:\n{output}"
);
}
#[test]
fn rust_output_contains_handler_bridge() {
let surface = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub struct RequestHandlerBridge"),
"expected `RequestHandlerBridge` struct in output:\n{output}"
);
}
#[test]
fn rust_output_contains_run_function() {
let surface = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub async fn test_service_run"),
"expected `test_service_run` function in output:\n{output}"
);
}
#[test]
fn rust_output_contains_thread_safe_function() {
let surface = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("ThreadsafeFunction"),
"expected `ThreadsafeFunction` in output:\n{output}"
);
}
#[test]
fn rust_output_implements_trait() {
let surface = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("impl my_crate::RequestHandler for RequestHandlerBridge"),
"expected trait impl in output:\n{output}"
);
}
#[test]
fn rust_output_extracts_metadata_params() {
let surface = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
!output.contains("/* TODO: extract metadata */"),
"expected no TODO placeholder in output:\n{output}"
);
assert!(
!output.contains("TODO: extract metadata"),
"expected no TODO marker in output:\n{output}"
);
assert!(
output.contains("let path: String"),
"expected `let path: String` extraction in output:\n{output}"
);
assert!(
output.contains("let method: String"),
"expected `let method: String` extraction in output:\n{output}"
);
assert!(
output.contains("owner.add_handler(path, method, handler)"),
"expected owner.add_handler(path, method, handler) call in output:\n{output}"
);
assert!(
output.contains("_metadata.get("),
"expected _metadata.get(...) access in output:\n{output}"
);
}
#[test]
fn registration_variants_emit_napi_methods() {
use crate::core::ir::{RegistrationVariant, WrapperConstructorArg, WrapperConstructorCall};
let mut surface = make_fixture_surface();
if let Some(reg) = surface.services[0].registrations.first_mut() {
reg.variants.push(RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: Some(WrapperConstructorCall {
metadata_param: "builder".to_owned(),
wrapper_type_path: "my_crate::RouteBuilder".to_owned(),
wrapper_type_name: "RouteBuilder".to_owned(),
constructor_method: "new".to_owned(),
args: vec![
WrapperConstructorArg::Fixed {
param_name: "method".to_owned(),
value_expr: "my_crate::Method::GET".to_owned(),
},
WrapperConstructorArg::Free {
param: ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
},
],
}),
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: Default::default(),
});
}
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("impl JsTestService {"),
"expected `impl JsTestService {{` wrapping in output:\n{output}"
);
assert!(
output.contains("use crate::JsTestService;"),
"expected `use crate::JsTestService;` in output:\n{output}"
);
assert!(
output.contains("#[napi]\n pub fn get("),
"expected `#[napi]\\n pub fn get(` inside impl block in output:\n{output}"
);
assert!(
output.contains("my_crate::RouteBuilder::new("),
"expected wrapper constructor call in output:\n{output}"
);
assert!(
output.contains("my_crate::Method::GET"),
"expected fixed arg substitution in output:\n{output}"
);
}
#[test]
fn typescript_variant_verb_decorator_style() {
use crate::core::ir::{RegistrationVariant, RegistrationVariantStyle};
let mut surface = make_fixture_surface();
if let Some(reg) = surface.services[0].registrations.first_mut() {
reg.variants.push(RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: None,
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: RegistrationVariantStyle::VerbDecorator,
});
}
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("get(path: string, handler: (...args: any[]) => any): this"),
"expected VerbDecorator form `get(path, handler): this` in output:\n{output}"
);
assert!(
output.contains("return this;"),
"expected `return this;` for chaining in VerbDecorator form:\n{output}"
);
let get_count = output.matches(" get(").count();
assert_eq!(
get_count, 1,
"expected exactly one `get(` method in VerbDecorator style, found {}: {}",
get_count, output
);
}
#[test]
fn typescript_variant_builder_style() {
use crate::core::ir::{RegistrationVariant, RegistrationVariantStyle};
let mut surface = make_fixture_surface();
if let Some(reg) = surface.services[0].registrations.first_mut() {
reg.variants.push(RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: None,
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: RegistrationVariantStyle::Builder,
});
}
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("get(path: string): (fn: (...args: any[]) => any) => (...args: any[]) => any"),
"expected Builder form `get(path): (fn) => ...` in output:\n{output}"
);
assert!(
output.contains("return fn;"),
"expected `return fn;` in Builder form:\n{output}"
);
assert!(
!output.contains("get(path: string, handler: (...args: any[]) => any): this"),
"Builder form should not emit direct method with handler parameter:\n{output}"
);
}
#[test]
fn typescript_variant_hybrid_style() {
use crate::core::ir::{RegistrationVariant, RegistrationVariantStyle};
let mut surface = make_fixture_surface();
if let Some(reg) = surface.services[0].registrations.first_mut() {
reg.variants.push(RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: None,
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: RegistrationVariantStyle::Hybrid,
});
}
let output = gen_service_ts(&surface, "my_crate");
assert!(
output.contains("get(path: string, handler: (...args: any[]) => any): this"),
"expected Hybrid to include direct form `get(path, handler): this`:\n{output}"
);
assert!(
output.contains("get(path: string): (fn: (...args: any[]) => any) => (...args: any[]) => any"),
"expected Hybrid to include factory form `get(path): (fn) => ...`:\n{output}"
);
let this_count = output.matches("return this;").count();
let fn_count = output.matches("return fn;").count();
assert!(
this_count >= 1 && fn_count >= 1,
"Hybrid form should have both return forms; this={}, fn={}: {}",
this_count,
fn_count,
output
);
}
}