use crate::codegen::generators;
use crate::codegen::shared::{binding_fields, function_params};
use crate::codegen::type_mapper::TypeMapper;
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, FieldDef, FunctionDef, ParamDef, ReceiverKind, TypeRef};
use ahash::AHashSet;
use crate::backends::magnus::type_map::MagnusMapper;
fn is_thread_unsafe_field(field: &FieldDef) -> bool {
matches!(&field.ty, TypeRef::Named(name) if name == "VisitorHandle")
|| matches!(field.ty, TypeRef::Optional(ref inner) if matches!(inner.as_ref(), TypeRef::Named(name) if name == "VisitorHandle"))
}
fn last_param_is_default_struct(func: &FunctionDef, api: &ApiSurface) -> bool {
func.params.last().is_some_and(|p| {
if let TypeRef::Named(name) = &p.ty {
api.types
.iter()
.find(|t| &t.name == name)
.is_some_and(|t| t.has_default)
} else {
false
}
})
}
pub(super) fn needs_variadic_arity(params: &[crate::core::ir::ParamDef]) -> bool {
params.iter().any(|p| p.optional) || {
let mut seen_optional = false;
params.iter().any(|p| {
if p.optional {
seen_optional = true;
false
} else {
seen_optional && !p.optional
}
})
}
}
fn param_scan_args_type(
p: &crate::core::ir::ParamDef,
promoted: bool,
mapper: &MagnusMapper,
opaque_types: &AHashSet<String>,
) -> String {
let inner = if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) {
"magnus::Value".to_string()
} else {
mapper.map_type(&p.ty)
}
} else {
mapper.map_type(&p.ty)
};
if p.optional || promoted {
format!("Option<{inner}>")
} else {
inner
}
}
fn param_scan_args_type_extended(
p: &crate::core::ir::ParamDef,
promoted: bool,
mapper: &MagnusMapper,
opaque_types: &AHashSet<String>,
treat_as_optional: bool,
) -> String {
let inner = if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) {
"magnus::Value".to_string()
} else {
mapper.map_type(&p.ty)
}
} else if matches!(p.ty, TypeRef::String) && (p.optional || promoted || treat_as_optional) {
"magnus::Value".to_string()
} else {
mapper.map_type(&p.ty)
};
if p.optional || promoted || treat_as_optional {
format!("Option<{inner}>")
} else {
inner
}
}
fn gen_scan_args_prologue_with_defaults(
params: &[crate::core::ir::ParamDef],
mapper: &MagnusMapper,
opaque_types: &AHashSet<String>,
last_is_default_config: bool,
) -> String {
let mut seen_optional = false;
let mut req_types: Vec<String> = Vec::new();
let mut opt_types: Vec<String> = Vec::new();
let mut req_names: Vec<String> = Vec::new();
let mut opt_names: Vec<String> = Vec::new();
for (idx, p) in params.iter().enumerate() {
let promoted = crate::codegen::shared::is_promoted_optional(params, idx);
let is_last = idx == params.len() - 1;
let treat_as_optional = (p.optional || promoted) || (is_last && last_is_default_config);
if treat_as_optional {
seen_optional = true;
opt_types.push(param_scan_args_type_extended(
p,
promoted,
mapper,
opaque_types,
is_last && last_is_default_config,
));
opt_names.push(p.name.clone());
} else {
let _ = seen_optional;
req_types.push(param_scan_args_type(p, false, mapper, opaque_types));
req_names.push(p.name.clone());
}
}
let req_type_str = req_types.join(", ");
let opt_type_str = opt_types.join(", ");
let _type_params = match (req_types.is_empty(), opt_types.is_empty()) {
(true, true) => "()".to_string(),
(false, true) => format!("({req_type_str},)"),
(true, false) => format!("((), ({opt_type_str},))"),
(false, false) => format!("(({req_type_str},), ({opt_type_str},))"),
};
let scan_args_line = crate::backends::magnus::template_env::render(
"function_scan_args_call.rs.jinja",
minijinja::context! {
has_required => !req_types.is_empty(),
has_optional => !opt_types.is_empty(),
required_types => &req_type_str,
optional_types => &opt_type_str,
},
);
let mut lines = vec![scan_args_line];
if !req_names.is_empty() {
let pat = if req_names.len() == 1 {
format!("({},)", req_names[0])
} else {
format!(
"({})",
req_names.iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", ")
)
};
lines.push(crate::backends::magnus::template_env::render(
"function_scan_args_destructure.rs.jinja",
minijinja::context! {
pattern => &pat,
source => "required",
},
));
}
if !opt_names.is_empty() {
let pat = if opt_names.len() == 1 {
format!("({},)", opt_names[0])
} else {
format!(
"({})",
opt_names.iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", ")
)
};
lines.push(crate::backends::magnus::template_env::render(
"function_scan_args_destructure.rs.jinja",
minijinja::context! {
pattern => &pat,
source => "optional",
},
));
}
for (idx, p) in params.iter().enumerate() {
let promoted = crate::codegen::shared::is_promoted_optional(params, idx);
let is_last = idx == params.len() - 1;
let treat_as_optional = (p.optional || promoted) || (is_last && last_is_default_config);
if treat_as_optional && matches!(p.ty, TypeRef::String) {
lines.push(crate::backends::magnus::template_env::render(
"function_optional_string_scan_arg.rs.jinja",
minijinja::context! {
name => &p.name,
},
));
}
}
lines.join("\n ")
}
fn magnus_ahash_pre_call_bindings(params: &[ParamDef]) -> Vec<String> {
let mut bindings = Vec::new();
for p in params {
if let TypeRef::Map(_, _) = &p.ty {
if p.map_is_ahash && p.map_key_is_cow {
let bound_name = format!("__{}_ahash", p.name);
bindings.push(format!(
" let {bound_name} = {}.map(|m| m.into_iter().map(|(k, v)| (std::borrow::Cow::Owned(k), serde_json::Value::String(v))).collect::<ahash::AHashMap<std::borrow::Cow<'static, str>, serde_json::Value>>()); ",
p.name
));
}
}
}
bindings
}
fn magnus_call_args_with_ahash(params: &[ParamDef], _opaque_types: &AHashSet<String>, base_call_args: &str) -> String {
if !params
.iter()
.any(|p| matches!(&p.ty, TypeRef::Map(_, _)) && p.map_is_ahash && p.map_key_is_cow)
{
return base_call_args.to_string();
}
let terms: Vec<&str> = base_call_args.split(", ").collect();
let result: Vec<String> = terms
.into_iter()
.zip(params.iter())
.map(|(term, p)| {
if let TypeRef::Map(_, _) = &p.ty {
if p.map_is_ahash && p.map_key_is_cow {
let bound_name = format!("__{}_ahash", p.name);
return if p.optional && p.is_ref {
format!("{bound_name}.as_ref()")
} else if p.is_ref {
format!("{bound_name}.as_ref().unwrap()")
} else {
bound_name
};
}
}
term.to_string()
})
.collect();
result.join(", ")
}
pub(super) fn gen_function(
func: &FunctionDef,
mapper: &MagnusMapper,
opaque_types: &AHashSet<String>,
mutex_types: &AHashSet<String>,
core_import: &str,
api: &ApiSurface,
) -> String {
let is_default_config_func = last_param_is_default_struct(func, api);
let variadic = needs_variadic_arity(&func.params) || is_default_config_func;
let params = if variadic {
"args: &[magnus::Value]".to_string()
} else {
function_params(&func.params, &|ty| {
if let TypeRef::Named(name) = ty {
if !opaque_types.contains(name.as_str()) {
return "magnus::Value".to_string();
}
}
mapper.map_type(ty)
})
};
let return_type = mapper.map_type(&func.return_type);
let has_error = func.error_type.is_some() || func.is_async || variadic;
let return_annotation = mapper.wrap_return(&return_type, has_error);
let can_delegate = crate::codegen::shared::can_auto_delegate_function(func, opaque_types);
let serde_recoverable = !can_delegate && magnus_serde_recoverable(func, opaque_types);
let mut deser_lines = Vec::new();
if serde_recoverable {
deser_lines.extend(magnus_serde_let_bindings(
&func.params,
opaque_types,
core_import,
mapper,
is_default_config_func,
));
} else {
for (idx, p) in func.params.iter().enumerate() {
let promoted = crate::codegen::shared::is_promoted_optional(&func.params, idx);
if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) {
let binding_ty = &p.name;
if p.optional {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "optional",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
} else if promoted || (idx == func.params.len() - 1 && is_default_config_func) {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "default",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
} else {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "required",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
}
}
} else if let TypeRef::Vec(inner) = &p.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) {
let core_inner_ty = format!("{core_import}::{name}");
let vec_ty = format!("Vec<{core_inner_ty}>");
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
vec_ty => &vec_ty,
optional => p.optional,
},
));
}
}
}
}
}
let ahash_bindings = magnus_ahash_pre_call_bindings(&func.params);
deser_lines.extend(ahash_bindings);
let scan_args_prologue = if variadic {
format!(
"{}\n ",
gen_scan_args_prologue_with_defaults(&func.params, mapper, opaque_types, is_default_config_func)
)
} else {
String::new()
};
let deser_preamble = if deser_lines.is_empty() {
String::new()
} else {
format!("{}\n ", deser_lines.join("\n "))
};
let needs_vec_named_let_binding = func.params.iter().any(|p| match &p.ty {
TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Named(name) if !opaque_types.contains(name.as_str())),
_ => false,
});
let body = if can_delegate || serde_recoverable {
let base_call_args = if serde_recoverable || needs_vec_named_let_binding {
generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
} else {
generators::gen_call_args(&func.params, opaque_types)
};
let call_args = magnus_call_args_with_ahash(&func.params, opaque_types, &base_call_args);
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let core_call = format!("{core_fn_path}({call_args})");
if func.is_async {
let wrap = generators::wrap_return_with_mutex_mapped(
"result",
&func.return_type,
"",
opaque_types,
mutex_types,
false,
func.returns_ref,
false,
mapper,
);
if func.error_type.is_some() {
crate::backends::magnus::template_env::render(
"function_async_body.rs.jinja",
minijinja::context! {
core_call => &core_call,
wrap => &wrap,
has_error => true,
},
)
} else {
crate::backends::magnus::template_env::render(
"function_async_body.rs.jinja",
minijinja::context! {
core_call => &core_call,
wrap => &wrap,
has_error => false,
},
)
}
} else if func.error_type.is_some() {
let wrap = generators::wrap_return_with_mutex_mapped(
"result",
&func.return_type,
"",
opaque_types,
mutex_types,
false,
func.returns_ref,
false,
mapper,
);
crate::backends::magnus::template_env::render(
"function_result_body.rs.jinja",
minijinja::context! {
core_call => &core_call,
wrap => &wrap,
},
)
} else if variadic {
let inner = generators::wrap_return_with_mutex_mapped(
&core_call,
&func.return_type,
"",
opaque_types,
mutex_types,
false,
func.returns_ref,
false,
mapper,
);
crate::backends::magnus::template_env::render(
"function_variadic_ok_body.rs.jinja",
minijinja::context! {
inner => &inner,
},
)
} else {
generators::wrap_return_with_mutex_mapped(
&core_call,
&func.return_type,
"",
opaque_types,
mutex_types,
false,
func.returns_ref,
false,
mapper,
)
}
} else {
gen_magnus_unimplemented_body(&func.return_type, &func.name, func.error_type.is_some() || variadic)
};
let allow_attr = if !can_delegate && !serde_recoverable {
"#[allow(unused_variables)]\n"
} else {
""
};
crate::backends::magnus::template_env::render(
"function_wrapper.rs.jinja",
minijinja::context! {
allow_attr => allow_attr,
name => &func.name,
params => ¶ms,
return_annotation => &return_annotation,
scan_args_prologue => &scan_args_prologue,
deser_preamble => &deser_preamble,
body => &body,
},
)
}
fn magnus_serde_recoverable(func: &FunctionDef, opaque_types: &AHashSet<String>) -> bool {
if func.error_type.is_none() && !func.is_async {
return false;
}
if !crate::codegen::shared::is_delegatable_return(&func.return_type) {
return false;
}
func.params.iter().all(|p| {
if p.sanitized {
return p.original_type.is_some()
&& matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String));
}
match &p.ty {
TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => true,
_ => crate::codegen::shared::is_delegatable_param(&p.ty, opaque_types),
}
})
}
fn magnus_serde_let_bindings(
params: &[crate::core::ir::ParamDef],
opaque_types: &AHashSet<String>,
core_import: &str,
_mapper: &MagnusMapper,
is_default_config_func: bool,
) -> Vec<String> {
let err = "magnus::Error::new(unsafe { Ruby::get_unchecked() }.exception_runtime_error(), e.to_string())";
let mut out = Vec::new();
for (idx, p) in params.iter().enumerate() {
let promoted = crate::codegen::shared::is_promoted_optional(params, idx);
let is_last = idx == params.len() - 1;
let is_last_config = is_last && is_default_config_func;
match &p.ty {
TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
if p.optional {
out.push(crate::backends::magnus::template_env::render(
"function_serde_named_binding.rs.jinja",
minijinja::context! {
mode => "optional",
name => &p.name,
core_import => core_import,
type_name => name,
error_expr => err,
},
));
} else if promoted || is_last_config {
out.push(crate::backends::magnus::template_env::render(
"function_serde_named_binding.rs.jinja",
minijinja::context! {
mode => "default",
name => &p.name,
core_import => core_import,
type_name => name,
error_expr => err,
},
));
} else {
out.push(crate::backends::magnus::template_env::render(
"function_serde_named_binding.rs.jinja",
minijinja::context! {
mode => "required",
name => &p.name,
core_import => core_import,
type_name => name,
error_expr => err,
},
));
}
}
TypeRef::Vec(inner)
if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref && !p.sanitized =>
{
if p.optional {
out.push(crate::backends::magnus::template_env::render(
"function_vec_refs_binding.rs.jinja",
minijinja::context! {
name => &p.name,
optional => true,
},
));
} else {
out.push(crate::backends::magnus::template_env::render(
"function_vec_refs_binding.rs.jinja",
minijinja::context! {
name => &p.name,
optional => false,
},
));
}
}
TypeRef::Vec(inner)
if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() =>
{
if p.optional {
out.push(crate::backends::magnus::template_env::render(
"function_sanitized_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
optional => true,
error_expr => err,
},
));
} else {
out.push(crate::backends::magnus::template_env::render(
"function_sanitized_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
optional => false,
error_expr => err,
},
));
}
}
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
if let TypeRef::Named(name) = inner.as_ref() {
let core_inner_ty = format!("{core_import}::{name}");
let vec_ty = format!("Vec<{core_inner_ty}>");
if p.optional {
out.push(crate::backends::magnus::template_env::render(
"function_named_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
vec_ty => &vec_ty,
optional => true,
},
));
} else {
out.push(crate::backends::magnus::template_env::render(
"function_named_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
vec_ty => &vec_ty,
optional => false,
},
));
}
}
}
_ => {}
}
}
out
}
pub(super) fn gen_async_function(
func: &FunctionDef,
mapper: &MagnusMapper,
opaque_types: &AHashSet<String>,
mutex_types: &AHashSet<String>,
core_import: &str,
api: &ApiSurface,
) -> String {
let is_default_config_func = last_param_is_default_struct(func, api);
let variadic = needs_variadic_arity(&func.params) || is_default_config_func;
let params = if variadic {
"args: &[magnus::Value]".to_string()
} else {
function_params(&func.params, &|ty| {
if let TypeRef::Named(name) = ty {
if !opaque_types.contains(name.as_str()) {
return "magnus::Value".to_string();
}
}
mapper.map_type(ty)
})
};
let return_type = mapper.map_type(&func.return_type);
let return_annotation = mapper.wrap_return(&return_type, true);
let can_delegate = crate::codegen::shared::can_auto_delegate_function(func, opaque_types);
let serde_recoverable = !can_delegate && magnus_serde_recoverable(func, opaque_types);
let mut deser_lines = Vec::new();
if serde_recoverable {
deser_lines.extend(magnus_serde_let_bindings(
&func.params,
opaque_types,
core_import,
mapper,
is_default_config_func,
));
} else {
for (idx, p) in func.params.iter().enumerate() {
let promoted = crate::codegen::shared::is_promoted_optional(&func.params, idx);
if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str()) {
let binding_ty = &p.name;
if p.optional {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "optional",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
} else if promoted || (idx == func.params.len() - 1 && is_default_config_func) {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "default",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
} else {
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_binding.rs.jinja",
minijinja::context! {
mode => "required",
binding_name => binding_ty,
core_import => core_import,
type_name => name,
},
));
}
}
} else if let TypeRef::Vec(inner) = &p.ty {
if let TypeRef::Named(name) = inner.as_ref() {
if !opaque_types.contains(name.as_str()) {
let core_inner_ty = format!("{core_import}::{name}");
let vec_ty = format!("Vec<{core_inner_ty}>");
deser_lines.push(crate::backends::magnus::template_env::render(
"function_named_vec_binding.rs.jinja",
minijinja::context! {
name => &p.name,
vec_ty => &vec_ty,
optional => p.optional,
},
));
}
}
}
}
}
let ahash_bindings = magnus_ahash_pre_call_bindings(&func.params);
deser_lines.extend(ahash_bindings);
let scan_args_prologue = if variadic {
format!(
"{}\n ",
gen_scan_args_prologue_with_defaults(&func.params, mapper, opaque_types, is_default_config_func)
)
} else {
String::new()
};
let deser_preamble = if deser_lines.is_empty() {
String::new()
} else {
format!("{}\n ", deser_lines.join("\n "))
};
let needs_vec_named_let_binding = func.params.iter().any(|p| match &p.ty {
TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Named(name) if !opaque_types.contains(name.as_str())),
_ => false,
});
let body = if can_delegate || serde_recoverable {
let base_call_args = if serde_recoverable || needs_vec_named_let_binding {
generators::gen_call_args_with_let_bindings(&func.params, opaque_types)
} else {
generators::gen_call_args(&func.params, opaque_types)
};
let call_args = magnus_call_args_with_ahash(&func.params, opaque_types, &base_call_args);
let core_fn_path = {
let path = func.rust_path.replace('-', "_");
if path.starts_with(core_import) {
path
} else {
format!("{core_import}::{}", func.name)
}
};
let core_call = format!("{core_fn_path}({call_args})");
let result_wrap = generators::wrap_return_with_mutex_mapped(
"result",
&func.return_type,
"",
opaque_types,
mutex_types,
false,
func.returns_ref,
false,
mapper,
);
if func.error_type.is_some() {
crate::backends::magnus::template_env::render(
"function_async_body.rs.jinja",
minijinja::context! {
core_call => &core_call,
wrap => &result_wrap,
has_error => true,
},
)
} else {
crate::backends::magnus::template_env::render(
"function_async_body.rs.jinja",
minijinja::context! {
core_call => &core_call,
wrap => &result_wrap,
has_error => false,
},
)
}
} else {
gen_magnus_unimplemented_body(
&func.return_type,
&format!("{}_async", func.name),
func.error_type.is_some(),
)
};
let allow_attr = if !can_delegate && !serde_recoverable {
"#[allow(unused_variables)]\n"
} else {
""
};
let name = format!("{}_async", func.name);
crate::backends::magnus::template_env::render(
"function_wrapper.rs.jinja",
minijinja::context! {
allow_attr => allow_attr,
name => &name,
params => ¶ms,
return_annotation => &return_annotation,
scan_args_prologue => &scan_args_prologue,
deser_preamble => &deser_preamble,
body => &body,
},
)
}
pub(super) fn gen_magnus_unimplemented_body(
return_type: &crate::core::ir::TypeRef,
fn_name: &str,
has_error: bool,
) -> String {
use crate::core::ir::TypeRef;
let err_msg = format!("Not implemented: {fn_name}");
if has_error {
crate::backends::magnus::template_env::render(
"function_unimplemented_error.rs.jinja",
minijinja::context! {
message => &err_msg,
},
)
} else {
match return_type {
TypeRef::Unit => "()".to_string(),
TypeRef::String | TypeRef::Char | TypeRef::Path => crate::backends::magnus::template_env::render(
"function_unimplemented_string.rs.jinja",
minijinja::context! {
name => fn_name,
},
),
TypeRef::Bytes => "Vec::new()".to_string(),
TypeRef::Primitive(p) => match p {
crate::core::ir::PrimitiveType::Bool => "false".to_string(),
_ => "0".to_string(),
},
TypeRef::Optional(_) => "None".to_string(),
TypeRef::Vec(_) => "Vec::new()".to_string(),
TypeRef::Map(_, _) => "Default::default()".to_string(),
TypeRef::Duration => "0u64".to_string(),
TypeRef::Named(_) | TypeRef::Json => crate::backends::magnus::template_env::render(
"function_unimplemented_panic.rs.jinja",
minijinja::context! {
name => fn_name,
},
),
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn gen_module_init(
module_name: &str,
api: &ApiSurface,
config: &ResolvedCrateConfig,
exclude_functions: &std::collections::HashSet<&str>,
exclude_types: &std::collections::HashSet<&str>,
streaming_methods_by_owner: &std::collections::HashMap<String, Vec<String>>,
streaming_iterator_registrations: &[String],
streaming_method_registrations: &std::collections::HashMap<String, Vec<String>>,
streaming_adapters: &[super::streaming::StreamingAdapter<'_>],
) -> String {
let mut lines = vec![
"#[magnus::init]".to_string(),
"fn ruby_init(ruby: &Ruby) -> Result<(), Error> {".to_string(),
crate::backends::magnus::template_env::render(
"module_define.rs.jinja",
minijinja::context! {
module_name => module_name,
},
),
"".to_string(),
" // Ensure JSON library is loaded for Hash#to_json".to_string(),
" let _ = ruby.eval::<magnus::Value>(\"require \\\"json\\\"\");".to_string(),
"".to_string(),
];
if let Some(reg) = config.custom_registrations.for_language(Language::Ruby) {
for class in ®.classes {
lines.push(crate::backends::magnus::template_env::render(
"module_class_define.rs.jinja",
minijinja::context! {
binding => "_class",
class_name => class,
},
));
}
for func in ®.functions {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => func,
function_name => func,
arity => 0,
},
));
}
lines.push("".to_string());
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if exclude_types.contains(typ.name.as_str()) {
continue;
}
let class_used = (!typ.is_opaque && !typ.fields.is_empty()) || typ.methods.iter().any(|m| !m.is_static);
let binding = if class_used { "class" } else { "_class" };
lines.push(crate::backends::magnus::template_env::render(
"module_class_define.rs.jinja",
minijinja::context! {
binding => binding,
class_name => &typ.name,
},
));
if !typ.is_opaque && !typ.fields.is_empty() {
lines.push(crate::backends::magnus::template_env::render(
"module_class_singleton_method_register.rs.jinja",
minijinja::context! {
ruby_name => "new",
type_name => &typ.name,
function_name => "new",
arity => -1,
},
));
}
if !typ.is_opaque {
for field in binding_fields(&typ.fields) {
if is_thread_unsafe_field(field) {
continue;
}
lines.push(crate::backends::magnus::template_env::render(
"module_class_method_register.rs.jinja",
minijinja::context! {
ruby_name => &field.name,
type_name => &typ.name,
function_name => &field.name,
arity => 0,
},
));
}
if super::classes::has_content_string_field(typ) {
lines.push(crate::backends::magnus::template_env::render(
"module_class_method_register.rs.jinja",
minijinja::context! {
ruby_name => "to_s",
type_name => &typ.name,
function_name => "to_s",
arity => 0,
},
));
}
}
let streaming_owner_methods = streaming_methods_by_owner
.get(typ.name.as_str())
.map(|v| v.as_slice())
.unwrap_or(&[]);
for method in &typ.methods {
if !method.is_static {
if method.name == "apply_update" {
continue;
}
if matches!(method.receiver, Some(ReceiverKind::RefMut)) {
continue;
}
if streaming_owner_methods.contains(&method.name) {
continue;
}
let method_name = if method.is_async {
format!("{}_async", method.name)
} else {
method.name.clone()
};
let param_count = method.params.len();
lines.push(crate::backends::magnus::template_env::render(
"module_class_method_register.rs.jinja",
minijinja::context! {
ruby_name => &method_name,
type_name => &typ.name,
function_name => &method_name,
arity => param_count,
},
));
}
}
if let Some(regs) = streaming_method_registrations.get(typ.name.as_str()) {
for reg in regs {
lines.push(reg.clone());
}
}
lines.push("".to_string());
}
if !streaming_iterator_registrations.is_empty() {
lines.extend(streaming_iterator_registrations.iter().cloned());
lines.push("".to_string());
}
for func in &api.functions {
if super::is_reserved_fn(&func.name) || exclude_functions.contains(func.name.as_str()) {
continue;
}
if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
continue;
}
let has_bridge_param =
crate::backends::magnus::trait_bridge::find_bridge_param(func, &config.trait_bridges).is_some();
let has_options_field_binding =
crate::backends::magnus::trait_bridge::find_options_field_binding(func, &config.trait_bridges).is_some();
let is_default_config_func = last_param_is_default_struct(func, api);
let param_count: i32 = if has_options_field_binding {
-1
} else if has_bridge_param {
func.params.len() as i32
} else if needs_variadic_arity(&func.params) || is_default_config_func {
-1
} else {
func.params.len() as i32
};
if func.is_async {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => &func.name,
function_name => &func.name,
arity => param_count,
},
));
let async_name = format!("{}_async", func.name);
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => &async_name,
function_name => &async_name,
arity => param_count,
},
));
} else {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => &func.name,
function_name => &func.name,
arity => param_count,
},
));
}
}
for bridge_cfg in &config.trait_bridges {
if bridge_cfg.exclude_languages.iter().any(|s| s == "ruby") {
continue;
}
if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => register_fn,
function_name => register_fn,
arity => 2,
},
));
}
if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => unregister_fn,
function_name => unregister_fn,
arity => 1,
},
));
}
if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => clear_fn,
function_name => clear_fn,
arity => 0,
},
));
}
}
for adapter in streaming_adapters {
lines.push(crate::backends::magnus::template_env::render(
"module_function_register.rs.jinja",
minijinja::context! {
ruby_name => adapter.name,
function_name => adapter.name,
arity => 2,
},
));
}
for error in &api.errors {
let regs = crate::codegen::error_gen::magnus_error_methods_registrations(error);
for reg_line in regs {
lines.push(reg_line);
}
}
lines.push("".to_string());
lines.push(" Ok(())".to_string());
lines.push("}".to_string());
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::new_config::NewAlefConfig;
use crate::core::ir::{FunctionDef, ParamDef, PrimitiveType, TypeRef};
fn resolved_one(toml: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
cfg.resolve().unwrap().remove(0)
}
fn make_config() -> ResolvedCrateConfig {
resolved_one(
r#"
[workspace]
languages = ["ruby"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.ruby]
gem_name = "test_lib"
"#,
)
}
fn simple_func(name: &str, error: bool) -> FunctionDef {
FunctionDef {
name: name.to_string(),
rust_path: format!("test_lib::{name}"),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "input".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::String,
is_async: false,
error_type: if error { Some("Error".to_string()) } else { None },
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
#[test]
fn gen_function_emits_fn_name() {
let func = simple_func("process", false);
let mapper = crate::backends::magnus::type_map::MagnusMapper;
let api = crate::core::ir::ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let code = gen_function(
&func,
&mapper,
&Default::default(),
&Default::default(),
"test_lib",
&api,
);
assert!(code.contains("fn process("), "must emit function name");
assert!(code.contains("input: String"), "must include typed param");
}
#[test]
fn gen_function_with_error_wraps_result() {
let func = simple_func("process", true);
let mapper = crate::backends::magnus::type_map::MagnusMapper;
let api = crate::core::ir::ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let code = gen_function(
&func,
&mapper,
&Default::default(),
&Default::default(),
"test_lib",
&api,
);
assert!(code.contains("Result<"), "error function must return Result");
}
#[test]
fn gen_module_init_emits_magnus_init_attr() {
let config = make_config();
let api = crate::core::ir::ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
};
let code = gen_module_init(
"TestLib",
&api,
&config,
&Default::default(),
&Default::default(),
&Default::default(),
&[],
&Default::default(),
&[],
);
assert!(code.contains("#[magnus::init]"), "must emit #[magnus::init]");
assert!(code.contains("fn ruby_init(ruby: &Ruby)"), "must emit init fn");
assert!(code.contains("define_module(\"TestLib\")"), "must define the module");
}
#[test]
fn needs_variadic_arity_detects_optional_params() {
let required = ParamDef {
name: "x".to_string(),
ty: TypeRef::Primitive(PrimitiveType::U32),
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
};
let optional = ParamDef {
optional: true,
..required.clone()
};
assert!(
!needs_variadic_arity(std::slice::from_ref(&required)),
"required-only: no variadic"
);
assert!(needs_variadic_arity(&[optional]), "optional param: needs variadic");
}
}