alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::codegen::naming::{go_free_function_name, go_type_name, to_go_name};
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, TypeDef};
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;

use super::setup::build_args_and_setup;

pub(super) fn render_snippet_body(
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[TypeDef],
    enums: &[EnumDef],
) -> String {
    let lang = "go";
    let mut call = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    call = crate::e2e::codegen::select_best_matching_call(call, e2e_config, fixture);
    let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call, type_defs);
    let override_config = recipe.override_config;
    let import_alias = override_config
        .and_then(|value| value.alias.as_deref())
        .or_else(|| {
            e2e_config
                .call
                .overrides
                .get(lang)
                .and_then(|value| value.alias.as_deref())
        })
        .unwrap_or("pkg");
    let module = override_config
        .and_then(|value| value.module.as_deref())
        .or_else(|| {
            e2e_config
                .call
                .overrides
                .get(lang)
                .and_then(|value| value.module.as_deref())
        })
        .or_else(|| config.go.as_ref().and_then(|value| value.module.as_deref()))
        .unwrap_or(&call.module);
    let reserved_type_names: std::collections::HashSet<String> = type_defs
        .iter()
        .filter(|value| !value.is_trait)
        .map(|value| go_type_name(&value.name))
        .chain(enums.iter().map(|value| go_type_name(&value.name)))
        .collect();
    let base_function = override_config
        .and_then(|value| value.function.as_deref())
        .unwrap_or(&call.function);
    let function_name = go_free_function_name(base_function, &reserved_type_names);
    let data_enum_names: std::collections::HashSet<&str> = enums
        .iter()
        .filter(|value| {
            value
                .variants
                .iter()
                .any(|variant| variant.fields.iter().any(|field| !field.name.is_empty()))
        })
        .map(|value| value.name.as_str())
        .collect();
    let options_ptr = override_config.map(|value| value.options_ptr).unwrap_or(false);
    let options_type = recipe.options_type.or_else(|| {
        e2e_config
            .call
            .overrides
            .get(lang)
            .and_then(|value| value.options_type.as_deref())
    });
    let (package_decls, setup_lines, mut args) = build_args_and_setup(
        &fixture.input,
        recipe.args,
        import_alias,
        options_type,
        fixture,
        options_ptr,
        false,
        &data_enum_names,
        config,
        type_defs,
        enums,
        true,
    );
    if !recipe.extra_args.is_empty() {
        let extras = recipe.extra_args.join(", ");
        args = if args.is_empty() {
            extras
        } else {
            format!("{args}, {extras}")
        };
    }
    let client_factory = override_config
        .and_then(|value| value.client_factory.as_deref())
        .or_else(|| {
            e2e_config
                .call
                .overrides
                .get(lang)
                .and_then(|value| value.client_factory.as_deref())
        });
    let (call_prefix, client_setup) = if let Some(factory) = client_factory {
        (
            "client".to_string(),
            format!(
                "\tclient, clientErr := {import_alias}.{}(\"your-api-key\", nil, nil, nil, nil)\n\tif clientErr != nil {{\n\t\tpanic(clientErr)\n\t}}",
                to_go_name(factory),
            ),
        )
    } else {
        (import_alias.to_string(), String::new())
    };
    let call_expr = format!("{call_prefix}.{function_name}({args})");
    let returns_error = override_config
        .and_then(|value| value.returns_result)
        .unwrap_or(call.returns_result)
        || recipe
            .args
            .iter()
            .any(|arg| matches!(arg.arg_type.as_str(), "json_object" | "bytes"))
        || client_factory.is_some();
    let mut standard_imports = std::collections::BTreeSet::new();
    let setup_lines: Vec<String> = setup_lines.into_iter().map(snippet_setup_line).collect();
    let joined_setup = setup_lines.join("\n");
    let joined_declarations = package_decls.join("\n");
    if joined_setup.contains("os.") || joined_declarations.contains("os.") {
        standard_imports.insert("os");
    }
    if joined_setup.contains("json.") {
        standard_imports.insert("encoding/json");
    }
    if joined_setup.contains("strings.") {
        standard_imports.insert("strings");
    }
    if !call.returns_void || joined_setup.contains("fmt.") {
        standard_imports.insert("fmt");
    }
    let mut imports = standard_imports
        .into_iter()
        .map(|path| (path.to_string(), String::new()))
        .collect::<Vec<_>>();
    imports.push((module.to_string(), import_alias.to_string()));
    imports.sort_by(|left, right| left.0.cmp(&right.0));
    let imports = imports
        .into_iter()
        .map(|(path, alias)| minijinja::context! { path => path, alias => alias })
        .collect::<Vec<_>>();

    crate::e2e::template_env::render(
        "go/snippet_body.jinja",
        minijinja::context! {
            imports => imports,
            package_decls => package_decls, setup_lines => setup_lines, client_setup => client_setup,
            call_expr => call_expr, result_var => call.result_var, returns_error => returns_error,
            returns_void => call.returns_void,
        },
    )
    .trim_end()
    .to_string()
}

fn snippet_setup_line(line: String) -> String {
    line.lines()
        .map(|part| {
            if part.contains("t.Fatalf(") {
                format!("{})", part.replace("t.Fatalf(", "panic(fmt.Sprintf("))
            } else {
                part.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::e2e::config::{CallConfig, CallOverride};

    fn fixture() -> Fixture {
        Fixture {
            docs: None,
            requirements: Vec::new(),
            id: "quick_start".to_string(),
            category: None,
            description: "Quick start".to_string(),
            tags: Vec::new(),
            skip: None,
            env: None,
            setup: Vec::new(),
            call: None,
            input: serde_json::Value::Null,
            mock_response: None,
            source: String::new(),
            http: None,
            asyncapi: None,
            websocket: None,
            assertions: Vec::new(),
            visitor: None,
            args: Vec::new(),
            assertion_recipes: Vec::new(),
        }
    }

    #[test]
    fn snippet_reuses_the_test_call_shape_without_test_harness() {
        let e2e = E2eConfig {
            call: CallConfig {
                function: "load_document".to_string(),
                module: "github.com/example/library".to_string(),
                result_var: "document".to_string(),
                returns_result: true,
                ..CallConfig::default()
            },
            ..E2eConfig::default()
        };
        let body = render_snippet_body(&fixture(), &e2e, &ResolvedCrateConfig::default(), &[], &[]);

        assert!(body.contains("pkg \"github.com/example/library\""));
        let fmt_position = body.find("\"fmt\"").expect("fmt import");
        let package_position = body.find("pkg \"").expect("binding import");
        assert!(fmt_position < package_position, "{body}");
        assert!(body.contains("document, err := pkg.LoadDocument()"));
        assert!(!body.contains("testing"));
        assert!(!body.contains("assert."));
    }

    #[test]
    fn snippet_replaces_testing_failures_in_typed_setup() {
        assert_eq!(
            snippet_setup_line("if err != nil {\n\tt.Fatalf(\"decode: %v\", err)\n}".into()),
            "if err != nil {\n\tpanic(fmt.Sprintf(\"decode: %v\", err))\n}"
        );
    }

    #[test]
    fn void_snippet_does_not_import_fmt_when_it_is_unused() {
        let mut e2e = E2eConfig::default();
        e2e.call.function = "reset".into();
        e2e.call.module = "github.com/example/library".into();
        e2e.call.returns_void = true;

        let body = render_snippet_body(&fixture(), &e2e, &ResolvedCrateConfig::default(), &[], &[]);

        assert!(!body.contains("\"fmt\""), "{body}");
    }

    #[test]
    fn snippet_separates_package_and_import_declarations() {
        let mut e2e = E2eConfig::default();
        e2e.call.module = "example.com/sample".into();

        let body = render_snippet_body(&fixture(), &e2e, &ResolvedCrateConfig::default(), &[], &[]);

        assert!(body.starts_with("package main\n\nimport (\n"), "{body}");
        assert!(!body.contains("package main import"), "{body}");
    }

    #[test]
    fn snippet_matches_gofmt_when_available() {
        let mut e2e = E2eConfig::default();
        e2e.call.module = "example.com/sample".into();
        e2e.call.function = "process".into();
        e2e.call.result_var = "result".into();
        e2e.call.returns_result = true;
        let body = render_snippet_body(&fixture(), &e2e, &ResolvedCrateConfig::default(), &[], &[]);
        let Ok(mut child) = std::process::Command::new("gofmt")
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .spawn()
        else {
            assert!(body.contains("\tresult, err := pkg.Process()"), "{body}");
            return;
        };
        use std::io::Write as _;
        child
            .stdin
            .take()
            .expect("gofmt stdin")
            .write_all(body.as_bytes())
            .expect("write Go snippet");
        let output = child.wait_with_output().expect("wait for gofmt");
        assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
        assert_eq!(
            String::from_utf8(output.stdout)
                .expect("gofmt output is UTF-8")
                .trim_end(),
            body
        );
    }

    #[test]
    fn snippet_constructs_known_dto_without_json_round_trip() {
        let mut fixture = fixture();
        fixture.input = serde_json::json!({"payload": {"kind": "active", "label": "sample"}});
        let mut e2e = E2eConfig::default();
        e2e.call.module = "example.com/sample".into();
        e2e.call.function = "process".into();
        e2e.call.result_var = "result".into();
        e2e.call.args = [
            ("payload", "input.payload", Some("SampleInput")),
            ("config", "input.config", None),
        ]
        .into_iter()
        .map(|(name, field, element_type)| crate::e2e::config::ArgMapping {
            name: name.into(),
            field: field.into(),
            arg_type: "json_object".into(),
            optional: false,
            owned: false,
            element_type: element_type.map(str::to_string),
            go_type: None,
            vec_inner_is_ref: false,
            trait_name: None,
        })
        .collect();
        e2e.call.overrides.insert(
            "go".into(),
            CallOverride {
                options_ptr: true,
                options_type: Some("SampleConfig".into()),
                ..CallOverride::default()
            },
        );
        let body = render_snippet_body(
            &fixture,
            &e2e,
            &ResolvedCrateConfig::default(),
            &[
                TypeDef {
                    name: "SampleInput".into(),
                    fields: vec![
                        crate::core::ir::FieldDef {
                            name: "kind".into(),
                            ty: crate::core::ir::TypeRef::Named("SampleKind".into()),
                            default: Some("active".into()),
                            ..Default::default()
                        },
                        crate::core::ir::FieldDef {
                            name: "label".into(),
                            ty: crate::core::ir::TypeRef::String,
                            ..Default::default()
                        },
                    ],
                    ..TypeDef::default()
                },
                TypeDef {
                    name: "SampleConfig".into(),
                    ..TypeDef::default()
                },
            ],
            &[],
        );

        assert!(
            body.contains(
                "payload := pkg.SampleInput{\n\t\tKind:  ptr(pkg.SampleKind(`active`)),\n\t\tLabel: `sample`,"
            ),
            "{body}"
        );
        assert!(body.contains("config := pkg.SampleConfig{}"), "{body}");
        assert!(body.contains("pkg.Process(payload, config)"), "{body}");
        assert!(!body.contains("pkg.Process(payload, nil)"), "{body}");
        assert!(!body.contains("json.Unmarshal"), "{body}");
        assert!(!body.contains("encoding/json"), "{body}");
    }
}