use crate::backends::extendr::template_env;
use crate::codegen::generators::RustBindingConfig;
use crate::codegen::generators::trait_bridge::BridgeFieldMatch;
use crate::codegen::naming::wire_variant_value;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{ApiSurface, EnumDef, FunctionDef, TypeRef};
use ahash::AHashSet;
fn enum_core_path(enum_def: &EnumDef, core_import: &str) -> String {
if enum_def.rust_path.is_empty() {
format!("{core_import}::{}", enum_def.name)
} else {
enum_def.rust_path.replace('-', "_")
}
}
pub(super) fn collect_named_types_into(ty: &TypeRef, out: &mut AHashSet<String>) {
match ty {
TypeRef::Named(name) => {
out.insert(name.clone());
}
TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types_into(inner, out),
TypeRef::Map(k, v) => {
collect_named_types_into(k, out);
collect_named_types_into(v, out);
}
_ => {}
}
}
pub(super) fn is_flat_data_enum(e: &EnumDef) -> bool {
let has_data = e.variants.iter().any(|v| !v.fields.is_empty());
has_data
&& e.variants
.iter()
.filter(|v| !v.fields.is_empty())
.all(|v| v.fields.len() == 1)
}
pub(super) fn can_flat_data_enum_round_trip(e: &EnumDef) -> bool {
e.variants.iter().all(|v| {
if v.fields.is_empty() {
return true;
}
if v.is_tuple && v.fields.len() == 1 {
let ty = &v.fields[0].ty;
matches!(ty, TypeRef::String)
|| matches!(ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String))
} else {
false
}
})
}
pub(super) fn is_json_passthrough_data_enum(e: &EnumDef) -> bool {
if is_flat_data_enum(e) {
return false;
}
if e.serde_tag.is_none() {
return false;
}
e.variants.iter().any(|v| !v.fields.is_empty())
}
pub(super) fn gen_extendr_json_passthrough_enum_struct(
enum_def: &EnumDef,
mapper: &dyn TypeMapper,
core_import: &str,
) -> String {
let name = &enum_def.name;
let core_path = enum_core_path(enum_def, core_import);
let variant_constructors = gen_extendr_enum_variant_constructors(enum_def, mapper, &core_path);
let variant_constructors_block = if variant_constructors.is_empty() {
String::new()
} else {
format!("\n{}", variant_constructors.join("\n"))
};
format!(
r#"#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "{core_path}", into = "{core_path}")]
pub struct {name} {{
/// Serde-JSON encoding of the underlying core enum value. Preserves the
/// tagged-variant payload across the FFI boundary so round trips don't drop
/// inner field data. The field is private-by-convention (double-underscore
/// prefix) and not surfaced in R; construction goes through `from_json`.
#[serde(skip)]
pub __inner: String,
}}
impl From<{core_path}> for {name} {{
fn from(value: {core_path}) -> Self {{
Self {{
__inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
}}
}}
}}
impl From<{name}> for {core_path} {{
fn from(value: {name}) -> Self {{
if value.__inner.is_empty() {{
return <{core_path}>::default();
}}
serde_json::from_str(&value.__inner).unwrap_or_default()
}}
}}
#[extendr]
impl {name} {{
#[allow(clippy::should_implement_trait)]
pub fn default() -> {name} {{
<{core_path}>::default().into()
}}
pub fn from_json(json: String) -> extendr_api::Result<{name}> {{
let core: {core_path} =
serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
Ok(core.into())
}}{variant_constructors_block}
}}
"#
)
}
fn extendr_factory_param_is_constructible(ty: &TypeRef) -> bool {
match ty {
TypeRef::Named(_) | TypeRef::Map(_, _) => false,
TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::String | TypeRef::Char),
TypeRef::Optional(inner) => extendr_factory_param_is_constructible(inner),
_ => true,
}
}
pub(super) fn gen_extendr_enum_variant_constructors(
enum_def: &EnumDef,
mapper: &dyn TypeMapper,
core_path: &str,
) -> Vec<String> {
use crate::codegen::generators::{collect_variant_constructors, variant_field_init};
use crate::codegen::shared::{function_params, is_promoted_optional};
let name = &enum_def.name;
let map_fn = |ty: &TypeRef| mapper.map_type(ty);
collect_variant_constructors(enum_def)
.iter()
.filter(|ctor| {
ctor.params
.iter()
.all(|p| extendr_factory_param_is_constructible(&p.ty))
})
.map(|ctor| {
let params_str = function_params(&ctor.params, &map_fn);
let field_inits: Vec<String> = ctor
.params
.iter()
.enumerate()
.map(|(idx, p)| {
let expr =
variant_field_init(p, is_promoted_optional(&ctor.params, idx), true, true, ctor.boxed[idx]);
if expr == p.name {
p.name.clone()
} else {
format!("{}: {expr}", p.name)
}
})
.collect();
template_env::render(
"enum_variant_constructor.rs.jinja",
minijinja::context! {
snake => &ctor.snake_name,
params_str => ¶ms_str,
name => name,
let_lines => Vec::<String>::new(),
core_path => core_path,
variant => &ctor.variant_name,
field_inits => field_inits.join(", "),
},
)
.trim_end()
.to_string()
})
.collect()
}
pub(super) fn extendr_enum_variant_constructor_registrations(enum_def: &EnumDef) -> Vec<(String, String, Vec<String>)> {
crate::codegen::generators::collect_variant_constructors(enum_def)
.into_iter()
.filter(|ctor| {
ctor.params
.iter()
.all(|p| extendr_factory_param_is_constructible(&p.ty))
})
.map(|ctor| {
let param_names: Vec<String> = ctor.params.iter().map(|p| p.name.clone()).collect();
(
ctor.snake_name.clone(),
format!("_factory_{}", ctor.snake_name),
param_names,
)
})
.collect()
}
pub(super) fn gen_extendr_bridge_field_function(
api: &ApiSurface,
func: &FunctionDef,
bridge_match: &BridgeFieldMatch<'_>,
core_import: &str,
) -> String {
let func_name = &func.name;
let options_param = &bridge_match.param_name;
let field_name = &bridge_match.field_name;
let handle_path =
crate::codegen::generators::trait_bridge::bridge_handle_path(api, bridge_match.bridge, core_import);
let struct_name = crate::codegen::generators::trait_bridge::bridge_wrapper_name("R", bridge_match.bridge);
let mut param_parts = Vec::new();
for param in &func.params {
if param.name == *options_param {
param_parts.push(format!("{}: Robj", param.name));
} else {
match ¶m.ty {
TypeRef::String => param_parts.push(format!("{}: String", param.name)),
_ => param_parts.push(format!("{}: Robj", param.name)),
}
}
}
let params_str = param_parts.join(", ");
let return_type = "Result<Robj>";
let mut call_args = Vec::new();
for param in &func.params {
if param.name == *options_param {
call_args.push("Some(opts)".to_string());
} else {
call_args.push(format!("&{}", param.name));
}
}
template_env::render(
"bridge_field_function.jinja",
minijinja::context! {
func_name => func_name,
params_str => params_str,
return_type => return_type,
field_name => field_name,
options_param => options_param,
handle_path => handle_path,
struct_name => struct_name,
core_import => core_import,
options_type => &bridge_match.options_type,
call_args_str => call_args.join(", "),
},
)
}
pub(super) fn gen_extendr_flat_data_enum_struct(
enum_def: &EnumDef,
mapper: &dyn TypeMapper,
cfg: &RustBindingConfig,
) -> String {
let name = &enum_def.name;
let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
let mut out = String::with_capacity(1024);
let mut derives: Vec<&str> = cfg.struct_derives.to_vec();
derives.push("Default");
derives.push("serde::Serialize");
derives.push("serde::Deserialize");
out.push_str(&template_env::render(
"flat_enum_derive.jinja",
minijinja::context! {
derives => derives.join(", "),
},
));
out.push_str(&template_env::render(
"flat_enum_struct_header.jinja",
minijinja::context! {
name => name,
},
));
let disc_ident = crate::core::keywords::rust_raw_ident(discriminator);
let serde_rename_disc: Option<&str> = if disc_ident != discriminator {
Some(discriminator)
} else {
None
};
out.push_str(&template_env::render(
"flat_enum_discriminator_field.jinja",
minijinja::context! {
disc_ident => &disc_ident,
serde_rename => serde_rename_disc,
},
));
for variant in &enum_def.variants {
if !variant.fields.is_empty() && variant.is_tuple {
if let Some(first_field) = variant.fields.first() {
let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
let inner_ty = mapper.map_type(&first_field.ty);
out.push_str(&template_env::render(
"flat_enum_variant_field.jinja",
minijinja::context! {
field_name => &field_name,
inner_ty => &inner_ty,
},
));
}
}
}
out.push_str(&template_env::render(
"flat_enum_struct_footer.jinja",
minijinja::context! {},
));
out
}
pub(super) fn gen_extendr_flat_data_enum_from_core(enum_def: &EnumDef, core_import: &str) -> String {
let name = &enum_def.name;
let core_path = enum_core_path(enum_def, core_import);
let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
let disc_ident = crate::core::keywords::rust_raw_ident(discriminator);
let mut out = String::with_capacity(512);
out.push_str(&template_env::render(
"flat_enum_from_core_impl.jinja",
minijinja::context! {
core_path => &core_path,
name => name,
},
));
for variant in &enum_def.variants {
let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
let wire_name = wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
if variant.fields.is_empty() {
out.push_str(&template_env::render(
"flat_enum_from_core_variant_unit.jinja",
minijinja::context! {
core_path => &core_path,
vname => &variant.name,
disc_ident => &disc_ident,
wire => &wire_name,
},
));
} else if variant.is_tuple {
let first_field = variant.fields.first().unwrap();
let is_boxed = first_field.is_boxed;
let is_sanitized_to_string = first_field.sanitized && matches!(first_field.ty, TypeRef::String);
let data_expr: String = if is_sanitized_to_string {
if is_boxed {
"format!(\"{:?}\", *_0)".to_string()
} else {
"format!(\"{:?}\", _0)".to_string()
}
} else if is_boxed {
"(*_0).into()".to_string()
} else {
"_0.into()".to_string()
};
out.push_str(&template_env::render(
"flat_enum_from_core_variant_tuple.jinja",
minijinja::context! {
core_path => &core_path,
vname => &variant.name,
disc_ident => &disc_ident,
wire => &wire_name,
fname => &field_name,
expr => &data_expr,
},
));
} else {
out.push_str(&template_env::render(
"flat_enum_from_core_variant_struct.jinja",
minijinja::context! {
core_path => &core_path,
vname => &variant.name,
disc_ident => &disc_ident,
wire => &wire_name,
},
));
}
}
out.push_str(&template_env::render(
"flat_enum_from_core_impl_catch_all.jinja",
minijinja::context! {},
));
out.push_str(&template_env::render(
"flat_enum_from_core_impl_footer.jinja",
minijinja::context! {},
));
out
}
pub(super) fn gen_extendr_flat_data_enum_to_core(enum_def: &EnumDef, core_import: &str) -> String {
let name = &enum_def.name;
let core_path = enum_core_path(enum_def, core_import);
let discriminator = enum_def.serde_tag.as_deref().unwrap_or("format_type");
let disc_ident = crate::core::keywords::rust_raw_ident(discriminator);
let mut out = String::with_capacity(512);
out.push_str(&template_env::render(
"flat_enum_from_binding_impl.jinja",
minijinja::context! {
name => name,
core_path => &core_path,
disc_ident => &disc_ident,
},
));
for variant in &enum_def.variants {
let field_name = heck::AsSnakeCase(variant.name.as_str()).to_string();
let wire_name = wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
if variant.fields.is_empty() {
out.push_str(&template_env::render(
"flat_enum_from_binding_variant_unit.jinja",
minijinja::context! {
wire => &wire_name,
vname => &variant.name,
},
));
} else if variant.is_tuple {
out.push_str(&template_env::render(
"flat_enum_from_binding_variant_tuple.jinja",
minijinja::context! {
wire => &wire_name,
vname => &variant.name,
fname => &field_name,
},
));
}
}
out.push_str(&template_env::render(
"flat_enum_from_binding_impl_footer.jinja",
minijinja::context! {},
));
out
}
mod json_functions;
pub use json_functions::{gen_extendr_json_bridged_function, return_type_needs_json};