alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::{test_method, values};
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
use anyhow::{Result, bail};
use heck::ToUpperCamelCase;

pub(super) fn render(
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
    enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
    let call = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    let expects_error = fixture
        .assertions
        .iter()
        .any(|assertion| assertion.assertion_type == "error");
    if call.args.iter().any(|argument| argument.arg_type == "test_backend") {
        bail!(
            "swift snippet `{}` requires test-backend lifecycle teardown",
            fixture.id
        );
    }

    let package = e2e_config
        .resolve_package("swift")
        .and_then(|package| package.name)
        .unwrap_or_else(|| config.name.to_upper_camel_case());
    let module = package.to_upper_camel_case();
    let first_class_map = values::build_swift_first_class_map(type_defs, enums, e2e_config, call);
    let override_config = call.overrides.get("swift");
    let mut call_fixture = fixture.clone();
    if !expects_error {
        call_fixture.assertions.clear();
    }
    let mut method = String::new();
    test_method::render_test_method(
        &mut method,
        &call_fixture,
        e2e_config,
        "",
        "",
        &[],
        false,
        override_config.and_then(|value| value.client_factory.as_deref()),
        &first_class_map,
        &module,
        config,
        type_defs,
        enums,
    );
    let body_line_count = method.lines().count().saturating_sub(3);
    let body = method
        .lines()
        .skip(2)
        .take(body_line_count)
        .map(|line| line.strip_prefix("        ").unwrap_or(line))
        .map(|line| line.replacen("let  =", "_ =", 1))
        .map(|line| {
            line.replace(
                "XCTFail(\"expected to throw\")",
                "fatalError(\"expected call to fail\")",
            )
        })
        .map(|line| line.replace("// success", "print(\"Call failed as expected: \\(error)\")"))
        .collect::<Vec<_>>()
        .join("\n");
    let needs_foundation = ["Data(", "URL(", "JSONDecoder", "JSONEncoder"]
        .iter()
        .any(|symbol| body.contains(symbol));
    Ok(crate::e2e::template_env::render(
        "swift/snippet_body.jinja",
        minijinja::context! { module => module, body => body, needs_foundation => needs_foundation },
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn snippet_reuses_typed_call_without_xctest_harness() {
        let fixture = Fixture {
            id: "count".into(),
            description: "Count".into(),
            ..Fixture::default()
        };
        let mut e2e = E2eConfig::default();
        e2e.call.function = "count_items".into();
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            ..ResolvedCrateConfig::default()
        };
        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("snippet renders");
        assert!(!rendered.contains("import RustBridge"));
        assert!(!rendered.contains("import Foundation"));
        assert!(rendered.contains("_ = try "));
        assert!(rendered.contains(".countItems()"));
        assert!(!rendered.contains("XCTest"));
    }

    #[test]
    fn expected_error_snippet_uses_native_do_catch() {
        let mut fixture = Fixture {
            id: "invalid".into(),
            description: "Invalid".into(),
            ..Fixture::default()
        };
        fixture.assertions.push(crate::e2e::fixture::Assertion {
            assertion_type: "error".into(),
            ..Default::default()
        });
        let mut e2e = E2eConfig::default();
        e2e.call.function = "parse".into();
        let rendered = render(&fixture, &e2e, &ResolvedCrateConfig::default(), &[], &[]).expect("snippet renders");
        assert!(rendered.contains("do {"));
        assert!(rendered.contains("catch {"));
        assert!(rendered.contains("Call failed as expected"));
        assert!(!rendered.contains("XCTFail"));
    }

    #[test]
    fn visitor_snippet_reuses_native_bridge_setup() {
        let mut fixture = Fixture {
            id: "custom_text".into(),
            description: "Custom text".into(),
            input: serde_json::json!({ "html": "<p>Hello</p>" }),
            ..Fixture::default()
        };
        fixture.visitor = Some(crate::e2e::fixture::VisitorSpec {
            callbacks: [("visit_text".into(), crate::e2e::fixture::CallbackAction::Continue)].into(),
        });
        let mut e2e = E2eConfig::default();
        e2e.call.function = "render_document".into();
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            trait_bridges: vec![crate::core::config::TraitBridgeConfig {
                trait_name: "DocumentVisitor".into(),
                type_alias: Some("VisitorHandle".into()),
                options_type: Some("RenderOptions".into()),
                options_field: Some("visitor".into()),
                ..Default::default()
            }],
            ..Default::default()
        };

        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("visitor snippet renders");
        assert!(rendered.contains("class LocalVisitor_CustomText"));
        assert!(rendered.contains("renderDocument"));
        assert!(!rendered.contains("XCTest"));
    }

    #[test]
    fn streaming_snippet_reuses_async_call_preparation() {
        let fixture = Fixture {
            id: "stream_items".into(),
            description: "Stream items".into(),
            ..Fixture::default()
        };
        let mut e2e = E2eConfig::default();
        e2e.call.function = "stream_items".into();
        e2e.call.streaming = Some(crate::core::config::e2e::StreamingConfig::Enabled(true));
        let config = ResolvedCrateConfig {
            name: "sample".into(),
            adapters: vec![
                serde_json::from_value(serde_json::json!({
                    "name": "stream_items",
                    "pattern": "streaming",
                    "core_path": "sample::stream_items",
                    "item_type": "StreamItem"
                }))
                .expect("streaming adapter config"),
            ],
            ..ResolvedCrateConfig::default()
        };

        let rendered = render(&fixture, &e2e, &config, &[], &[]).expect("streaming snippet renders");

        assert!(rendered.contains("streamItems"), "{rendered}");
        assert!(rendered.contains("for try await"), "{rendered}");
        assert!(!rendered.contains("XCTest"));
    }
}