alef 0.67.4

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::backends::extendr::gen_bindings::ExtendrBackend;
use crate::backends::extendr::gen_bindings::bridges::{
    extendr_enum_variant_constructor_registrations, gen_extendr_enum_variant_constructors,
    gen_extendr_flat_data_enum_from_core, gen_extendr_flat_data_enum_struct, gen_extendr_flat_data_enum_to_core,
    gen_extendr_json_passthrough_enum_struct,
};
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, MethodDef, PrimitiveType, TypeRef};

fn field(name: &str, ty: TypeRef) -> FieldDef {
    FieldDef {
        name: name.to_string(),
        ty,
        ..Default::default()
    }
}

fn variant(name: &str, fields: Vec<FieldDef>) -> EnumVariant {
    EnumVariant {
        name: name.to_string(),
        fields,
        ..Default::default()
    }
}

/// A tagged data enum with struct variants — the JSON-passthrough shape.
fn shape_enum() -> EnumDef {
    EnumDef {
        name: "Shape".to_string(),
        rust_path: "test_lib::Shape".to_string(),
        variants: vec![
            variant("Circle", vec![field("radius", TypeRef::Primitive(PrimitiveType::F64))]),
            variant(
                "Rect",
                vec![
                    field("width", TypeRef::Primitive(PrimitiveType::F64)),
                    field("height", TypeRef::Primitive(PrimitiveType::F64)),
                ],
            ),
        ],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    }
}

#[test]
fn emits_constructor_per_struct_variant_building_core_then_into() {
    let core_path = "test_lib::Shape";
    let methods = gen_extendr_enum_variant_constructors(&shape_enum(), &ExtendrBackend, core_path);

    let code = methods.join("\n");
    assert!(code.contains("pub fn _factory_circle(radius: f64) -> Shape"), "{code}");
    assert!(code.contains("test_lib::Shape::Circle { radius }.into()"), "{code}");
    assert!(
        code.contains("pub fn _factory_rect(width: f64, height: f64) -> Shape"),
        "{code}"
    );
    assert!(
        code.contains("test_lib::Shape::Rect { width, height }.into()"),
        "{code}"
    );
}

#[test]
fn casts_remapped_primitive_back_to_core() {
    let def = EnumDef {
        name: "Sized_".to_string(),
        rust_path: "test_lib::Sized_".to_string(),
        variants: vec![variant(
            "Big",
            vec![field("count", TypeRef::Primitive(PrimitiveType::U64))],
        )],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    };
    let methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Sized_");
    let code = methods.join("\n");
    assert!(code.contains("pub fn _factory_big(count: f64) -> Sized_"), "{code}");
    assert!(
        code.contains("test_lib::Sized_::Big { count: count as u64 }.into()"),
        "{code}"
    );
}

#[test]
fn skips_variant_constructor_with_named_dto_field() {
    // extendr derives `TryFrom<&Robj>` only for `&T` of #[extendr] types, never owned `T`, so a
    // `#[extendr]` proc-macro (`error[E0277]: T: TryFrom<&Robj> not satisfied`). Variants whose
    let def = EnumDef {
        name: "Wrapper".to_string(),
        rust_path: "test_lib::Wrapper".to_string(),
        variants: vec![
            variant("Llm", vec![field("llm", TypeRef::Named("LlmConfig".to_string()))]),
            variant("Tag", vec![field("name", TypeRef::String)]),
        ],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    };
    let methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Wrapper");
    let code = methods.join("\n");
    assert!(
        !code.contains("_factory_llm"),
        "variant with a Named DTO field must be skipped: {code}"
    );
    assert!(
        code.contains("pub fn _factory_tag(name: String) -> Wrapper"),
        "primitive/String variant must still be generated: {code}"
    );
}

#[test]
fn skips_variant_constructor_when_any_field_is_unconstructible() {
    // field by value breaks the whole `#[extendr]` constructor.
    let def = EnumDef {
        name: "Job".to_string(),
        rust_path: "test_lib::Job".to_string(),
        variants: vec![
            variant(
                "Run",
                vec![
                    field("config", TypeRef::Named("RunConfig".to_string())),
                    field("retries", TypeRef::Primitive(PrimitiveType::U32)),
                    field("name", TypeRef::String),
                ],
            ),
            variant(
                "Tag",
                vec![field(
                    "entries",
                    TypeRef::Vec(Box::new(TypeRef::Named("Entry".to_string()))),
                )],
            ),
            variant("Ping", vec![field("seq", TypeRef::Primitive(PrimitiveType::U32))]),
        ],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    };
    let methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Job");
    let code = methods.join("\n");
    assert!(
        !code.contains("_factory_run"),
        "Named-DTO-field variant must be skipped: {code}"
    );
    assert!(
        !code.contains("_factory_tag"),
        "Vec<DTO>-field variant must be skipped: {code}"
    );
    assert!(
        code.contains("test_lib::Job::Ping { seq: seq as u32 }.into()"),
        "primitive-only variant must still be generated: {code}"
    );
}

#[test]
fn skips_unit_tuple_and_excluded_variants() {
    let mut tuple_variant = variant("Pair", vec![field("_0", TypeRef::String)]);
    tuple_variant.is_tuple = true;
    let mut excluded = variant("Hidden", vec![field("value", TypeRef::String)]);
    excluded.binding_excluded = true;

    let def = EnumDef {
        name: "Mixed".to_string(),
        rust_path: "test_lib::Mixed".to_string(),
        variants: vec![
            variant("Empty", vec![]),
            tuple_variant,
            excluded,
            variant("Real", vec![field("value", TypeRef::String)]),
        ],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    };
    let methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Mixed");
    let code = methods.join("\n");
    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) -> Mixed"), "{code}");
}

/// Regression for the `ContentPart` bug: a hand-written inherent static method
/// (`enum_def.methods`, extracted from a separate `impl EnumType { .. }` block) is never forwarded
/// into the generated `#[extendr] impl` block, so suppressing the derived factory on a name
/// collision used to drop the constructor entirely (`ContentPart$text(...)` was unreachable from R).
/// Every data-carrying variant must always get a reachable factory.
#[test]
fn emits_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 methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Shape");
    let code = methods.join("\n");
    assert!(
        code.contains("pub fn _factory_circle(radius: f64) -> Shape"),
        "Circle factory must stay reachable despite the colliding hand-written method: {code}"
    );
    assert!(code.contains("pub fn _factory_rect"), "{code}");
}

#[test]
fn struct_embeds_constructors_in_impl_block() {
    // End to end: the generated `#[extendr] impl` block carries default/from_json AND the
    let code = gen_extendr_json_passthrough_enum_struct(&shape_enum(), &ExtendrBackend, "test_lib");
    assert!(code.contains("pub fn default() -> Shape"), "{code}");
    assert!(code.contains("pub fn from_json(json: String)"), "{code}");
    assert!(code.contains("pub fn _factory_circle(radius: f64) -> Shape"), "{code}");
}

#[test]
fn casts_optional_remapped_primitive_back_to_core() {
    let mut max_field = field("max", TypeRef::Primitive(PrimitiveType::U64));
    max_field.optional = true;
    let def = EnumDef {
        name: "Bounded".to_string(),
        rust_path: "test_lib::Bounded".to_string(),
        variants: vec![variant("Limit", vec![max_field])],
        serde_content: None,
        serde_tag: Some("type".to_string()),
        ..Default::default()
    };
    let methods = gen_extendr_enum_variant_constructors(&def, &ExtendrBackend, "test_lib::Bounded");
    let code = methods.join("\n");
    assert!(
        code.contains("pub fn _factory_limit(max: Option<f64>) -> Bounded"),
        "{code}"
    );
    assert!(
        code.contains("test_lib::Bounded::Limit { max: max.map(|v| v as u64) }.into()"),
        "{code}"
    );
}

#[test]
fn registrations_pair_r_name_with_factory_fn() {
    let regs = extendr_enum_variant_constructor_registrations(&shape_enum());
    assert_eq!(
        regs,
        vec![
            (
                "circle".to_string(),
                "_factory_circle".to_string(),
                vec!["radius".to_string()]
            ),
            (
                "rect".to_string(),
                "_factory_rect".to_string(),
                vec!["width".to_string(), "height".to_string()]
            ),
        ]
    );
}

/// The R wrapper registration list must stay in lockstep with the generated `#[extendr]`
/// constructors: a colliding hand-written method must not drop the variant from either.
#[test]
fn registrations_include_variant_colliding_with_hand_written_method() {
    let def = EnumDef {
        methods: vec![MethodDef {
            name: "circle".to_string(),
            is_static: true,
            ..Default::default()
        }],
        ..shape_enum()
    };
    let regs = extendr_enum_variant_constructor_registrations(&def);
    assert_eq!(
        regs,
        vec![
            (
                "circle".to_string(),
                "_factory_circle".to_string(),
                vec!["radius".to_string()]
            ),
            (
                "rect".to_string(),
                "_factory_rect".to_string(),
                vec!["width".to_string(), "height".to_string()]
            ),
        ]
    );
}

#[test]
fn r_wrapper_binds_variant_constructor_under_snake_name() {
    use crate::core::backend::Backend;

    let backend = ExtendrBackend;
    let config = super::make_config();
    let mut api = super::make_api_surface();
    api.enums = vec![shape_enum()];

    let files = backend.generate_public_api(&api, &config).unwrap();
    let wrappers = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with("extendr-wrappers.R"))
        .expect("extendr-wrappers.R must be generated");
    let content = &wrappers.content;

    assert!(
        content.contains("Shape$circle <- function(radius)"),
        "variant ctor must bind under the bare snake name: {content}"
    );
    assert!(
        content.contains(".Call(\"wrap__Shape___factory_circle\", radius"),
        "variant ctor must call the _factory_ symbol: {content}"
    );
    assert!(content.contains("Shape$rect <- function(width, height)"), "{content}");
    assert!(
        content.contains(".Call(\"wrap__Shape___factory_rect\", width, height"),
        "{content}"
    );
}

/// The discriminator field name a flat data enum's struct declares (`gen_extendr_flat_data_enum_struct`)
/// and the field its `From<core>`/`From<binding>` impls populate and read
/// (`gen_extendr_flat_data_enum_from_core`/`gen_extendr_flat_data_enum_to_core`) must be the exact
/// same string, whether it comes from the generic `"type"` fallback (`flat_data_enum_discriminator`
/// in `gen_bindings/bridges/mod.rs`) or an explicit `serde_tag`. All three call sites read that one
/// function instead of hard-coding the fallback independently, so a regression that reintroduces a
/// second hard-coded literal would surface here as a field name mismatch. ~keep
#[test]
fn flat_data_enum_discriminator_is_consistent_across_struct_and_from_impls() {
    let backend = ExtendrBackend;
    let lossy_skip_types: Vec<String> = vec![];
    let cfg = ExtendrBackend::binding_config("test_lib", &lossy_skip_types);

    let default_enum = EnumDef {
        name: "Payload".to_string(),
        rust_path: "test_lib::Payload".to_string(),
        variants: vec![variant("Text", vec![field("inner", TypeRef::String)])],
        serde_tag: None,
        ..Default::default()
    };

    let struct_src = gen_extendr_flat_data_enum_struct(&default_enum, &backend, &cfg);
    assert!(
        struct_src.contains("pub r#type: String"),
        "no explicit serde_tag should fall back to the generic `type` discriminator, not a \
         domain-specific default; got:\n{struct_src}"
    );

    let from_core = gen_extendr_flat_data_enum_from_core(&default_enum, "test_lib");
    assert!(
        from_core.contains("r#type: \"Text\".to_string()"),
        "From<core> impl must populate the exact field the struct declares; got:\n{from_core}"
    );

    let to_core = gen_extendr_flat_data_enum_to_core(&default_enum, "test_lib");
    assert!(
        to_core.contains("val.r#type.as_str()"),
        "From<binding> impl must dispatch on the exact field the struct declares; got:\n{to_core}"
    );

    let tagged_enum = EnumDef {
        serde_tag: Some("kind".to_string()),
        ..default_enum
    };
    let tagged_struct_src = gen_extendr_flat_data_enum_struct(&tagged_enum, &backend, &cfg);
    assert!(
        tagged_struct_src.contains("pub kind: String"),
        "an explicit serde_tag must be used verbatim as the discriminator field name; got:\n{tagged_struct_src}"
    );
    let tagged_from_core = gen_extendr_flat_data_enum_from_core(&tagged_enum, "test_lib");
    assert!(
        tagged_from_core.contains("kind: \"Text\".to_string()"),
        "an explicit serde_tag must thread into the From<core> impl too; got:\n{tagged_from_core}"
    );
}