use super::*;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
fn make_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
version: Default::default(),
name: name.to_string(),
ty,
optional,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: crate::core::ir::CoreWrapper::None,
vec_inner_core_wrapper: crate::core::ir::CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
serde_with: None,
serde_skip_serializing_if: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
}
fn make_typedef(name: &str, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("test_lib::{name}"),
original_rust_path: String::new(),
fields,
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
serde_container_default: false,
serde_container_conversion: Default::default(),
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
has_private_fields: false,
version: Default::default(),
}
}
#[test]
fn explicit_default_impl_preserves_serde_default_fn_instead_of_type_zero_value() {
let mut span = make_field(
"row_span",
TypeRef::Primitive(crate::core::ir::PrimitiveType::U32),
false,
);
span.typed_default = Some(crate::core::ir::DefaultValue::FunctionCall("default_span".to_string()));
let mut typ = make_typedef(
"GridCell",
vec![
make_field("content", TypeRef::String, false),
make_field("row", TypeRef::Primitive(crate::core::ir::PrimitiveType::U32), false),
span,
],
);
typ.has_serde = true;
let map_fn = |ty: &TypeRef| match ty {
TypeRef::String => "String".to_string(),
_ => "u32".to_string(),
};
let output = gen_struct_default_impl_explicit(&typ, &map_fn, &[])
.expect("a struct with a field-level default must get an explicit Default impl");
assert!(
!output.contains("row_span: Default::default()"),
"row_span must not fall back to the type's zero value, which is not `default_span()`:\n{output}"
);
assert!(
output.contains("serde_json::from_str::<test_lib::GridCell>"),
"row_span must recover the real serde default by deserializing a stub:\n{output}"
);
}
#[test]
fn gen_enum_unit_variants_emit_ruby_symbols() {
let enum_def = EnumDef {
name: "Status".to_string(),
rust_path: "test_lib::Status".to_string(),
original_rust_path: String::new(),
variants: vec![
EnumVariant {
name: "Pending".to_string(),
fields: vec![],
doc: String::new(),
is_default: false,
serde_rename: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
},
EnumVariant {
name: "Done".to_string(),
fields: vec![],
doc: String::new(),
is_default: false,
serde_rename: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
},
],
methods: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: false,
has_default: false,
serde_content: None,
serde_tag: None,
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
};
let code = gen_enum(&enum_def);
assert!(code.contains("enum Status"), "must emit enum definition");
assert!(code.contains("to_symbol"), "unit enums use Ruby symbols");
assert!(
code.contains("Status::Pending => \"Pending\","),
"no rename_all declared, so the IntoValue output is the verbatim wire value:\n{code}"
);
}
#[test]
fn gen_enum_unit_variant_wire_value_is_verbatim_without_rename_all() {
let enum_def = EnumDef {
name: "DataNodeKind".to_string(),
rust_path: "test_lib::DataNodeKind".to_string(),
variants: vec![make_variant("KeyValue", vec![]), make_variant("Sequence", vec![])],
..Default::default()
};
let code = gen_enum(&enum_def);
assert!(
code.contains("DataNodeKind::KeyValue => \"KeyValue\","),
"serde's real default (no rename_all) serializes unit variants verbatim, not snake_cased:\n{code}"
);
assert!(
!code.contains("=> \"key_value\","),
"must not fabricate a snake_case wire value the Rust enum never declared:\n{code}"
);
}
#[test]
fn gen_enum_unit_variant_try_convert_still_accepts_the_legacy_snake_case_spelling() {
let enum_def = EnumDef {
name: "DataNodeKind".to_string(),
rust_path: "test_lib::DataNodeKind".to_string(),
variants: vec![make_variant("KeyValue", vec![])],
..Default::default()
};
let code = gen_enum(&enum_def);
assert!(
code.contains("\"key_value\""),
"existing consumer code passing the old snake_case symbol must keep working:\n{code}"
);
assert!(
code.contains("\"KeyValue\""),
"the new verbatim wire value must also be accepted on input:\n{code}"
);
}
fn make_variant(name: &str, fields: Vec<FieldDef>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields,
doc: String::new(),
is_default: false,
serde_rename: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
}
}
fn make_data_enum(name: &str, serde_tag: Option<&str>) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: format!("test_lib::{name}"),
original_rust_path: String::new(),
variants: vec![
make_variant("Png", vec![]),
make_variant("Jpeg", vec![make_field("quality", TypeRef::String, false)]),
],
methods: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: true,
has_default: false,
serde_content: None,
serde_tag: serde_tag.map(str::to_string),
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
}
}
#[test]
fn gen_enum_wraps_string_for_internally_tagged_enum() {
let code = gen_enum(&make_data_enum("ImageOutputFormat", Some("type")));
assert!(
code.contains(r#".or_else(|_| serde_json::from_value(serde_json::json!({ "type": json_str })))"#),
"expected tagged string wrap for internally-tagged enum: {code}"
);
}
#[test]
fn gen_enum_keeps_bare_string_for_externally_tagged_enum() {
let code = gen_enum(&make_data_enum("ExternallyTagged", None));
assert!(
!code.contains("serde_json::from_value(serde_json::json!({"),
"externally-tagged enum must not wrap the string in a tag object: {code}"
);
assert!(
code.contains("serde_json::from_str(&json_str)"),
"data enum must keep the from_str path: {code}"
);
}
#[test]
fn gen_enum_emits_adjacent_serde_representation() {
let mut enum_def = make_data_enum("OperationResult", Some("type"));
enum_def.serde_content = Some("output".to_string());
enum_def.variants[1].is_tuple = true;
enum_def.variants[1].fields[0].name = "_0".to_string();
let code = gen_enum(&enum_def);
assert!(code.contains(r#"#[serde(tag = "type", content = "output")]"#));
assert!(code.contains("Jpeg(String)"));
assert!(code.contains("Self::Jpeg(_0) => Some(_0)"), "{code}");
assert!(!code.contains("Self::Jpeg { _0 }"), "{code}");
syn::parse_file(&code).unwrap_or_else(|error| panic!("generated Rust must parse: {error}\n{code}"));
}
#[test]
fn adjacent_tuple_default_uses_tuple_constructor_syntax() {
let mut enum_def = make_data_enum("OperationResult", Some("type"));
enum_def.serde_content = Some("output".to_string());
enum_def.variants[1].is_tuple = true;
enum_def.variants[1].is_default = true;
enum_def.variants[1].fields[0].name = "_0".to_string();
let code = gen_enum(&enum_def);
assert!(code.contains("Self::Jpeg(Default::default())"), "{code}");
assert!(!code.contains("Self::Jpeg { _0:"), "{code}");
syn::parse_file(&code).unwrap_or_else(|error| panic!("generated Rust must parse: {error}\n{code}"));
}
#[test]
fn gen_struct_emits_magnus_wrap_attribute() {
let typ = make_typedef("Config", vec![make_field("value", TypeRef::String, false)]);
let mapper = crate::backends::magnus::type_map::MagnusMapper;
let code = gen_struct(&typ, &mapper, "TestLib", "test_lib", false, &[], false);
assert!(code.contains("magnus::wrap"), "struct must have magnus::wrap");
assert!(code.contains("struct Config"), "must emit struct Config");
}
fn container_conversion() -> crate::core::ir::SerdeContainerConversion {
crate::core::ir::SerdeContainerConversion {
from: Some("(f64, f64)".to_string()),
into: Some("(f64, f64)".to_string()),
try_from: None,
transparent: false,
}
}
#[test]
fn gen_struct_delegates_deserialize_when_caller_confirms_eligibility() {
let mut typ = make_typedef(
"Point",
vec![
make_field("x", TypeRef::Primitive(crate::core::ir::PrimitiveType::F64), false),
make_field("y", TypeRef::Primitive(crate::core::ir::PrimitiveType::F64), false),
],
);
typ.serde_container_conversion = container_conversion();
let mapper = crate::backends::magnus::type_map::MagnusMapper;
let code = gen_struct(&typ, &mapper, "TestLib", "test_lib", false, &[], true);
let derive_line = code.lines().find(|l| l.trim_start().starts_with("#[derive(")).unwrap();
assert!(
!derive_line.contains("serde::Deserialize"),
"derive must drop Deserialize when delegating: {derive_line}"
);
assert!(
derive_line.contains("serde::Serialize"),
"Serialize stays derived: {derive_line}"
);
assert!(
code.contains("impl<'de> serde::Deserialize<'de> for Point {"),
"expected a delegating Deserialize impl in: {code}"
);
assert!(
code.contains("<test_lib::Point as serde::Deserialize>::deserialize(deserializer).map(Into::into)"),
"delegating impl must read the core type: {code}"
);
assert!(code.contains("serde_json::from_str::<Point>(&json_str)"));
}
#[test]
fn gen_struct_keeps_derive_when_delegation_not_confirmed() {
let mut typ = make_typedef(
"Point",
vec![
make_field("x", TypeRef::Primitive(crate::core::ir::PrimitiveType::F64), false),
make_field("y", TypeRef::Primitive(crate::core::ir::PrimitiveType::F64), false),
],
);
typ.serde_container_conversion = container_conversion();
let mapper = crate::backends::magnus::type_map::MagnusMapper;
let code = gen_struct(&typ, &mapper, "TestLib", "test_lib", false, &[], false);
let derive_line = code.lines().find(|l| l.trim_start().starts_with("#[derive(")).unwrap();
assert!(derive_line.contains("serde::Deserialize"), "{derive_line}");
assert!(!code.contains("impl<'de> serde::Deserialize<'de> for Point"));
}
#[test]
fn gen_opaque_struct_emits_arc_inner() {
let typ = make_typedef("Handle", vec![]);
let code = gen_opaque_struct(&typ, "test_lib", "TestLib");
assert!(code.contains("inner: Arc<"), "opaque struct must have Arc inner");
assert!(code.contains("struct Handle"), "must emit struct Handle");
}
use crate::core::ir::MethodDef;
fn shape_enum() -> EnumDef {
EnumDef {
name: "Shape".to_string(),
rust_path: "test_lib::Shape".to_string(),
original_rust_path: String::new(),
variants: vec![
make_variant("Circle", vec![make_field("radius", TypeRef::String, false)]),
make_variant(
"Rect",
vec![
make_field("width", TypeRef::String, false),
make_field("height", TypeRef::String, false),
],
),
],
methods: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: true,
has_default: false,
serde_content: None,
serde_tag: Some("type".to_string()),
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
}
}
#[test]
fn variant_constructors_emit_singleton_per_struct_variant() {
let code = gen_data_enum_variant_constructors(&shape_enum());
assert!(code.contains("impl Shape {"), "must emit an impl block: {code}");
assert!(
code.contains("pub fn _factory_circle(radius: String) -> Self"),
"{code}"
);
assert!(code.contains("Self::Circle { radius }"), "{code}");
assert!(
code.contains("pub fn _factory_rect(width: String, height: String) -> Self"),
"{code}"
);
assert!(code.contains("Self::Rect { width, height }"), "{code}");
}
#[test]
fn variant_constructors_use_serde_shaped_named_field_type() {
let def = EnumDef {
name: "Wrapper".to_string(),
rust_path: "test_lib::Wrapper".to_string(),
original_rust_path: String::new(),
variants: vec![make_variant(
"Llm",
vec![
make_field("llm", TypeRef::Named("LlmConfig".to_string()), false),
make_field(
"opts",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
),
],
)],
methods: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: true,
has_default: false,
serde_content: None,
serde_tag: Some("type".to_string()),
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
};
let code = gen_data_enum_variant_constructors(&def);
assert!(
code.contains("pub fn _factory_llm(llm: LlmConfig, opts: String) -> Self"),
"{code}"
);
assert!(code.contains("Self::Llm { llm, opts }"), "{code}");
assert!(
!code.contains("_core"),
"magnus enum is binding-shaped, no core conversion: {code}"
);
}
#[test]
fn variant_constructors_skip_unit_tuple_and_excluded() {
let mut tuple_variant = make_variant("Pair", vec![make_field("_0", TypeRef::String, false)]);
tuple_variant.is_tuple = true;
let mut excluded = make_variant("Hidden", vec![make_field("value", TypeRef::String, false)]);
excluded.binding_excluded = true;
let def = EnumDef {
variants: vec![
make_variant("Empty", vec![]),
tuple_variant,
excluded,
make_variant("Real", vec![make_field("value", TypeRef::String, false)]),
],
..shape_enum()
};
let code = gen_data_enum_variant_constructors(&def);
assert!(!code.contains("_factory_empty"), "{code}");
assert!(!code.contains("_factory_pair"), "{code}");
assert!(!code.contains("_factory_hidden"), "{code}");
assert!(code.contains("pub fn _factory_real(value: String) -> Self"), "{code}");
}
#[test]
fn variant_constructors_emit_factory_even_with_colliding_hand_written_method() {
let def = EnumDef {
methods: vec![MethodDef {
name: "circle".to_string(),
is_static: true,
..Default::default()
}],
..shape_enum()
};
let code = gen_data_enum_variant_constructors(&def);
assert!(
code.contains("pub fn _factory_circle(radius: String) -> Self"),
"Circle factory must stay reachable despite the colliding hand-written method: {code}"
);
assert!(code.contains("Self::Circle { radius }"), "{code}");
assert!(
code.contains("pub fn _factory_rect(width: String, height: String) -> Self"),
"{code}"
);
}
#[test]
fn variant_constructors_empty_for_unit_only_enum() {
let def = EnumDef {
variants: vec![make_variant("A", vec![]), make_variant("B", vec![])],
..shape_enum()
};
let code = gen_data_enum_variant_constructors(&def);
assert!(code.is_empty(), "expected no output for unit-only enum: {code}");
}
#[test]
fn adjacently_tagged_tuple_variant_uses_tuple_form_in_both_definition_and_conversions() {
use crate::codegen::conversions::helpers::variant_emits_tuple_form;
let mut adjacent = make_data_enum("OperationResult", Some("type"));
adjacent.serde_content = Some("output".to_string());
adjacent.variants[1].is_tuple = true;
adjacent.variants[1].fields[0].name = "_0".to_string();
let code = gen_enum(&adjacent);
assert!(code.contains("Jpeg(String)"), "{code}");
assert!(!code.contains("Self::Jpeg { _0 }"), "{code}");
assert!(
variant_emits_tuple_form(&adjacent, &adjacent.variants[1]),
"adjacently-tagged tuple variant must report tuple form to the conversion layer"
);
let mut untagged = make_data_enum("OperationResult", None);
untagged.serde_untagged = true;
untagged.variants[1].is_tuple = true;
untagged.variants[1].fields[0].name = "_0".to_string();
assert!(variant_emits_tuple_form(&untagged, &untagged.variants[1]));
assert!(
!variant_emits_tuple_form(&adjacent, &adjacent.variants[0]),
"struct-form variants must not be reported as tuple form"
);
}
fn async_method_without_error(name: &str, receiver: ReceiverKind) -> MethodDef {
MethodDef {
name: name.to_string(),
return_type: TypeRef::String,
is_async: true,
error_type: None,
receiver: Some(receiver),
cfg: None,
..Default::default()
}
}
#[test]
fn opaque_async_method_without_error_type_still_returns_result() {
let typ = make_typedef("Widget", vec![]);
let method = async_method_without_error("process", ReceiverKind::Ref);
let mapper = MagnusMapper;
let code = gen_opaque_async_instance_method(
&typ,
&method,
&mapper,
"Widget",
&AHashSet::default(),
&AHashSet::default(),
"test_lib",
false,
);
assert!(
code.contains("fn process_async(&self, ) -> Result<String, Error> {"),
"async opaque method must stay Result-shaped even without a declared error type, got: {code}"
);
assert!(
code.contains("tokio::runtime::Runtime::new()"),
"delegable async body must build a runtime, got: {code}"
);
}
#[test]
fn non_opaque_async_method_without_error_type_still_returns_result() {
let typ = make_typedef("Widget", vec![]);
let method = async_method_without_error("process", ReceiverKind::Ref);
let mapper = MagnusMapper;
let code = gen_async_instance_method(&method, &mapper, &typ, &AHashSet::default(), "test_lib");
assert!(
code.contains("fn process_async(&self, ) -> Result<String, Error> {"),
"async instance method must stay Result-shaped even without a declared error type, got: {code}"
);
assert!(
code.contains("tokio::runtime::Runtime::new()"),
"delegable async body must build a runtime, got: {code}"
);
}