use crate::e2e::escape::go_string_literal;
use super::json_values::{convert_json_for_go, element_type_to_go_slice, json_to_go, json_to_go_yields_string_literal};
use super::test_backend::emit_test_backend_with_context;
fn json_object_go_type<'a>(arg: &'a crate::e2e::config::ArgMapping, options_type: Option<&'a str>) -> Option<&'a str> {
arg.go_type.as_deref().or(arg.element_type.as_deref()).or(options_type)
}
fn qualified_go_type(import_alias: &str, type_name: &str) -> String {
if type_name.contains('.') {
type_name.to_string()
} else {
format!("{import_alias}.{}", crate::codegen::naming::go_type_name(type_name))
}
}
fn go_enum_shape(
enums: &[crate::core::ir::EnumDef],
type_name: &str,
) -> Option<crate::backends::go::GoEnumRepresentation> {
enums
.iter()
.find(|candidate| candidate.name == type_name)
.map(crate::backends::go::go_enum_representation)
}
fn go_empty_value_expression(import_alias: &str, type_name: &str, enums: &[crate::core::ir::EnumDef]) -> String {
match go_enum_shape(enums, type_name) {
Some(crate::backends::go::GoEnumRepresentation::DataInterface) => "nil".to_string(),
Some(
crate::backends::go::GoEnumRepresentation::UnitString
| crate::backends::go::GoEnumRepresentation::NewtypeTupleString,
) => "\"\"".to_string(),
_ => format!("{}{{}}", qualified_go_type(import_alias, type_name)),
}
}
fn ensure_value_helpers(package_decls: &mut Vec<String>, literal: &str) {
if literal.contains("ptr(")
&& !package_decls
.iter()
.any(|declaration| declaration.starts_with("func ptr["))
{
package_decls.push("func ptr[T any](value T) *T { return &value }".to_string());
}
if literal.contains("mustReadFile(")
&& !package_decls
.iter()
.any(|declaration| declaration.starts_with("func mustReadFile("))
{
package_decls.push(
crate::e2e::template_env::render("go/read_file_helper.jinja", minijinja::context! {})
.trim_end()
.to_string(),
);
}
}
const MAX_DIAGNOSTIC_VALUE_CHARS: usize = 80;
fn named_field_type_mismatch(
owner_type: &str,
field_name: &str,
go_type: &str,
representation: crate::backends::go::GoEnumRepresentation,
rendered: &str,
) -> anyhow::Error {
let quoted: String = rendered.chars().take(MAX_DIAGNOSTIC_VALUE_CHARS).collect();
let elided = if quoted.len() < rendered.len() { "..." } else { "" };
let declaration = representation.go_declaration();
anyhow::anyhow!(
"e2e go codegen: field `{field_name}` of `{owner_type}` is declared as the IR enum `{go_type}`, which the \
Go binding backend emits as `type {go_type} {declaration}`. The fixture value lowers to the Go \
expression {quoted}{elided}, and Go converts an untyped string constant only to a defined type whose \
underlying type is `string` or `[]byte` — `{go_type}({quoted}{elided})` is a `cannot convert` compile \
error. Alef will not publish a snippet that does not compile: give the fixture a value matching one of \
`{go_type}`'s variants, or record a `docs.coverage_exceptions` entry for go."
)
}
#[derive(Clone, Copy)]
pub(super) struct GoValueContext<'a> {
pub import_alias: &'a str,
pub type_defs: &'a [crate::core::ir::TypeDef],
pub enums: &'a [crate::core::ir::EnumDef],
pub files: &'a [crate::e2e::fixture::FixtureDocsFileInput],
}
#[derive(Clone, Copy)]
pub(super) struct GoFieldSite<'a> {
pub owner_type: &'a str,
pub field_name: &'a str,
pub pointer: &'a str,
}
fn go_named_scalar_expression(
value: &serde_json::Value,
type_name: &str,
context: GoValueContext<'_>,
site: GoFieldSite<'_>,
) -> anyhow::Result<String> {
let go_type = crate::codegen::naming::go_type_name(type_name);
let conversion = format!("{}.{go_type}({})", context.import_alias, json_to_go(value));
let Some(enum_def) = context.enums.iter().find(|candidate| candidate.name == type_name) else {
return Ok(conversion);
};
if let Some(wire_value) = value.as_str()
&& let Some(constant) = crate::backends::go::go_enum_constant_for_wire_value(enum_def, wire_value)
{
return Ok(format!("{}.{constant}", context.import_alias));
}
let representation = crate::backends::go::go_enum_representation(enum_def);
if representation.accepts_string_conversion() && json_to_go_yields_string_literal(value) {
return Ok(conversion);
}
if let Some(constructed) = super::enum_literals::go_enum_value_expression(value, enum_def, context, site)? {
return Ok(constructed);
}
Err(named_field_type_mismatch(
site.owner_type,
site.field_name,
&go_type,
representation,
&json_to_go(value),
))
}
pub(super) fn go_named_field_expression(
value: &serde_json::Value,
type_name: &str,
context: GoValueContext<'_>,
site: GoFieldSite<'_>,
uses_pointer: bool,
) -> anyhow::Result<String> {
if let Some(nested) = native_go_dto_literal_at(value, type_name, context, site.pointer)? {
return Ok(if uses_pointer { format!("&{nested}") } else { nested });
}
let literal = go_named_scalar_expression(value, type_name, context, site)?;
Ok(if uses_pointer {
format!("ptr({literal})")
} else {
literal
})
}
pub(super) fn go_struct_field_expression(
field: &crate::core::ir::FieldDef,
value: &serde_json::Value,
context: GoValueContext<'_>,
site: GoFieldSite<'_>,
uses_pointer: bool,
) -> anyhow::Result<Option<String>> {
let inner = match &field.ty {
crate::core::ir::TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
if context.files.iter().any(|file| file.field == site.pointer) && matches!(inner, crate::core::ir::TypeRef::Bytes) {
let Some(path) = value.as_str() else {
return Ok(None);
};
return Ok(Some(format!("mustReadFile({})", go_string_literal(path))));
}
let expression = match inner {
crate::core::ir::TypeRef::String | crate::core::ir::TypeRef::Path => {
let Some(literal) = value.as_str().map(go_string_literal) else {
return Ok(None);
};
if uses_pointer {
format!("ptr({literal})")
} else {
literal
}
}
crate::core::ir::TypeRef::Bytes => {
let Some(array) = value.as_array() else {
return Ok(None);
};
let items = array.iter().filter_map(serde_json::Value::as_u64).collect::<Vec<_>>();
format!(
"[]byte{{{}}}",
items.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
)
}
crate::core::ir::TypeRef::Named(name) => go_named_field_expression(value, name, context, site, uses_pointer)?,
crate::core::ir::TypeRef::Primitive(primitive) => {
let literal = json_to_go(value);
if uses_pointer {
if matches!(primitive, crate::core::ir::PrimitiveType::Bool) {
format!("ptr({literal})")
} else {
let go_primitive_type = crate::backends::go::type_map::go_struct_field_type(inner);
format!("ptr({go_primitive_type}({literal}))")
}
} else {
literal
}
}
crate::core::ir::TypeRef::Json => {
if value.is_null() {
return Ok(None);
}
let literal = format!("json.RawMessage({})", go_string_literal(&value.to_string()));
if uses_pointer {
format!("ptr({literal})")
} else {
literal
}
}
_ => return Ok(None),
};
Ok(Some(expression))
}
fn typed_named_argument_expression(
value: &serde_json::Value,
type_name: &str,
context: GoValueContext<'_>,
arg_name: &str,
uses_pointer: bool,
) -> anyhow::Result<Option<String>> {
let is_struct = context
.type_defs
.iter()
.any(|definition| definition.name == type_name && !definition.is_opaque);
let is_enum = context.enums.iter().any(|candidate| candidate.name == type_name);
if !is_enum && !(is_struct && value.is_object()) {
return Ok(None);
}
let site = GoFieldSite {
owner_type: type_name,
field_name: arg_name,
pointer: "",
};
go_named_field_expression(value, type_name, context, site, uses_pointer).map(Some)
}
pub(super) fn native_go_dto_literal(
value: &serde_json::Value,
type_name: &str,
context: GoValueContext<'_>,
) -> anyhow::Result<Option<String>> {
native_go_dto_literal_at(value, type_name, context, "")
}
fn native_go_dto_literal_at(
value: &serde_json::Value,
type_name: &str,
context: GoValueContext<'_>,
pointer: &str,
) -> anyhow::Result<Option<String>> {
let Some(object) = value.as_object() else {
return Ok(None);
};
let Some(definition) = context.type_defs.iter().find(|definition| definition.name == type_name) else {
return Ok(None);
};
let struct_names: std::collections::HashSet<&str> = context
.type_defs
.iter()
.filter(|t| !t.is_opaque)
.map(|t| t.name.as_str())
.collect();
let field_values = definition
.fields
.iter()
.map(|field| -> anyhow::Result<Option<(String, String)>> {
let wire_name = crate::codegen::naming::wire_field_name(
&field.name,
field.serde_rename.as_deref(),
definition.serde_rename_all.as_deref(),
);
let Some(value) = object.get(&wire_name).or_else(|| object.get(&field.name)) else {
return Ok(None);
};
let field_pointer = format!("{pointer}/{}", field.name);
let uses_pointer = field.optional
|| (!field.optional && crate::backends::go::needs_omitempty_pointer(definition, field, &struct_names));
let site = GoFieldSite {
owner_type: &definition.name,
field_name: &field.name,
pointer: &field_pointer,
};
let Some(expression) = go_struct_field_expression(field, value, context, site, uses_pointer)? else {
return Ok(None);
};
Ok(Some((crate::codegen::naming::to_go_name(&field.name), expression)))
})
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let max_name_len = field_values
.iter()
.map(|(name, _)| name.len())
.max()
.unwrap_or_default();
let fields = field_values
.into_iter()
.map(|(name, expression)| {
let padding = " ".repeat(max_name_len.saturating_sub(name.len()));
minijinja::context! { name => name, padding => padding, expression => expression }
})
.collect::<Vec<_>>();
if fields.is_empty() {
return Ok(Some(
crate::e2e::template_env::render(
"go/empty_dto_literal.jinja",
minijinja::context! { type_name => qualified_go_type(context.import_alias, type_name) },
)
.trim_end()
.to_string(),
));
}
Ok(Some(
crate::e2e::template_env::render(
"go/dto_literal.jinja",
minijinja::context! {
type_name => qualified_go_type(context.import_alias, type_name), fields => fields,
},
)
.trim_end()
.to_string(),
))
}
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],
native_dtos: bool,
target: crate::e2e::codegen::call_ir::TargetParams<'_>,
) -> anyhow::Result<(Vec<String>, Vec<String>, String)> {
let fixture_id = &fixture.id;
use heck::ToUpperCamelCase;
if args.is_empty() {
return Ok((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_index, arg) in args.iter().enumerate() {
if arg.arg_type == "mock_url" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let value = input.get(field).unwrap_or(&serde_json::Value::Null);
if let Some(url) = crate::e2e::codegen::preserved_url_literal(fixture.preserve_input_urls, value) {
setup_lines.push(format!("{} := {}", arg.name, go_string_literal(url)));
} else 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 var_name = &arg.name;
if let Some(urls) = crate::e2e::codegen::preserved_url_list(fixture.preserve_input_urls, val) {
let literals: Vec<String> = urls.iter().map(|url| go_string_literal(url)).collect();
setup_lines.push(format!("{var_name} := []string{{{}}}", literals.join(", ")));
parts.push(var_name.to_string());
continue;
}
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(", ");
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
&& 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
&& 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,
enums,
);
package_decls.push(emission.setup_block);
parts.push(emission.arg_expr);
continue;
}
panic!(
"Go e2e generator: fixture `{}` declares a `test_backend` arg `{}` with trait `{:?}`, but either it has no `trait_name` configured or no `[[crates.trait_bridges]]` entry matches it; cannot generate a Go stub without a resolvable trait bridge",
fixture.id, arg.name, arg.trait_name
);
}
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.get("extract_input").unwrap_or(input))
} else {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
input.get(field)
};
let docs_files = fixture.docs_files_for_arg(&arg.field);
let value_context = GoValueContext {
import_alias,
type_defs,
enums,
files: &docs_files,
};
let declared_type_name = target.declared_type_name(&arg.name, arg_index);
let json_object_type = json_object_go_type(arg, options_type).or(declared_type_name);
let native_declared_type = if native_dtos { declared_type_name } else { None };
if native_dtos
&& arg.arg_type == "json_object"
&& val.is_none_or(serde_json::Value::is_null)
&& let Some(type_name) = json_object_type
&& let Some(literal) = native_go_dto_literal(
&serde_json::Value::Object(serde_json::Map::new()),
type_name,
value_context,
)?
{
setup_lines.push(format!("{} := {literal}", arg.name));
parts.push(if Some(type_name) == options_type && options_ptr {
format!("&{}", arg.name)
} else {
arg.name.clone()
});
continue;
}
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) = json_object_type {
parts.push(go_empty_value_expression(import_alias, opts_type, enums));
} 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) = json_object_type {
go_empty_value_expression(import_alias, opts_type, enums)
} 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 native_dtos
&& !is_array
&& let Some(opts_type) = json_object_type
&& let Some(literal) = native_go_dto_literal(v, opts_type, value_context)?
{
ensure_value_helpers(&mut package_decls, &literal);
setup_lines.push(format!("{} := {}", arg.name, literal.replace('\n', "\n\t")));
let arg_expr = if Some(opts_type) == options_type && options_ptr {
format!("&{}", arg.name)
} else {
arg.name.clone()
};
parts.push(arg_expr);
continue;
}
if is_empty_obj {
if options_ptr {
parts.push("nil".to_string());
} else if let Some(opts_type) = json_object_type {
parts.push(go_empty_value_expression(import_alias, opts_type, enums));
} 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;
let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
let go_literal = go_string_literal(&json_str);
if crate::e2e::codegen::value_contains_mock_url_placeholder(&converted_v) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
setup_lines.push(format!(
"{var_name}MockBaseURL := os.Getenv(\"{env_key}\")\n\tif {var_name}MockBaseURL == \"\" {{\n\t\t{var_name}MockBaseURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
));
setup_lines.push(format!(
"{var_name}JSON := strings.ReplaceAll({go_literal}, \"{}\", {var_name}MockBaseURL)",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
}
let json_expr = if crate::e2e::codegen::value_contains_mock_url_placeholder(&converted_v) {
format!("{var_name}JSON")
} else {
go_literal
};
if is_sum_type {
let element_type = element_type_name.unwrap();
setup_lines.push(format!(
"var {var_name}Raw []json.RawMessage\n\tif err := json.Unmarshal([]byte({json_expr}), &{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 {
setup_lines.push(format!(
"var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({json_expr}), &{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) = json_object_type {
let remapped_v = if Some(opts_type) == options_type && 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;
if crate::e2e::codegen::value_contains_mock_url_placeholder(&remapped_v) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
setup_lines.push(format!(
"{var_name}MockBaseURL := os.Getenv(\"{env_key}\")\n\tif {var_name}MockBaseURL == \"\" {{\n\t\t{var_name}MockBaseURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
));
setup_lines.push(format!(
"{var_name}JSON := strings.ReplaceAll({go_literal}, \"{}\", {var_name}MockBaseURL)",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
}
let json_expr = if crate::e2e::codegen::value_contains_mock_url_placeholder(&remapped_v) {
format!("{var_name}JSON")
} else {
go_literal
};
if matches!(
go_enum_shape(enums, opts_type),
Some(crate::backends::go::GoEnumRepresentation::DataInterface)
) {
let go_enum_name = crate::codegen::naming::go_type_name(opts_type);
setup_lines.push(format!(
"{var_name}, {var_name}Err := {import_alias}.Unmarshal{go_enum_name}([]byte({json_expr}))\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"unmarshal {go_enum_name} failed: %v\", {var_name}Err)\n\t}}"
));
parts.push(var_name.to_string());
continue;
}
let type_name = qualified_go_type(import_alias, opts_type);
setup_lines.push(format!(
"var {var_name} {type_name}\n\tif err := json.Unmarshal([]byte({json_expr}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
));
let arg_expr = if Some(opts_type) == options_type && 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 typed = if let Some(type_name) = native_declared_type {
typed_named_argument_expression(v, type_name, value_context, &arg.name, false)?
} else {
None
};
let go_val = typed.unwrap_or_else(|| json_to_go(v));
ensure_value_helpers(&mut package_decls, &go_val);
setup_lines.push(format!("{var_name} := {}", go_val.replace('\n', "\n\t")));
parts.push(format!("&{var_name}"));
}
_ => {
let typed = if let Some(type_name) = native_declared_type {
let uses_pointer = target
.param_for(&arg.name, arg_index)
.is_some_and(|param| param.optional);
typed_named_argument_expression(v, type_name, value_context, &arg.name, uses_pointer)?
} else {
None
};
if let Some(expression) = typed {
ensure_value_helpers(&mut package_decls, &expression);
if expression.contains('\n') {
setup_lines.push(format!("{} := {}", arg.name, expression.replace('\n', "\n\t")));
parts.push(arg.name.clone());
} else {
parts.push(expression);
}
} else {
parts.push(json_to_go(v));
}
}
},
}
}
Ok((package_decls, setup_lines, parts.join(", ")))
}
#[cfg(test)]
mod sealed_interface_argument_tests {
use super::build_args_and_setup;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeRef};
use crate::e2e::codegen::call_ir::TargetParams;
use crate::e2e::config::ArgMapping;
use crate::e2e::fixture::Fixture;
fn sealed_document_enum() -> EnumDef {
EnumDef {
name: "SampleDoc".into(),
rust_path: "samplelib::SampleDoc".into(),
variants: vec![EnumVariant {
name: "Url".into(),
fields: vec![FieldDef {
name: "url".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..EnumVariant::default()
}],
serde_rename_all: Some("snake_case".into()),
..EnumDef::default()
}
}
fn document_arg() -> ArgMapping {
ArgMapping {
name: "document".into(),
field: "input.document".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: Some("SampleDoc".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn render(input: serde_json::Value, enums: &[EnumDef]) -> (String, String) {
let fixture = Fixture {
id: "load_document".into(),
input,
..Fixture::default()
};
let (_declarations, setup, args) = build_args_and_setup(
&fixture.input,
&[document_arg()],
"pkg",
None,
&fixture,
false,
false,
&std::collections::HashSet::new(),
&crate::core::config::ResolvedCrateConfig::default(),
&[],
enums,
false,
TargetParams::IrAbsent,
)
.expect("args render");
(setup.join("\n"), args)
}
#[test]
fn a_sealed_interface_argument_is_built_by_its_unmarshal_dispatcher() {
let (setup, args) = render(
serde_json::json!({"document": {"url": "https://example.com/doc.pdf"}}),
&[sealed_document_enum()],
);
assert!(
setup.contains("document, documentErr := pkg.UnmarshalSampleDoc([]byte("),
"{setup}"
);
assert!(
!setup.contains("var document pkg.SampleDoc"),
"json.Unmarshal into an interface fails at run time:\n{setup}"
);
assert_eq!(args, "document");
}
#[test]
fn a_struct_typed_argument_keeps_the_json_unmarshal_rendering() {
let (setup, args) = render(
serde_json::json!({"document": {"url": "https://example.com/doc.pdf"}}),
&[],
);
assert!(setup.contains("var document pkg.SampleDoc"), "{setup}");
assert!(!setup.contains("UnmarshalSampleDoc"), "{setup}");
assert_eq!(args, "document");
}
#[test]
fn an_absent_sealed_interface_argument_defaults_to_nil() {
let (_setup, args) = render(serde_json::json!({"document": null}), &[sealed_document_enum()]);
assert_eq!(args, "nil");
}
#[test]
fn an_empty_sealed_interface_argument_defaults_to_nil() {
let (_setup, args) = render(serde_json::json!({"document": {}}), &[sealed_document_enum()]);
assert_eq!(args, "nil");
}
#[test]
fn an_absent_struct_typed_argument_keeps_its_composite_literal() {
let (_setup, args) = render(serde_json::json!({"document": null}), &[]);
assert_eq!(args, "pkg.SampleDoc{}");
}
#[test]
fn an_absent_unit_enum_argument_defaults_to_the_empty_string() {
let unit = EnumDef {
name: "SampleDoc".into(),
rust_path: "samplelib::SampleDoc".into(),
variants: vec![EnumVariant {
name: "Url".into(),
..EnumVariant::default()
}],
..EnumDef::default()
};
let (_setup, args) = render(serde_json::json!({"document": null}), &[unit]);
assert_eq!(args, "\"\"");
}
}
#[cfg(test)]
mod test_backend_fallback_tests {
use super::build_args_and_setup;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::ArgMapping;
use crate::e2e::fixture::Fixture;
fn test_backend_arg(trait_name: Option<&str>) -> ArgMapping {
ArgMapping {
name: "backend".into(),
field: "backend".into(),
arg_type: "test_backend".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: trait_name.map(str::to_string),
}
}
#[test]
fn unregistered_trait_panics_instead_of_falling_back_to_nil() {
let config = ResolvedCrateConfig::default();
let fixture = Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
};
let args = vec![test_backend_arg(Some("SampleBackend"))];
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_args_and_setup(
&fixture.input,
&args,
"sample",
None,
&fixture,
false,
false,
&std::collections::HashSet::new(),
&config,
&[],
&[],
false,
crate::e2e::codegen::call_ir::TargetParams::IrAbsent,
)
}));
let error = result.expect_err("an unregistered trait must panic, not return generated Go code");
let message = error
.downcast_ref::<String>()
.cloned()
.or_else(|| error.downcast_ref::<&str>().map(|s| (*s).to_string()))
.unwrap_or_default();
assert!(
message.contains("cannot generate a Go stub"),
"panic message should explain the unresolved trait bridge, got: {message}"
);
}
#[test]
fn missing_trait_name_panics_instead_of_falling_back_to_nil() {
let config = ResolvedCrateConfig::default();
let fixture = Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
};
let args = vec![test_backend_arg(None)];
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_args_and_setup(
&fixture.input,
&args,
"sample",
None,
&fixture,
false,
false,
&std::collections::HashSet::new(),
&config,
&[],
&[],
false,
crate::e2e::codegen::call_ir::TargetParams::IrAbsent,
)
}));
assert!(
result.is_err(),
"a `test_backend` arg with no `trait_name` must panic, not return generated Go code"
);
}
}