alef 0.23.33

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Go argument and setup rendering.

use crate::e2e::escape::go_string_literal;

use super::json_values::{convert_json_for_go, element_type_to_go_slice, json_to_go};
use super::test_backend::emit_test_backend_with_context;

pub(super) fn resolve_handle_config_type(
    arg: &crate::e2e::config::ArgMapping,
    options_type: Option<&str>,
    type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
    if arg.arg_type != "handle" {
        return None;
    }
    options_type.map(str::to_string).or_else(|| {
        let candidate = format!("{}Config", arg.name.to_uppercase_first());
        type_defs.iter().any(|ty| ty.name == candidate).then_some(candidate)
    })
}

trait UppercaseFirst {
    fn to_uppercase_first(&self) -> String;
}

impl UppercaseFirst for str {
    fn to_uppercase_first(&self) -> String {
        let mut chars = self.chars();
        match chars.next() {
            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
            None => String::new(),
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub(super) fn build_args_and_setup(
    input: &serde_json::Value,
    args: &[crate::e2e::config::ArgMapping],
    import_alias: &str,
    options_type: Option<&str>,
    fixture: &crate::e2e::fixture::Fixture,
    options_ptr: bool,
    expects_error: bool,
    data_enum_names: &std::collections::HashSet<&str>,
    config: &crate::core::config::ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
    enums: &[crate::core::ir::EnumDef],
) -> (Vec<String>, Vec<String>, String) {
    let fixture_id = &fixture.id;
    use heck::ToUpperCamelCase;

    if args.is_empty() {
        return (Vec::new(), Vec::new(), String::new());
    }

    let mut package_decls: Vec<String> = Vec::new();
    let mut setup_lines: Vec<String> = Vec::new();
    let mut parts: Vec<String> = Vec::new();

    for arg in args {
        if arg.arg_type == "mock_url" {
            if fixture.has_host_root_route() {
                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
                setup_lines.push(format!(
                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
                    arg.name, arg.name
                ));
            } else {
                setup_lines.push(format!(
                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
                    arg.name,
                ));
            }
            parts.push(arg.name.clone());
            continue;
        }

        if arg.arg_type == "mock_url_list" {
            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let val = input.get(field).unwrap_or(&serde_json::Value::Null);

            let paths: Vec<String> = if let Some(arr) = val.as_array() {
                arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
            } else {
                Vec::new()
            };

            let paths_literal = paths.join(", ");
            let var_name = &arg.name;

            setup_lines.push(format!(
                "{var_name}Base := os.Getenv(\"{env_key}\")\n\tif {var_name}Base == \"\" {{\n\t\t{var_name}Base = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
            ));
            setup_lines.push(format!(
                "var {var_name} []string\n\tfor _, p := range []string{{{paths_literal}}} {{\n\t\tif strings.HasPrefix(p, \"http\") {{\n\t\t\t{var_name} = append({var_name}, p)\n\t\t}} else {{\n\t\t\t{var_name} = append({var_name}, {var_name}Base + p)\n\t\t}}\n\t}}"
            ));
            parts.push(var_name.to_string());
            continue;
        }

        if arg.arg_type == "test_backend" {
            if let Some(trait_name) = &arg.trait_name {
                if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
                    let mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
                        .iter()
                        .find(|t| t.name == *trait_name)
                        .map(|t| t.methods.iter().collect())
                        .unwrap_or_default();

                    if let Some(super_trait) = &trait_bridge.super_trait {
                        if let Some(super_type) = type_defs.iter().find(|t| &t.rust_path == super_trait) {
                            for method in &super_type.methods {
                                if !methods.iter().any(|m| m.name == method.name) {
                                    methods.push(method);
                                }
                            }
                        }
                    }

                    let excluded_named =
                        crate::e2e::codegen::recipe::trait_bridge_excluded_type_names(config, type_defs, &methods);
                    let enum_names: std::collections::HashSet<&str> = enums.iter().map(|e| e.name.as_str()).collect();
                    let emission = emit_test_backend_with_context(
                        trait_bridge,
                        &methods,
                        fixture,
                        &excluded_named,
                        import_alias,
                        &enum_names,
                    );
                    package_decls.push(emission.setup_block);
                    parts.push(emission.arg_expr);
                    continue;
                }
            }
            let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("go");
            setup_lines.push(format!("// {}", emission.arg_expr));
            parts.push("nil".to_string());
            continue;
        }

        if arg.arg_type == "handle" {
            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
            let create_err_handler = if expects_error {
                "assert.Error(t, createErr)\n\t\treturn".to_string()
            } else {
                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
            };
            if config_value.is_null()
                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
            {
                setup_lines.push(format!(
                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
                    name = arg.name,
                ));
            } else {
                let json_str = serde_json::to_string(config_value).unwrap_or_default();
                let go_literal = go_string_literal(&json_str);
                let name = &arg.name;
                if let Some(config_type) = resolve_handle_config_type(arg, options_type, type_defs) {
                    setup_lines.push(format!(
                        "var {name}Config {import_alias}.{config_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                    ));
                    setup_lines.push(format!(
                        "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
                    ));
                } else {
                    setup_lines.push(format!(
                        "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
                    ));
                }
            }
            parts.push(arg.name.clone());
            continue;
        }

        let val: Option<&serde_json::Value> = if arg.field == "input" {
            Some(input)
        } else {
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            input.get(field)
        };

        if arg.arg_type == "bytes" {
            let var_name = format!("{}Bytes", arg.name);
            match val {
                None | Some(serde_json::Value::Null) => {
                    if arg.optional {
                        parts.push("nil".to_string());
                    } else {
                        parts.push("[]byte{}".to_string());
                    }
                }
                Some(serde_json::Value::String(s)) => {
                    let go_path = go_string_literal(s);
                    setup_lines.push(format!(
                        "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
                    ));
                    parts.push(var_name);
                }
                Some(other) => {
                    parts.push(format!("[]byte({})", json_to_go(other)));
                }
            }
            continue;
        }

        match val {
            None | Some(serde_json::Value::Null) if arg.optional => match arg.arg_type.as_str() {
                "string" => {
                    parts.push("nil".to_string());
                }
                "json_object" => {
                    if options_ptr {
                        parts.push("nil".to_string());
                    } else if let Some(opts_type) = options_type {
                        parts.push(format!("{import_alias}.{opts_type}{{}}"));
                    } else {
                        parts.push("nil".to_string());
                    }
                }
                _ => {
                    parts.push("nil".to_string());
                }
            },
            None | Some(serde_json::Value::Null) => {
                let default_val = match arg.arg_type.as_str() {
                    "string" => "\"\"".to_string(),
                    "int" | "integer" | "i64" => "0".to_string(),
                    "float" | "number" => "0.0".to_string(),
                    "bool" | "boolean" => "false".to_string(),
                    "json_object" => {
                        if options_ptr {
                            "nil".to_string()
                        } else if let Some(opts_type) = options_type {
                            format!("{import_alias}.{opts_type}{{}}")
                        } else {
                            "nil".to_string()
                        }
                    }
                    _ => "nil".to_string(),
                };
                parts.push(default_val);
            }
            Some(v) => match arg.arg_type.as_str() {
                "json_object" => {
                    let is_array = v.is_array();
                    let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
                    if is_empty_obj {
                        if options_ptr {
                            parts.push("nil".to_string());
                        } else if let Some(opts_type) = options_type {
                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
                        } else {
                            parts.push("nil".to_string());
                        }
                    } else if is_array {
                        let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
                            if go_t.starts_with('[') {
                                go_t.to_string()
                            } else {
                                let qualified = if go_t.contains('.') {
                                    go_t.to_string()
                                } else {
                                    format!("{import_alias}.{go_t}")
                                };
                                format!("[]{qualified}")
                            }
                        } else {
                            element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
                        };

                        let element_type_name = if let Some(go_t) = arg.go_type.as_deref() {
                            if go_t.starts_with('[') {
                                None
                            } else if let Some(idx) = go_t.rfind('.') {
                                Some(&go_t[idx + 1..])
                            } else {
                                Some(go_t)
                            }
                        } else {
                            arg.element_type.as_deref()
                        };

                        let is_sum_type = element_type_name.is_some_and(|et| data_enum_names.contains(et));
                        let converted_v = convert_json_for_go(v.clone());
                        let var_name = &arg.name;

                        if is_sum_type {
                            let element_type = element_type_name.unwrap();
                            let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
                            let go_literal = go_string_literal(&json_str);
                            setup_lines.push(format!(
                                "var {var_name}Raw []json.RawMessage\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}Raw); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                            ));
                            setup_lines.push(format!(
                                "var {var_name} {go_slice_type}\n\tfor _, raw := range {var_name}Raw {{\n\t\telem, err := {import_alias}.Unmarshal{element_type}(raw)\n\t\tif err != nil {{\n\t\t\tt.Fatalf(\"unmarshal {element_type} failed: %v\", err)\n\t\t}}\n\t\t{var_name} = append({var_name}, elem)\n\t}}"
                            ));
                        } else {
                            let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
                            let go_literal = go_string_literal(&json_str);
                            setup_lines.push(format!(
                                "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                            ));
                        }
                        parts.push(var_name.to_string());
                    } else if let Some(opts_type) = options_type {
                        let remapped_v = if options_ptr {
                            convert_json_for_go(v.clone())
                        } else {
                            v.clone()
                        };
                        let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
                        let go_literal = go_string_literal(&json_str);
                        let var_name = &arg.name;
                        setup_lines.push(format!(
                            "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                        ));
                        let arg_expr = if options_ptr {
                            format!("&{var_name}")
                        } else {
                            var_name.to_string()
                        };
                        parts.push(arg_expr);
                    } else {
                        parts.push(json_to_go(v));
                    }
                }
                "string" if arg.optional => {
                    let var_name = format!("{}Val", arg.name);
                    let go_val = json_to_go(v);
                    setup_lines.push(format!("{var_name} := {go_val}"));
                    parts.push(format!("&{var_name}"));
                }
                _ => {
                    parts.push(json_to_go(v));
                }
            },
        }
    }

    (package_decls, setup_lines, parts.join(", "))
}