use super::assertions::json_to_zig;
use super::stubs::emit_test_backend_with_excluded;
use super::*;
pub(super) fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
fixture_id: &str,
_module_name: &str,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
fixture: &Fixture,
) -> (Vec<String>, String, bool) {
if args.is_empty() {
return (Vec::new(), String::new(), false);
}
let mut setup_lines: Vec<String> = Vec::new();
let mut parts: Vec<String> = Vec::new();
let mut setup_needs_gpa = false;
for arg in args {
if arg.arg_type == "mock_url" {
let name = arg.name.clone();
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) {
parts.push(format!("\"{}\"", escape_zig(url)));
continue;
}
let id_upper = fixture_id.to_uppercase();
setup_lines.push(format!(
"const {name} = if (std.c.getenv(\"MOCK_SERVER_{id_upper}\")) |_pf| try std.fmt.allocPrint(allocator, \"{{s}}\", .{{std.mem.span(_pf)}}) else try std.fmt.allocPrint(allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}});"
));
setup_lines.push(format!("defer allocator.free({name});"));
parts.push(name);
setup_needs_gpa = true;
continue;
}
if arg.arg_type == "mock_url_list" {
let value = crate::e2e::codegen::resolve_urls_field(input, &arg.field);
if let Some(urls) = crate::e2e::codegen::preserved_url_list(fixture.preserve_input_urls, value) {
let values = urls
.iter()
.map(|url| format!("\"{}\"", escape_zig(url)))
.collect::<Vec<_>>()
.join(", ");
parts.push(format!("&[_][]const u8{{{values}}}"));
continue;
}
}
if arg.arg_type == "handle" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_str = match input.get(field) {
Some(serde_json::Value::Null) | None => "null".to_string(),
Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
};
parts.push(json_str);
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 methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
let excluded_named =
crate::e2e::codegen::recipe::trait_bridge_excluded_type_names(config, type_defs, &methods);
let emission = emit_test_backend_with_excluded(trait_bridge, &methods, fixture, &excluded_named);
let setup_block = emission.setup_block.replace("lib.", &format!("{_module_name}."));
let arg_expr = emission.arg_expr.replace("lib.", &format!("{_module_name}."));
for line in setup_block.lines() {
setup_lines.push(line.to_string());
}
parts.push(arg_expr);
continue;
}
panic!(
"Zig 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 Zig stub without a resolvable trait bridge",
fixture.id, arg.name, arg.trait_name
);
}
if arg.name == "config" && arg.arg_type == "json_object" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_str = match input.get(field) {
Some(serde_json::Value::Null) | None => "{}".to_string(),
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
};
parts.push(format!("\"{}\"", escape_zig(&json_str)));
continue;
}
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = if field.is_empty() || field == "input" {
Some(input.get("extract_input").unwrap_or(input))
} else {
input.get(field)
};
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
parts.push("null".to_string());
}
None | Some(serde_json::Value::Null) => {
let default_val = match arg.arg_type.as_str() {
"string" => "\"\"".to_string(),
"int" | "integer" => "0".to_string(),
"float" | "number" => "0.0".to_string(),
"bool" | "boolean" => "false".to_string(),
"json_object" => "\"{}\"".to_string(),
_ => "null".to_string(),
};
parts.push(default_val);
}
Some(v) => {
if arg.arg_type == "json_object" {
let docs_files = fixture.docs_files_for_arg(&arg.field);
if !docs_files.is_empty() {
let (lines, expression) = render_docs_json(&arg.name, v, &docs_files);
setup_lines.extend(lines);
parts.push(expression);
setup_needs_gpa = true;
continue;
}
let json_str = serde_json::to_string(v).unwrap_or_default();
if crate::e2e::codegen::value_contains_mock_url_placeholder(v) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
let base_var = format!("{}_mock_base_url", arg.name);
let json_var = format!("{}_json", arg.name);
setup_lines.push(format!(
"const {base_var} = if (std.c.getenv(\"{env_key}\")) |_pf| try std.fmt.allocPrint(allocator, \"{{s}}\", .{{std.mem.span(_pf)}}) else try std.fmt.allocPrint(allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |url| std.mem.span(url) else \"http://localhost:8080\"}});"
));
setup_lines.push(format!("defer allocator.free({base_var});"));
setup_lines.push(format!(
"const {json_var} = try std.mem.replaceOwned(u8, allocator, \"{}\", \"{}\", {base_var});",
escape_zig(&json_str),
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
setup_lines.push(format!("defer allocator.free({json_var});"));
parts.push(json_var);
setup_needs_gpa = true;
} else {
parts.push(format!("\"{}\"", escape_zig(&json_str)));
}
} else if arg.arg_type == "bytes" {
if let serde_json::Value::String(path) = v {
let var_name = format!("{}_bytes", arg.name);
let io_name = format!("{var_name}_io");
let threaded_name = format!("{var_name}_threaded");
let epath = escape_zig(path);
setup_lines.push(format!(
"var {threaded_name} = std.Io.Threaded.init(std.heap.c_allocator, .{{}});"
));
setup_lines.push(format!("defer {threaded_name}.deinit();"));
setup_lines.push(format!("const {io_name} = {threaded_name}.io();"));
setup_lines.push(format!(
"const {var_name} = try std.Io.Dir.cwd().readFileAlloc({io_name}, \"{epath}\", std.heap.c_allocator, .unlimited);"
));
setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
parts.push(var_name);
} else {
parts.push(json_to_zig(v));
}
} else {
parts.push(json_to_zig(v));
}
}
}
}
(setup_lines, parts.join(", "), setup_needs_gpa)
}
fn render_docs_json(
variable: &str,
value: &serde_json::Value,
files: &[crate::e2e::fixture::FixtureDocsFileInput],
) -> (Vec<String>, String) {
let mut value = value.clone();
let mut reads = Vec::new();
for (index, file) in files.iter().enumerate() {
let marker = format!("__ALEF_DOC_FILE_{index}__");
let target = if file.field.is_empty() {
Some(&mut value)
} else {
value.pointer_mut(&file.field)
};
let Some(target) = target else { continue };
*target = serde_json::Value::String(marker.clone());
reads.push((index, marker, file.path.clone()));
}
let mut lines = Vec::new();
let mut source = format!("\"{}\"", escape_zig(&serde_json::to_string(&value).unwrap_or_default()));
for (index, marker, path) in reads {
lines.push(
crate::e2e::template_env::render(
"zig/docs_file_read.jinja",
minijinja::context! { variable => variable, index => index, path => escape_zig(&path) },
)
.trim_end()
.to_string(),
);
lines.push(
crate::e2e::template_env::render(
"zig/docs_file_json.jinja",
minijinja::context! { variable => variable, index => index },
)
.trim_end()
.to_string(),
);
let output = format!("{variable}_json_{index}");
lines.push(
crate::e2e::template_env::render(
"zig/docs_json_replace.jinja",
minijinja::context! {
output => output,
source => source,
marker => escape_zig(&format!("\"{marker}\"")),
variable => variable,
index => index,
},
)
.trim_end()
.to_string(),
);
source = output;
}
(lines, source)
}
#[cfg(test)]
mod docs_file_tests {
use super::render_docs_json;
use crate::e2e::fixture::FixtureDocsFileInput;
#[test]
fn nested_typed_dto_files_become_runtime_json_byte_arrays() {
let (lines, expression) = render_docs_json(
"request",
&serde_json::json!({"content": "ignored"}),
&[FixtureDocsFileInput {
field: "/content".into(),
path: "document.pdf".into(),
}],
);
assert!(
lines
.iter()
.any(|line| line.contains("readFileAlloc") && line.contains("document.pdf"))
);
assert!(lines.iter().any(|line| line.contains("emit_strings_as_arrays")));
assert_eq!(expression, "request_json_0");
}
#[test]
fn runtime_file_io_compiles_as_a_zig_binary() {
let zig = std::process::Command::new("zig").arg("version").output();
if zig.is_err() {
return;
}
let directory = tempfile::tempdir().expect("temporary directory");
let source_path = directory.path().join("main.zig");
let source = r#"const std = @import("std");
pub fn main(init: std.process.Init) !void {
var content_bytes_threaded = std.Io.Threaded.init(std.heap.c_allocator, .{});
defer content_bytes_threaded.deinit();
const content_bytes_io = content_bytes_threaded.io();
const content_bytes = try std.Io.Dir.cwd().readFileAlloc(content_bytes_io, "Cargo.toml", std.heap.c_allocator, .unlimited);
defer std.heap.c_allocator.free(content_bytes);
_ = init;
}
"#;
std::fs::write(&source_path, source).expect("write Zig source");
let output = std::process::Command::new("zig")
.args(["build-exe", "-fno-emit-bin"])
.arg(&source_path)
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("run Zig compiler");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
}
}