use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{
ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, RegistrationVariantStyle, ServiceDef, TypeRef,
};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
#[allow(dead_code)]
fn elixir_type_annotation(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char => "String.t()".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "boolean()".to_owned(),
PrimitiveType::F32 | PrimitiveType::F64 => "float()".to_owned(),
_ => "integer()".to_owned(),
}
}
TypeRef::Bytes => "binary()".to_owned(),
TypeRef::Optional(inner) => format!("{} | nil", elixir_type_annotation(inner)),
TypeRef::Vec(inner) => format!("list({})", elixir_type_annotation(inner)),
TypeRef::Map(k, v) => format!(
"map() :: %{{optional({}) => {}}}",
elixir_type_annotation(k),
elixir_type_annotation(v)
),
TypeRef::Unit => "nil".to_owned(),
TypeRef::Named(n) => n.to_string(),
TypeRef::Json => "any()".to_owned(),
TypeRef::Path => "String.t()".to_owned(),
TypeRef::Duration => "non_neg_integer()".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_ex(api: &ApiSurface, module_prefix: &str) -> String {
let mut out = String::new();
out.push_str("# This file is generated. Do not edit.\n\n");
for service in &api.services {
gen_service_module(&mut out, service, api, module_prefix);
}
out
}
fn elixir_heredoc_body(text: &str, indent: usize) -> String {
let trimmed = text.trim();
if trimmed.is_empty() {
return String::new();
}
let pad = " ".repeat(indent);
let mut out = String::new();
for line in trimmed.lines() {
if line.trim().is_empty() {
out.push('\n');
} else {
out.push_str(&pad);
out.push_str(line);
out.push('\n');
}
}
out
}
fn gen_service_module(out: &mut String, service: &ServiceDef, api: &ApiSurface, module_prefix: &str) {
let module_name = &service.name;
let module_snake = service.name.to_snake_case();
out.push_str(&format!("defmodule {module_name} do\n"));
if !service.doc.is_empty() {
out.push_str(" @moduledoc \"\"\"\n");
out.push_str(&elixir_heredoc_body(&service.doc, 2));
out.push_str(" \"\"\"\n\n");
}
if !module_prefix.is_empty() {
out.push_str(&format!(" alias {module_prefix}.Native\n\n"));
}
out.push_str(" defstruct [\n");
out.push_str(" :registrations,\n");
for p in &service.constructor.params {
out.push_str(&format!(" :{},\n", p.name));
}
for method in &service.configurators {
for p in &method.params {
out.push_str(&format!(" :{},\n", p.name));
}
}
out.push_str(" ]\n\n");
{
let ctor = &service.constructor;
let params = ["options \\\\ []".to_owned()];
let mut field_inits = vec!["registrations: []".to_owned()];
for p in &ctor.params {
if p.optional {
field_inits.push(format!("{}: Keyword.get(options, :{}, nil)", p.name, p.name));
} else {
field_inits.push(format!("{}: Keyword.fetch!(options, :{})", p.name, p.name));
}
}
if !ctor.doc.is_empty() {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(&ctor.doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def new({}) do\n", params.join(", ")));
out.push_str(" %__MODULE__{\n");
for init in field_inits {
out.push_str(&format!(" {},\n", init));
}
out.push_str(" }\n");
out.push_str(" end\n\n");
}
for method in &service.configurators {
let method_name = &method.name;
let mut params = vec!["self".to_owned()];
for p in &method.params {
if p.optional {
params.push(format!("{} \\\\ nil", p.name));
} else {
params.push(p.name.clone());
}
}
if !method.doc.is_empty() {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(&method.doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}({}) do\n", method_name, params.join(", ")));
for p in &method.params {
out.push_str(&format!(" self = %__MODULE__{{self | {}: {}}}\n", p.name, p.name));
}
out.push_str(" self\n");
out.push_str(" end\n\n");
}
for reg in &service.registrations {
gen_registration_method(out, reg, service, api, module_prefix);
}
gen_genserver_module(out, service, api);
for ep in &service.entrypoints {
let ep_name = &ep.method;
let mut params = vec!["self".to_owned()];
for p in &ep.params {
if p.optional {
params.push(format!("{} \\\\ nil", p.name));
} else {
params.push(p.name.clone());
}
}
match ep.kind {
EntrypointKind::Run => {
if !ep.doc.is_empty() {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(&ep.doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}({}) do\n", ep_name, params.join(", ")));
let native_fn = format!("{}_{}", module_snake, ep_name);
out.push_str(&format!(" Native.{}(self.registrations", native_fn));
for p in &ep.params {
out.push_str(&format!(", {}", p.name));
}
out.push_str(")\n");
out.push_str(" end\n\n");
}
EntrypointKind::Finalize => {
if !ep.doc.is_empty() {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(&ep.doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}({}) do\n", ep_name, params.join(", ")));
let native_fn = format!("{}_{}", module_snake, ep_name);
out.push_str(&format!(" Native.{}(self.registrations", native_fn));
for p in &ep.params {
out.push_str(&format!(", {}", p.name));
}
out.push_str(")\n");
out.push_str(" end\n\n");
}
}
}
out.push_str("end\n\n");
}
fn gen_registration_method(
out: &mut String,
reg: &RegistrationDef,
_service: &ServiceDef,
_api: &ApiSurface,
module_prefix: &str,
) {
let method_name = ®.method;
if !reg.doc.is_empty() {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(®.doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}(self", method_name));
for p in ®.metadata_params {
if p.optional {
out.push_str(&format!(", {} \\\\ nil", p.name));
} else {
out.push_str(&format!(", {}", p.name));
}
}
out.push_str(", handler) do\n");
let meta_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
let meta_tuple = if meta_names.is_empty() {
"{}".to_owned()
} else {
format!("{{{}}}", meta_names.join(", "))
};
out.push_str(" # Wrap handler closure in a process if it's not already one\n");
out.push_str(" handler_pid = case handler do\n");
out.push_str(" pid when is_pid(pid) -> pid\n");
out.push_str(" fun when is_function(fun) ->\n");
out.push_str(" {:ok, pid} = GenServer.start_link(__MODULE__.HandlerWrapper, fun)\n");
out.push_str(" pid\n");
out.push_str(" end\n\n");
out.push_str(&format!(
" entry = {{\"{}\", {}, handler_pid}}\n",
method_name, meta_tuple
));
out.push_str(" %__MODULE__{self | registrations: [entry | self.registrations]}\n");
out.push_str(" end\n\n");
if method_name == "route" {
out.push_str(" # HandlerWrapper GenServer: wraps a closure for use as a handler\n");
out.push_str(" defmodule HandlerWrapper do\n");
out.push_str(" use GenServer\n\n");
out.push_str(" def start_link(handler_fn) do\n");
out.push_str(" GenServer.start_link(__MODULE__, handler_fn)\n");
out.push_str(" end\n\n");
out.push_str(" def init(handler_fn) do\n");
out.push_str(" {:ok, handler_fn}\n");
out.push_str(" end\n\n");
out.push_str(" def handle_cast({:trait_call, _method, args_json, reply_id}, handler_fn) do\n");
out.push_str(" case Jason.decode(args_json) do\n");
out.push_str(" {:ok, _args} ->\n");
out.push_str(" # Call the wrapped closure\n");
out.push_str(" try do\n");
out.push_str(" response = handler_fn.(nil)\n");
out.push_str(" response_json = Jason.encode!(response)\n");
out.push_str(" Native.complete_trait_call(reply_id, response_json)\n");
out.push_str(" rescue\n");
out.push_str(
" _e -> Native.complete_trait_call(reply_id, \"{\\\"error\\\": \\\"handler error\\\"}\")\n",
);
out.push_str(" end\n");
out.push_str(" {:error, _} ->\n");
out.push_str(
" Native.complete_trait_call(reply_id, \"{\\\"error\\\": \\\"json decode error\\\"}\")\n",
);
out.push_str(" end\n");
out.push_str(" {:noreply, handler_fn}\n");
out.push_str(" end\n");
out.push_str(" end\n\n");
}
for variant in ®.variants {
gen_registration_variant_method(out, variant, reg, module_prefix);
}
}
fn gen_registration_variant_method(
out: &mut String,
variant: &crate::core::ir::RegistrationVariant,
base_reg: &RegistrationDef,
module_prefix: &str,
) {
match variant.style {
RegistrationVariantStyle::VerbDecorator => {
emit_verb_decorator_variant(out, variant, base_reg, module_prefix);
}
RegistrationVariantStyle::Builder => {
emit_builder_variant(out, variant, base_reg, module_prefix);
}
RegistrationVariantStyle::Hybrid => {
emit_verb_decorator_variant(out, variant, base_reg, module_prefix);
emit_builder_variant(out, variant, base_reg, module_prefix);
}
}
}
fn rust_enum_expr_to_elixir(value_expr: &str, module_prefix: &str) -> String {
let parts: Vec<&str> = value_expr.split("::").collect();
if parts.len() >= 2 {
let type_name = parts[parts.len() - 2];
let variant = parts[parts.len() - 1].to_lowercase();
if module_prefix.is_empty() {
format!("{type_name}.{variant}()")
} else {
format!("{module_prefix}.{type_name}.{variant}()")
}
} else {
value_expr.to_owned()
}
}
fn build_elixir_wrapper_constructor_expr(
variant: &crate::core::ir::RegistrationVariant,
module_prefix: &str,
) -> Option<String> {
let wc = variant.wrapper_call.as_ref()?;
let mut call_args: Vec<String> = Vec::new();
for arg in &wc.args {
match arg {
crate::core::ir::WrapperConstructorArg::Fixed { value_expr, .. } => {
call_args.push(rust_enum_expr_to_elixir(value_expr, module_prefix));
}
crate::core::ir::WrapperConstructorArg::Free { param } => {
call_args.push(param.name.clone());
}
}
}
let type_name = &wc.wrapper_type_name;
let ctor = &wc.constructor_method;
let qualified_type = if module_prefix.is_empty() {
type_name.clone()
} else {
format!("{module_prefix}.{type_name}")
};
let call_expr = if ctor.is_empty() || ctor == "__init__" {
format!("{qualified_type}({})", call_args.join(", "))
} else {
format!("{qualified_type}.{ctor}({})", call_args.join(", "))
};
Some(format!("{} = {}", wc.metadata_param, call_expr))
}
fn emit_verb_decorator_variant(
out: &mut String,
variant: &crate::core::ir::RegistrationVariant,
base_reg: &RegistrationDef,
module_prefix: &str,
) {
let variant_name = &variant.name;
let base_method = &base_reg.method;
if let Some(doc) = &variant.doc {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}(app", variant_name));
for param in &variant.signature_params {
if param.optional {
out.push_str(&format!(", {} \\\\ nil", param.name));
} else {
out.push_str(&format!(", {}", param.name));
}
}
out.push_str(", handler) do\n");
if let Some(wrapper_expr) = build_elixir_wrapper_constructor_expr(variant, module_prefix) {
out.push_str(&format!(" {wrapper_expr}\n"));
out.push_str(&format!(
" {}(app, {}, handler)\n",
base_method,
base_reg
.metadata_params
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>()
.join(", ")
));
} else {
let mut call_args: Vec<String> = Vec::new();
for base_param in &base_reg.metadata_params {
if let Some(override_) = variant.overrides.iter().find(|o| o.param_name == base_param.name) {
call_args.push(rust_enum_expr_to_elixir(&override_.value_expr, module_prefix));
} else if let Some(sig_param) = variant.signature_params.iter().find(|s| s.name == base_param.name) {
call_args.push(sig_param.name.clone());
}
}
let args_str = if call_args.is_empty() {
String::new()
} else {
format!(", {}", call_args.join(", "))
};
out.push_str(&format!(" {}(app{}, handler)\n", base_method, args_str));
}
out.push_str(" end\n\n");
}
fn emit_builder_variant(
out: &mut String,
variant: &crate::core::ir::RegistrationVariant,
_base_reg: &RegistrationDef,
module_prefix: &str,
) {
let variant_name = &variant.name;
let builder_name = format!("{}_decorator", variant_name);
if let Some(doc) = &variant.doc {
out.push_str(" @doc \"\"\"\n");
out.push_str(&elixir_heredoc_body(doc, 2));
out.push_str(" \"\"\"\n");
}
out.push_str(&format!(" def {}(app", builder_name));
for param in &variant.signature_params {
if param.optional {
out.push_str(&format!(", {} \\\\ nil", param.name));
} else {
out.push_str(&format!(", {}", param.name));
}
}
out.push_str(") do\n");
out.push_str(" fn(handler) ->\n");
out.push_str(&format!(" {}(app", variant_name));
for param in &variant.signature_params {
out.push_str(&format!(", {}", param.name));
}
out.push_str(", handler)\n");
out.push_str(" end\n");
out.push_str(" end\n\n");
let _ = module_prefix; }
fn gen_genserver_module(out: &mut String, service: &ServiceDef, _api: &ApiSurface) {
let module_name = &service.name;
let server_module = format!("{}.Handler", module_name);
out.push_str(" # GenServer for dispatching trait_call messages from Rust.\n");
out.push_str(&format!(" defmodule {} do\n", server_module));
out.push_str(" use GenServer\n\n");
out.push_str(" def start_link(state) do\n");
out.push_str(" GenServer.start_link(__MODULE__, state)\n");
out.push_str(" end\n\n");
out.push_str(" def init(state) do\n");
out.push_str(" {:ok, state}\n");
out.push_str(" end\n\n");
out.push_str(" def handle_cast({:trait_call, method, args_json, reply_id}, registrations) do\n");
out.push_str(" # Decode JSON args and dispatch to registered handler\n");
out.push_str(" case decode_args_and_dispatch(method, args_json, registrations) do\n");
out.push_str(" {:ok, response} ->\n");
out.push_str(" Native.complete_trait_call(reply_id, response)\n");
out.push_str(" {:error, reason} ->\n");
out.push_str(" error_response = %{\"error\" => reason}\n");
out.push_str(" Native.complete_trait_call(reply_id, error_response)\n");
out.push_str(" end\n");
out.push_str(" {:noreply, registrations}\n");
out.push_str(" end\n\n");
out.push_str(" defp decode_args_and_dispatch(method, args_json, registrations) do\n");
out.push_str(" # Find handler entry for the method\n");
out.push_str(" case find_handler(method, registrations) do\n");
out.push_str(" nil ->\n");
out.push_str(" {:error, \"Handler not registered for method: #{method}\"}\n");
out.push_str(" {^method, _metadata, handler} ->\n");
out.push_str(" # Decode JSON args (assumes handler accepts a single arg)\n");
out.push_str(" case Jason.decode(args_json) do\n");
out.push_str(" {:ok, args} ->\n");
out.push_str(" # Call the registered handler with decoded args\n");
out.push_str(" try do\n");
out.push_str(" response = handler.(args)\n");
out.push_str(" # Encode response to JSON\n");
out.push_str(" case Jason.encode(response) do\n");
out.push_str(" {:ok, response_json} -> {:ok, response_json}\n");
out.push_str(" {:error, reason} -> {:error, \"Failed to encode response: #{reason}\"}\n");
out.push_str(" end\n");
out.push_str(" rescue\n");
out.push_str(" e ->\n");
out.push_str(" {:error, \"Handler raised exception: #{inspect(e)}\"}\n");
out.push_str(" end\n");
out.push_str(" {:error, reason} ->\n");
out.push_str(" {:error, \"Failed to decode args: #{reason}\"}\n");
out.push_str(" end\n");
out.push_str(" end\n");
out.push_str(" end\n\n");
out.push_str(" defp find_handler(_method, []), do: nil\n");
out.push_str(
" defp find_handler(target, [{name, _metadata, _handler} = entry | _rest]) when name == target do\n",
);
out.push_str(" entry\n");
out.push_str(" end\n");
out.push_str(" defp find_handler(target, [_head | rest]) do\n");
out.push_str(" find_handler(target, rest)\n");
out.push_str(" end\n\n");
out.push_str(" end\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 rustler::{Encoder, LocalPid, NifResult, OwnedEnv, ResourceArc, types::atom::Atom};\n");
out.push_str("use rustler::Error as NifError;\n");
out.push_str("use std::collections::HashMap;\n");
out.push_str("use std::sync::Arc;\n");
out.push_str("use std::sync::atomic::{AtomicU64, Ordering};\n");
out.push_str("use std::sync::{Mutex, OnceLock};\n");
out.push_str(&format!("use {}::*;\n\n", core_import));
out.push_str("/// Atom constants used by the service NIFs.\n");
out.push_str("mod atoms {\n");
out.push_str(" rustler::atoms! {\n");
out.push_str(" ok,\n");
out.push_str(" error,\n");
out.push_str(" trait_call,\n");
out.push_str(" }\n");
out.push_str("}\n\n");
out.push_str("static REPLY_ID_COUNTER: AtomicU64 = AtomicU64::new(0);\n\n");
out.push_str(
"type TraitReplySender = tokio::sync::oneshot::Sender<String>;\n\
type TraitReplyMap = Mutex<HashMap<u64, TraitReplySender>>;\n\n\
static TRAIT_REPLY_MAP: OnceLock<TraitReplyMap> = OnceLock::new();\n\n\
fn trait_reply_map() -> &'static TraitReplyMap {\n \
TRAIT_REPLY_MAP.get_or_init(|| Mutex::new(HashMap::new()))\n\
}\n\n",
);
out.push_str(
"/// Complete a pending trait call with the JSON response from Elixir.\n\
#[rustler::nif]\n\
pub fn complete_trait_call(reply_id: u64, response_json: String) -> Atom {\n \
if let Some(tx) = trait_reply_map().lock().unwrap().remove(&reply_id) {\n \
let _ = tx.send(response_json);\n \
}\n \
atoms::ok()\n\
}\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_nif(&mut out, service, ep, api, &core_import);
}
for reg in &service.registrations {
for variant in ®.variants {
gen_registration_variant_nif(&mut out, service, reg, variant, api, &core_import);
}
}
}
out
}
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
let trait_name = &contract.trait_name;
let bridge_name = format!("Elixir{}Bridge", trait_name.to_upper_camel_case());
let dispatch_name = &contract.dispatch.name;
let _unused = bridge_name.clone();
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");
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");
out.push_str(&format!(
"/// Generated rustler bridge for the `{trait_name}` contract.\n\
///\n\
/// Wraps an Elixir GenServer pid so it can be used\n\
/// as `Arc<dyn {trait_name}>` from Rust async code.\n\
/// Uses message-passing to avoid blocking the BEAM scheduler.\n\
/// Pending replies are stored in the module-level `TRAIT_REPLY_MAP`\n\
/// keyed by `reply_id`; the GenServer completes them via the\n\
/// `complete_trait_call` NIF.\n\
pub struct {bridge_name} {{\n \
pid: LocalPid,\n\
}}\n\n"
));
out.push_str(&format!(
"impl {bridge_name} {{\n \
/// Create a bridge from an Elixir GenServer pid.\n \
pub fn new(pid: LocalPid) -> Self {{\n \
Self {{ pid }}\n \
}}\n\
}}\n\n"
));
out.push_str(&format!(
"// SAFETY: LocalPid is Send+Sync as guaranteed by Rustler.\n\
unsafe impl Send for {bridge_name} {{}}\n\
unsafe impl Sync for {bridge_name} {{}}\n\n"
));
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 \
let outcome: {wire_output} = async move {{\n \
let request_json = serde_json::to_string(&{wire_name})\n \
.map_err(|e| Box::new(e) as {box_err})?;\n\n \
let reply_id = REPLY_ID_COUNTER.fetch_add(1, Ordering::Relaxed);\n \
let (tx, rx) = tokio::sync::oneshot::channel();\n \
trait_reply_map().lock().unwrap().insert(reply_id, tx);\n\n \
// Send trait_call message to Elixir GenServer\n \
{{\n \
let pid = self.pid;\n \
let method_name = \"{dispatch_name}\";\n \
let request_json_clone = request_json.clone();\n \
tokio::task::spawn_blocking(move || {{\n \
let mut env = OwnedEnv::new();\n \
let _ = env.send_and_clear(&pid, |env| {{\n \
(Atom::from_str(env, \"trait_call\").unwrap(),\n \
method_name, request_json_clone.as_str(), reply_id).encode(env)\n \
}});\n \
}}).await.ok();\n \
}}\n\n \
// Await response\n \
let response_json = rx.await\n \
.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)) as {box_err})?;\n\n \
let response: {resp_path} = serde_json::from_str(&response_json)\n \
.map_err(|e| Box::new(e) as {box_err})?;\n \
Ok(response)\n \
}}\n \
.await;\n\n \
{tail}\n \
}})\n \
}}\n\
}}\n\n"
));
}
fn gen_run_nif(
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 params = vec!["registrations: rustler::Term<'_>".to_owned()];
for p in &ep.params {
let rust_ty = typeref_to_rust_type(&p.ty, core_import);
params.push(format!("{}: {}", p.name, rust_ty));
}
let param_sig = params.join(", ");
out.push_str(&format!(
"/// Drive `{owner_path}::{ep_method}` from Elixir.\n\
///\n\
/// This NIF is scheduled on the dirty CPU scheduler to avoid blocking\n\
/// the BEAM scheduler during the (potentially long) run operation.\n\
///\n\
/// # Arguments\n\
///\n\
/// - `registrations` — Elixir list of `{{method_name, metadata, handler}}` tuples\n\
/// where `handler` is an Elixir function/closure that accepts request JSON and returns response JSON.\n"
));
for p in &ep.params {
out.push_str(&format!("/// - `{}` — entrypoint parameter\n", p.name));
}
out.push_str("///\n");
out.push_str("/// # Returns\n");
out.push_str("/// `:ok` or `{{:error, reason}}` after the entrypoint completes.\n");
out.push_str(&format!(
"#[rustler::nif(schedule = \"DirtyCpu\")]\n\
pub fn {fn_name}({param_sig}) -> NifResult<Atom> {{\n"
));
out.push_str(" // Parse registrations from Elixir term\n");
out.push_str(" let registration_list: Vec<rustler::Term<'_>> = registrations\n");
out.push_str(" .decode::<Vec<rustler::Term<'_>>>()\n");
out.push_str(" .unwrap_or_else(|_| vec![]);\n\n");
out.push_str(" // Build the service owner from its constructor\n");
out.push_str(&format!(" let mut owner = {owner_path}::new();\n\n"));
out.push_str(" // Register handlers from Elixir registrations\n");
out.push_str(" // Each registration entry is a tuple: {method_name, metadata, handler_pid}\n");
out.push_str(" for reg_entry in registration_list {\n");
out.push_str(" if let Ok((method_name, metadata, handler_pid)) = reg_entry.decode::<(String, rustler::Term<'_>, rustler::LocalPid)>()\n");
out.push_str(" {\n");
for (i, reg) in service.registrations.iter().enumerate() {
let contract_name = ®.callback_contract;
let reg_method = ®.method;
let metadata_param_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
let bridge_wrapper = format!("Elixir{contract_name}Bridge");
if i == 0 {
out.push_str(" ");
} else {
out.push_str(" } else ");
}
out.push_str(&format!("if method_name == \"{}\" {{\n", reg_method));
if !metadata_param_names.is_empty() {
let trailing = if metadata_param_names.len() == 1 { "," } else { "" };
let tuple_types = reg
.metadata_params
.iter()
.map(|p| {
if let TypeRef::Named(n) = &p.ty {
if api.types.iter().any(|t| &t.name == n && !t.is_trait && t.is_opaque) {
return format!("rustler::ResourceArc<super::{}>", n);
}
}
typeref_to_rust_type(&p.ty, core_import)
})
.collect::<Vec<_>>()
.join(", ");
let tuple_types_with_trailing = format!("{}{}", tuple_types, trailing);
out.push_str(&format!(
" if let Ok(({names}{trailing})) = metadata.decode::<({types})>()\n",
names = metadata_param_names.join(", "),
trailing = trailing,
types = tuple_types_with_trailing
));
out.push_str(" {\n");
for meta_param in reg.metadata_params.iter() {
let is_opaque = if let TypeRef::Named(n) = &meta_param.ty {
api.types.iter().any(|t| &t.name == n && !t.is_trait && t.is_opaque)
} else {
false
};
if is_opaque {
if let TypeRef::Named(n) = &meta_param.ty {
out.push_str(&format!(
" let {pname}: {core_import}::{name} = (*{pname}).inner.as_ref().clone();\n",
pname = meta_param.name,
core_import = core_import,
name = n,
));
}
}
}
out.push_str(&format!(
" let bridge = {bridge_wrapper}::new(handler_pid);\n"
));
out.push_str(&format!(
" let handler: Arc<dyn {core_import}::{trait_name}> = Arc::new(bridge);\n",
trait_name = ®.callback_contract
));
let args_list = metadata_param_names
.iter()
.map(|name| format!("{}, ", name))
.collect::<String>();
out.push_str(&format!(
" let _ = owner.{reg_method}({}handler);\n",
args_list
));
out.push_str(" }\n");
} else {
out.push_str(&format!(
" let bridge = {bridge_wrapper}::new(handler_pid);\n"
));
out.push_str(&format!(
" let handler: Arc<dyn {core_import}::{trait_name}> = Arc::new(bridge);\n",
trait_name = ®.callback_contract
));
out.push_str(&format!(" let _ = owner.{reg_method}(handler);\n"));
}
}
if !service.registrations.is_empty() {
out.push_str(" }\n");
}
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" // Call the entrypoint method\n");
match ep.kind {
EntrypointKind::Run => {
let ep_params = ep.params.iter().map(|p| p.name.as_str()).collect::<Vec<_>>().join(", ");
out.push_str(" let rt = tokio::runtime::Runtime::new().map_err(|_e| {\n");
out.push_str(" NifError::Atom(\"runtime_error\")\n");
out.push_str(" })?;\n\n");
if ep.params.is_empty() {
out.push_str(" let result = rt.block_on(owner.run());\n");
} else {
out.push_str(&format!(" let result = rt.block_on(owner.run({}));\n", ep_params));
}
out.push_str(" match result {\n");
out.push_str(" Ok(_) => Ok(atoms::ok()),\n");
out.push_str(" Err(_e) => Err(NifError::Atom(\"error\")),\n");
out.push_str(" }\n");
}
EntrypointKind::Finalize => {
let ep_params = ep.params.iter().map(|p| p.name.as_str()).collect::<Vec<_>>().join(", ");
if ep.params.is_empty() {
out.push_str(&format!(" match owner.{}() {{\n", ep_method));
} else {
out.push_str(&format!(" match owner.{}({}) {{\n", ep_method, ep_params));
}
out.push_str(" Ok(_) => Ok(atoms::ok()),\n");
out.push_str(" Err(_e) => Err(NifError::Atom(\"error\")),\n");
out.push_str(" }\n");
}
}
out.push_str("}\n\n");
}
fn gen_registration_variant_nif(
out: &mut String,
service: &ServiceDef,
base_reg: &RegistrationDef,
variant: &crate::core::ir::RegistrationVariant,
api: &ApiSurface,
core_import: &str,
) {
let service_snake = service.name.to_snake_case();
let variant_name = &variant.name;
let nif_name = format!("{}_{}", service_snake, variant_name);
let base_method = &base_reg.method;
let contract_name = &base_reg.callback_contract;
let bridge_wrapper = format!("Elixir{contract_name}Bridge");
let owner_path = &service.rust_path;
let mut params = vec!["registrations: rustler::Term<'_>".to_owned()];
for param in &variant.signature_params {
let rust_ty = typeref_to_rust_type(¶m.ty, core_import);
params.push(format!("{}: {}", param.name, rust_ty));
}
params.push("handler: rustler::LocalPid".to_owned());
let param_sig = params.join(", ");
out.push_str(&format!(
"/// Registration variant `{}` for the `{}` base method.\n\
///\n\
/// This NIF pre-builds the wrapper and delegates to the base registration.\n\
#[rustler::nif(schedule = \"DirtyCpu\")]\n\
pub fn {}({}) -> NifResult<Atom> {{\n",
variant_name, base_method, nif_name, param_sig
));
out.push_str(" let registration_list: Vec<rustler::Term<'_>> = registrations\n");
out.push_str(" .decode::<Vec<rustler::Term<'_>>>()\n");
out.push_str(" .unwrap_or_else(|_| vec![]);\n\n");
out.push_str(&format!(" let mut owner = {owner_path}::new();\n\n"));
if let Some(wrapper_call) = &variant.wrapper_call {
let wrapper_type_path = &wrapper_call.wrapper_type_path;
let wrapper_type_name = &wrapper_call.wrapper_type_name;
let constructor_method = &wrapper_call.constructor_method;
out.push_str(&format!(
" // Build {} via {}\n",
wrapper_type_name, wrapper_type_path
));
out.push_str(&format!(
" let wrapper = {wrapper_type_path}::{constructor_method}(\n"
));
for arg in &wrapper_call.args {
match arg {
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} => {
out.push_str(&format!(" {},\n", value_expr));
}
crate::core::ir::WrapperConstructorArg::Free { param } => {
out.push_str(&format!(" {},\n", param.name));
}
}
}
out.push_str(" );\n\n");
}
out.push_str(" // Register the handler with wrapper or direct metadata\n");
out.push_str(" for reg_entry in registration_list {\n");
out.push_str(
" if let Ok((_method, _metadata, handler_pid)) = reg_entry.decode::<(String, rustler::Term<'_>, rustler::LocalPid)>()\n",
);
out.push_str(" {\n");
let metadata_param_names: Vec<&str> = base_reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
if !metadata_param_names.is_empty() {
let trailing = if metadata_param_names.len() == 1 { "," } else { "" };
let tuple_types = base_reg
.metadata_params
.iter()
.map(|p| {
if let TypeRef::Named(n) = &p.ty {
if api.types.iter().any(|t| &t.name == n && !t.is_trait && t.is_opaque) {
return format!("rustler::ResourceArc<super::{}>", n);
}
}
typeref_to_rust_type(&p.ty, core_import)
})
.collect::<Vec<_>>()
.join(", ");
let tuple_types_with_trailing = format!("{}{}", tuple_types, trailing);
out.push_str(&format!(
" if let Ok(({names}{trailing})) = _metadata.decode::<({types})>()\n",
names = metadata_param_names.join(", "),
trailing = trailing,
types = tuple_types_with_trailing
));
out.push_str(" {\n");
for meta_param in base_reg.metadata_params.iter() {
let is_opaque = if let TypeRef::Named(n) = &meta_param.ty {
api.types.iter().any(|t| &t.name == n && !t.is_trait && t.is_opaque)
} else {
false
};
if is_opaque {
if let TypeRef::Named(n) = &meta_param.ty {
out.push_str(&format!(
" let {pname}: {core_import}::{name} = (*{pname}).inner.as_ref().clone();\n",
pname = meta_param.name,
core_import = core_import,
name = n,
));
}
}
}
out.push_str(&format!(
" let bridge = {bridge_wrapper}::new(handler_pid);\n"
));
out.push_str(&format!(
" let handler: Arc<dyn {core_import}::{contract_name}> = Arc::new(bridge);\n",
contract_name = base_reg.callback_contract
));
if let Some(wrapper_call) = &variant.wrapper_call {
let _metadata_param = &wrapper_call.metadata_param;
out.push_str(&format!(
" let _ = owner.{}({}, handler);\n",
base_method,
metadata_param_names.join(", ")
));
} else {
out.push_str(&format!(
" let _ = owner.{}({}, handler);\n",
base_method,
metadata_param_names.join(", ")
));
}
out.push_str(" }\n");
} else {
out.push_str(&format!(
" let bridge = {bridge_wrapper}::new(handler_pid);\n"
));
out.push_str(&format!(
" let _ = owner.{}(std::sync::Arc::new(bridge));\n",
base_method
));
}
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" Ok(atoms::ok())\n");
out.push_str("}\n\n");
}
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("elixir"),
&config.name,
"packages/elixir/native/{name}_nif/src/",
);
let service_rs = gen_service_rs(api, config);
let (_, module_prefix) = super::helpers::get_module_info(api, config);
let service_ex = gen_service_ex(api, &module_prefix);
let elixir_pkg = config.output_paths.get("elixir").map(PathBuf::from).unwrap_or_else(|| {
let app_name = config.elixir_app_name();
PathBuf::from(format!("packages/elixir/lib/{}", app_name))
});
Ok(vec![
GeneratedFile {
path: PathBuf::from(&output_dir).join("service.rs"),
content: service_rs,
generated_header: true,
},
GeneratedFile {
path: elixir_pkg.join("service.ex"),
content: service_ex,
generated_header: true,
},
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, PrimitiveType, 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(crate::core::ir::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(crate::core::ir::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 finalize_ep = EntrypointDef {
method: "into_router".to_owned(),
kind: EntrypointKind::Finalize,
is_async: false,
params: vec![],
return_type: TypeRef::Named("Router".to_owned()),
error_type: None,
doc: "Consume and convert into a router.".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, finalize_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(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()),
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 elixir_output_contains_service_module() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("defmodule TestService do"),
"expected `defmodule TestService do` in output:\n{output}"
);
}
#[test]
fn elixir_output_contains_struct_definition() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("defstruct"),
"expected `defstruct` in output:\n{output}"
);
assert!(
output.contains(":registrations"),
"expected `:registrations` field in output:\n{output}"
);
}
#[test]
fn elixir_output_contains_constructor() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(output.contains("def new("), "expected `def new(` in output:\n{output}");
}
#[test]
fn elixir_output_contains_configurator() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("def with_timeout("),
"expected `with_timeout` configurator:\n{output}"
);
}
#[test]
fn elixir_output_contains_registration() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("def add_handler("),
"expected `add_handler` registration method:\n{output}"
);
}
#[test]
fn elixir_output_contains_genserver_module() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("defmodule TestService.Handler do"),
"expected `TestService.Handler` GenServer:\n{output}"
);
assert!(
output.contains("use GenServer"),
"expected `use GenServer` in output:\n{output}"
);
}
#[test]
fn elixir_output_contains_run_entrypoint() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(output.contains("def run("), "expected `def run(` in output:\n{output}");
}
#[test]
fn rust_output_contains_handler_bridge_struct() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub struct ElixirRequestHandlerBridge"),
"expected `ElixirRequestHandlerBridge` struct:\n{output}"
);
}
#[test]
fn rust_output_contains_handler_bridge_impl() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("impl my_crate::RequestHandler for ElixirRequestHandlerBridge"),
"expected trait impl:\n{output}"
);
assert!(
output.contains("fn handle(") && output.contains("Pin<Box<dyn std::future::Future<Output"),
"expected boxed-future dispatch method:\n{output}"
);
}
#[test]
fn rust_output_contains_nif_run() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("#[rustler::nif(schedule = \"DirtyCpu\")]"),
"expected `#[rustler::nif(schedule = \"DirtyCpu\")]` attribute:\n{output}"
);
assert!(
output.contains("pub fn test_service_run("),
"expected `test_service_run` function:\n{output}"
);
}
#[test]
fn generate_returns_two_files_for_non_empty_services() {
let surface = make_fixture_surface();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert_eq!(files.len(), 2, "expected 2 generated files, got {}", files.len());
let paths: Vec<&str> = files
.iter()
.map(|f| f.path.file_name().unwrap().to_str().unwrap())
.collect();
assert!(paths.contains(&"service.rs"), "expected service.rs in output");
assert!(paths.contains(&"service.ex"), "expected service.ex in output");
}
#[test]
fn generate_returns_empty_for_no_services() {
let surface = ApiSurface::default();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
#[test]
fn elixir_genserver_handle_cast_decodes_args_and_dispatches() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("decode_args_and_dispatch(method, args_json, registrations)"),
"expected decode_args_and_dispatch call in handle_cast:\n{output}"
);
assert!(
output.contains("Native.complete_trait_call(reply_id, response)"),
"expected Native.complete_trait_call(reply_id, response) call:\n{output}"
);
assert!(
!output.contains("simplified stub"),
"found 'simplified stub' comment — dispatch should not be stubbed:\n{output}"
);
assert!(
!output.contains("placeholder"),
"found unsupported comment in dispatch logic:\n{output}"
);
assert!(
!output.contains("# This is a simplified stub"),
"found stub marker in dispatch:\n{output}"
);
}
#[test]
fn elixir_genserver_dispatch_helper_invokes_handler() {
let surface = make_fixture_surface();
let output = gen_service_ex(&surface, "");
assert!(
output.contains("defp decode_args_and_dispatch(method, args_json, registrations) do"),
"expected decode_args_and_dispatch helper function:\n{output}"
);
assert!(
output.contains("Jason.decode(args_json)"),
"expected Jason.decode(args_json) in dispatch:\n{output}"
);
assert!(
output.contains("response = handler.(args)"),
"expected handler.(args) invocation:\n{output}"
);
assert!(
output.contains("Jason.encode(response)"),
"expected Jason.encode(response) in dispatch:\n{output}"
);
assert!(
output.contains("defp find_handler"),
"expected find_handler helper function:\n{output}"
);
}
#[test]
fn rust_nif_parses_registrations_and_constructs_owner() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("let registration_list: Vec<rustler::Term<'_>> = registrations"),
"expected registration list parsing in NIF:\n{output}"
);
assert!(
output.contains("let mut owner = my_crate::TestService::new()"),
"expected owner construction in NIF:\n{output}"
);
assert!(
output.contains("for reg_entry in registration_list"),
"expected registration iteration in NIF:\n{output}"
);
assert!(
!output.contains("placeholder: parse registrations"),
"found placeholder in registration parsing — should be implemented:\n{output}"
);
assert!(
!output.contains("For now, return a stub"),
"found stub return in NIF — should be fully implemented:\n{output}"
);
}
#[test]
fn no_stub_responses_in_generated_code() {
let surface = make_fixture_surface();
let config = make_test_config();
let elixir_output = gen_service_ex(&surface, "");
let rust_output = gen_service_rs(&surface, &config);
assert!(
!elixir_output.contains("response = {:ok, %{}}"),
"found stub response {{:ok, %{{}}}} in Elixir generated code:\n{elixir_output}"
);
assert!(
!elixir_output.contains("# Native.complete_trait_call"),
"found commented-out complete_trait_call in Elixir:\n{elixir_output}"
);
assert!(
!rust_output.contains("would be called here"),
"found 'would be called here' stub comment in Rust NIF:\n{rust_output}"
);
assert!(
!rust_output.contains("would happen here"),
"found 'would happen here' stub comment in Rust NIF:\n{rust_output}"
);
assert!(
rust_output.contains("owner.run(") || rust_output.contains("owner.finalize("),
"Rust NIF should call owner.run(...) or owner.finalize(...), found neither:\n{rust_output}"
);
assert!(
rust_output.contains("ElixirRequestHandlerBridge"),
"Rust NIF should create handler bridge instances:\n{rust_output}"
);
assert!(
!rust_output.contains("): Result<"),
"found illegal if-let type ascription pattern '): Result<' in generated Rust:\n{rust_output}"
);
assert!(
rust_output.contains("Term<'_>"),
"expected lifetime-annotated Term<'_> in generated Rust NIF signature:\n{rust_output}"
);
}
#[test]
fn registration_variant_style_hybrid_emits_both_forms() {
let mut surface = make_fixture_surface();
let _config = make_test_config();
surface.services[0].registrations[0]
.variants
.push(crate::core::ir::RegistrationVariant {
name: "get".to_owned(),
overrides: vec![crate::core::ir::RegistrationVariantOverride {
param_name: "method".to_owned(),
value_expr: "\"GET\"".to_owned(),
}],
wrapper_call: None,
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: None,
style: RegistrationVariantStyle::Hybrid,
});
let elixir_output = gen_service_ex(&surface, "");
assert!(
elixir_output.contains("def get(app, path, handler) do"),
"expected verb-decorator form 'def get(app, path, handler) do' in Elixir output:\n{elixir_output}"
);
assert!(
elixir_output.contains("def get_decorator(app, path) do"),
"expected builder form 'def get_decorator(app, path) do' in Elixir output:\n{elixir_output}"
);
}
#[test]
fn handler_bridge_sends_trait_call_message() {
let surface = make_fixture_surface();
let config = make_test_config();
let rust_output = gen_service_rs(&surface, &config);
assert!(
rust_output.contains("OwnedEnv"),
"expected OwnedEnv import in generated code"
);
assert!(
rust_output.contains("env.send_and_clear(&pid"),
"expected env.send_and_clear(&pid, ...) call in generated handler bridge:\n{rust_output}"
);
assert!(
rust_output.contains("Atom::from_str(env, \"trait_call\")"),
"expected atom::from_str for 'trait_call' in generated message:\n{rust_output}"
);
assert!(
rust_output.contains("method_name"),
"expected method_name variable in trait_call message"
);
assert!(
rust_output.contains("request_json_clone"),
"expected request JSON to be sent in trait_call message"
);
assert!(
rust_output.contains("reply_id)"),
"expected reply_id in trait_call tuple"
);
assert!(
!rust_output.contains("// crate::nif_support::send_trait_call"),
"found old commented-out send_trait_call in output — should be replaced with real call"
);
assert!(
rust_output.contains("tokio::task::spawn_blocking(move || {"),
"expected spawn_blocking to wrap the message send"
);
}
#[test]
fn rust_codegen_emits_core_import_and_trait_impl() {
let surface = make_fixture_surface();
let config = make_test_config();
let rust_output = gen_service_rs(&surface, &config);
assert!(
rust_output.contains("use my_crate::*;"),
"expected core crate wildcard import in gen_service_rs output:\n{rust_output}"
);
assert!(
rust_output.contains("impl my_crate::RequestHandler for ElixirRequestHandlerBridge"),
"expected trait impl for bridge in generated output:\n{rust_output}"
);
assert!(
rust_output.contains("let handler: Arc<dyn my_crate::RequestHandler> = Arc::new(bridge);"),
"expected handler trait cast in registration code:\n{rust_output}"
);
assert!(
rust_output.contains("pub struct ElixirRequestHandlerBridge"),
"expected ElixirRequestHandlerBridge struct definition:\n{rust_output}"
);
}
fn make_test_config() -> ResolvedCrateConfig {
use crate::core::config::resolved::ResolvedCrateConfig;
ResolvedCrateConfig {
name: "my-crate".to_owned(),
..ResolvedCrateConfig::default()
}
}
}