use crate::codegen::conversions::ConversionConfig;
use crate::core::ir::{FieldDef, TypeRef};
use super::field_fragments::sanitized_vec_field_to_core_expr;
use super::{
apply_field_newtype_from_core, apply_field_newtype_to_core, core_container_type,
explicit_newtype_replaces_base_conversion, explicit_newtype_uses_wasm_jsvalue, field_references_excluded_type,
is_tuple_variant,
};
fn field_init(field_name: &str, expr: &str) -> String {
if expr == field_name {
field_name.to_string()
} else {
format!("{field_name}: {expr}")
}
}
fn explicit_newtype_to_core_expr(field: &FieldDef, binding: &str, config: &ConversionConfig) -> String {
use crate::codegen::conversions::field_conversion_to_core_cfg;
let wrapper = field.newtype_wrapper.as_ref().expect("caller checked wrapper");
let wasm_jsvalue =
config.wasm_explicit_newtype_containers_use_jsvalue && explicit_newtype_uses_wasm_jsvalue(wrapper);
let source = if wasm_jsvalue {
let binding_ty = core_container_type(&field.ty, config.core_type_paths);
let deserialize = |value: &str| {
format!(
"serde_wasm_bindgen::from_value::<{binding_ty}>({value}).unwrap_or_else(|error| \
wasm_bindgen::throw_val(wasm_bindgen::JsValue::from_str(&error.to_string())))"
)
};
if field.optional {
format!("({binding}).map(|value| {})", deserialize("value"))
} else {
deserialize(binding)
}
} else if explicit_newtype_replaces_base_conversion(&field.ty, field.optional, wrapper, config) {
binding.to_string()
} else {
let base = field_conversion_to_core_cfg(binding, &field.ty, field.optional, config);
if let Some(expr) = base.strip_prefix(&format!("{binding}: ")) {
expr.replace(&format!("val.{binding}"), binding)
} else {
base
}
};
apply_field_newtype_to_core(&source, &field.ty, field.optional, wrapper)
}
fn explicit_newtype_from_core_expr(field: &FieldDef, binding: &str, config: &ConversionConfig) -> String {
use crate::codegen::conversions::field_conversion_from_core_cfg;
use ahash::AHashSet;
let wrapper = field.newtype_wrapper.as_ref().expect("caller checked wrapper");
let wasm_jsvalue =
config.wasm_explicit_newtype_containers_use_jsvalue && explicit_newtype_uses_wasm_jsvalue(wrapper);
let converted = apply_field_newtype_from_core(binding, &field.ty, field.optional, wrapper);
if wasm_jsvalue {
return if field.optional {
format!(
"({converted}).map(|value| serde_wasm_bindgen::to_value(&value).unwrap_or_else(|error| \
wasm_bindgen::throw_val(wasm_bindgen::JsValue::from_str(&error.to_string()))))"
)
} else {
format!(
"serde_wasm_bindgen::to_value(&{converted}).unwrap_or_else(|error| \
wasm_bindgen::throw_val(wasm_bindgen::JsValue::from_str(&error.to_string())))"
)
};
}
if explicit_newtype_replaces_base_conversion(&field.ty, field.optional, wrapper, config) {
return converted;
}
let base = field_conversion_from_core_cfg(
binding,
&field.ty,
field.optional,
field.sanitized,
&AHashSet::new(),
config,
);
base.strip_prefix(&format!("{binding}: "))
.unwrap_or(&base)
.replace(&format!("val.{binding}"), &converted)
}
fn sanitized_field_parse_or_warn(access: &str, variant_name: &str, field_name: &str) -> String {
let context = format!("variant = \"{variant_name}\", field = \"{field_name}\"");
crate::codegen::template_env::render(
"conversions/sanitized_json_parse_or_warn",
crate::alef_context! {
access => access,
context => context,
message => "binding provided unparseable JSON for enum variant field; substituting default",
},
)
.trim_end()
.to_string()
}
pub fn binding_to_core_match_arm_ext_cfg(
binding_prefix: &str,
variant_name: &str,
fields: &[FieldDef],
binding_has_data: bool,
config: &ConversionConfig,
binding_uses_tuple_form: bool,
) -> String {
use crate::codegen::conversions::field_conversion_to_core_cfg;
if fields.is_empty() {
format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
} else if !binding_has_data {
if is_tuple_variant(fields) {
let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
format!(
"{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
defaults.join(", ")
)
} else {
let defaults: Vec<String> = fields
.iter()
.map(|f| format!("{}: Default::default()", f.name))
.collect();
format!(
"{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
defaults.join(", ")
)
}
} else if is_tuple_variant(fields) {
let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
let binding_pattern = field_names.join(", ");
let core_args: Vec<String> = fields
.iter()
.map(|f| {
let name = &f.name;
if f.sanitized {
let expr = if let TypeRef::Vec(_) = &f.ty {
sanitized_vec_field_to_core_expr(name, &f.ty)
} else {
sanitized_field_parse_or_warn(name, variant_name, name)
};
return if f.is_boxed { format!("Box::new({expr})") } else { expr };
}
if !config.exclude_types.is_empty() && field_references_excluded_type(&f.ty, config.exclude_types) {
let expr = sanitized_field_parse_or_warn(name, variant_name, name);
return if f.is_boxed { format!("Box::new({expr})") } else { expr };
}
if f.newtype_wrapper.as_deref().is_some_and(super::is_explicit_newtype) {
let expr = explicit_newtype_to_core_expr(f, name, config);
return if f.is_boxed { format!("Box::new({expr})") } else { expr };
}
let conv = field_conversion_to_core_cfg(name, &f.ty, f.optional, config);
let expr = if let Some(expr) = conv.strip_prefix(&format!("{name}: ")) {
let expr = expr.replace(&format!("val.{name}"), name);
expr.to_string()
} else {
conv
};
let expr = if config.binding_enum_fields_are_boxed && f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
if f.optional {
expr.replace(
&format!("{name}.map(Into::into)"),
&format!("{name}.map(|v| (*v).into())"),
)
} else {
expr.replace(&format!("{name}.into()"), &format!("(*{name}).into()"))
}
} else {
expr
};
let expr = f
.newtype_wrapper
.as_ref()
.map(|wrapper| apply_field_newtype_to_core(&expr, &f.ty, f.optional, wrapper))
.unwrap_or(expr);
if f.is_boxed { format!("Box::new({expr})") } else { expr }
})
.collect();
let pattern_syntax = if binding_uses_tuple_form {
format!("{binding_prefix}::{variant_name}({binding_pattern})")
} else {
format!("{binding_prefix}::{variant_name} {{ {binding_pattern} }}")
};
format!("{pattern_syntax} => Self::{variant_name}({}),", core_args.join(", "))
} else {
let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
let pattern = field_names.join(", ");
let core_fields: Vec<String> = fields
.iter()
.map(|f| {
if f.sanitized {
if let TypeRef::Vec(_) = &f.ty {
let expr = sanitized_vec_field_to_core_expr(&f.name, &f.ty);
return format!("{}: {expr}", f.name);
}
let expr = sanitized_field_parse_or_warn(&f.name, variant_name, &f.name);
return format!("{}: {expr}", f.name);
}
if f.newtype_wrapper.as_deref().is_some_and(super::is_explicit_newtype) {
let expr = explicit_newtype_to_core_expr(f, &f.name, config);
return field_init(&f.name, &expr);
}
let conv = field_conversion_to_core_cfg(&f.name, &f.ty, f.optional, config);
let expr = if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
expr.replace(&format!("val.{}", f.name), &f.name)
} else {
conv.strip_prefix(&format!("{}: ", f.name)).unwrap_or(&conv).to_string()
};
let binding_field_is_boxed =
config.binding_enum_fields_are_boxed && f.is_boxed && matches!(&f.ty, TypeRef::Named(_));
let expr = if binding_field_is_boxed {
if f.optional {
format!("{}.map(|v| Box::new((*v).into()))", f.name)
} else {
format!("Box::new((*{}).into())", f.name)
}
} else if f.is_boxed {
if f.optional {
format!("{expr}.map(Box::new)")
} else {
format!("Box::new({expr})")
}
} else {
expr
};
let expr = f
.newtype_wrapper
.as_ref()
.map(|wrapper| apply_field_newtype_to_core(&expr, &f.ty, f.optional, wrapper))
.unwrap_or(expr);
field_init(&f.name, &expr)
})
.collect();
format!(
"{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
core_fields.join(", ")
)
}
}
pub fn core_to_binding_match_arm_ext_cfg(
core_prefix: &str,
variant_name: &str,
fields: &[FieldDef],
binding_has_data: bool,
config: &ConversionConfig,
binding_uses_tuple_form: bool,
) -> String {
use crate::codegen::conversions::field_conversion_from_core_cfg;
use ahash::AHashSet;
if fields.is_empty() {
format!("{core_prefix}::{variant_name} => Self::{variant_name},")
} else if !binding_has_data {
if is_tuple_variant(fields) {
format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
} else {
format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
}
} else if is_tuple_variant(fields) {
let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
let core_pattern = field_names.join(", ");
let binding_fields: Vec<String> = fields
.iter()
.map(|f| {
if f.newtype_wrapper.as_deref().is_some_and(super::is_explicit_newtype) {
let expr = explicit_newtype_from_core_expr(f, &f.name, config);
return if binding_uses_tuple_form {
expr
} else {
field_init(&f.name, &expr)
};
}
let conv =
field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
let source = f
.newtype_wrapper
.as_ref()
.map(|wrapper| apply_field_newtype_from_core(&f.name, &f.ty, f.optional, wrapper))
.unwrap_or_else(|| f.name.clone());
let mut expr = expr.replace(&format!("val.{}", f.name), &source);
if f.is_boxed {
if config.binding_enum_fields_are_boxed {
if f.optional {
expr = expr.replace(
&format!("{}.map(Into::into)", f.name),
&format!("{}.map(|v| Box::new((*v).into()))", f.name),
);
} else {
expr = expr.replace(
&format!("{}.into()", f.name),
&format!("Box::new((*{}).into())", f.name),
);
}
} else {
expr = expr.replace(&format!("{}.into()", f.name), &format!("(*{}).into()", f.name));
}
}
if binding_uses_tuple_form {
let string_move = format!("{}.to_string()", f.name);
if expr == string_move {
expr = f.name.clone();
}
expr
} else {
field_init(&f.name, &expr)
}
} else {
conv
}
})
.collect();
if binding_uses_tuple_form {
format!(
"{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name}({}),",
binding_fields.join(", ")
)
} else {
format!(
"{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
binding_fields.join(", ")
)
}
} else {
let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
let pattern = field_names.join(", ");
let binding_fields: Vec<String> = fields
.iter()
.map(|f| {
if f.newtype_wrapper.as_deref().is_some_and(super::is_explicit_newtype) {
let expr = explicit_newtype_from_core_expr(f, &f.name, config);
return field_init(&f.name, &expr);
}
let conv =
field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
let source = f
.newtype_wrapper
.as_ref()
.map(|wrapper| apply_field_newtype_from_core(&f.name, &f.ty, f.optional, wrapper))
.unwrap_or_else(|| f.name.clone());
let mut expr = expr.replace(&format!("val.{}", f.name), &source);
if f.is_boxed {
if config.binding_enum_fields_are_boxed {
if f.optional {
expr = expr.replace(
&format!("{}.map(Into::into)", f.name),
&format!("{}.map(|v| Box::new((*v).into()))", f.name),
);
} else {
expr = expr.replace(
&format!("{}.into()", f.name),
&format!("Box::new((*{}).into())", f.name),
);
}
} else {
expr = expr.replace(&format!("{}.into()", f.name), &format!("(*{}).into()", f.name));
}
}
field_init(&f.name, &expr)
} else {
conv
}
})
.collect();
format!(
"{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
binding_fields.join(", ")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::PrimitiveType;
#[test]
fn field_init_collapses_only_when_expr_is_exactly_the_field_name() {
let cases: &[(&str, &str, &str)] = &[
("url", "url", "url"),
("url", "val.url.to_string()", "url: val.url.to_string()"),
("id", "identifier", "id: identifier"),
("bar", "bar.clone()", "bar: bar.clone()"),
("bar", "into_x(bar)", "bar: into_x(bar)"),
("bar", "*bar", "bar: *bar"),
("r#type", "r#type", "r#type"),
("r#type", "r#type.to_string()", "r#type: r#type.to_string()"),
];
for (field_name, expr, expected) in cases {
assert_eq!(
field_init(field_name, expr),
*expected,
"field_init({field_name:?}, {expr:?}) mismatch"
);
}
}
fn string_field(name: &str) -> FieldDef {
FieldDef {
name: name.to_string(),
ty: TypeRef::String,
..Default::default()
}
}
#[test]
fn core_to_binding_match_arm_collapses_no_op_fields_only() {
let fields = vec![
FieldDef {
name: "id".to_string(),
ty: TypeRef::Primitive(PrimitiveType::I32),
..Default::default()
},
string_field("url"),
];
let config = ConversionConfig::default();
let arm = core_to_binding_match_arm_ext_cfg("Core", "Link", &fields, true, &config, false);
assert_eq!(
arm, "Core::Link { id, url } => Self::Link { id, url: url.to_string() },",
"no-op field `id` must collapse to shorthand while converted field `url` keeps its \
`name: expr` form, got:\n{arm}"
);
}
#[test]
fn binding_to_core_match_arm_collapses_all_no_op_fields() {
let fields = vec![string_field("url"), string_field("title")];
let config = ConversionConfig::default();
let arm = binding_to_core_match_arm_ext_cfg("Binding", "Link", &fields, true, &config, false);
assert_eq!(
arm, "Binding::Link { url, title } => Self::Link { url, title },",
"both no-op fields must collapse to shorthand, got:\n{arm}"
);
}
}