use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, TypeRef};
use crate::e2e::codegen::call_ir::TargetParams;
use crate::e2e::fixture::Fixture;
use heck::ToLowerCamelCase;
use super::values::{escape_swift, from_json_helper_for_arg, is_scalar_element_type, json_to_swift};
#[allow(clippy::too_many_arguments)]
pub(super) fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
fixture_id: &str,
has_host_root_route: bool,
function_name: &str,
options_via: Option<&str>,
options_type: Option<&str>,
handle_config_fn: Option<&str>,
visitor_handle_expr: Option<&str>,
is_method_call: bool,
module_name: &str,
unnamed_arg_indices: &[usize],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
fixture: &Fixture,
arg_name_map: Option<&std::collections::HashMap<String, String>>,
streaming_request_type: Option<&str>,
enums: &[crate::core::ir::EnumDef],
target_params: TargetParams<'_>,
) -> (Vec<String>, String) {
if args.is_empty() {
return (Vec::new(), String::new());
}
let mut setup_lines: Vec<String> = Vec::new();
let mut parts: Vec<(usize, String)> = Vec::new();
let later_emits: Vec<bool> = (0..args.len())
.map(|i| {
args.iter().skip(i + 1).any(|a| {
let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
let v = input.get(f);
let has_value = matches!(v, Some(x) if !x.is_null());
has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
})
})
.collect();
for (idx, arg) in args.iter().enumerate() {
if arg.arg_type == "mock_url" {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let value = input.get(field).unwrap_or(&serde_json::Value::Null);
let url_expr = if let Some(url) =
crate::e2e::codegen::preserved_url_literal(fixture.preserve_input_urls, value)
{
format!("\"{}\"", escape_swift(url))
} else if has_host_root_route {
format!(
"ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (AlefE2EMockServer.baseURL + \"/fixtures/{fixture_id}\")"
)
} else {
format!("AlefE2EMockServer.baseURL + \"/fixtures/{fixture_id}\"")
};
setup_lines.push(format!("let {} = {url_expr}", arg.name));
if let Some(request_type) = streaming_request_type.filter(|_| idx > 0) {
let request_label = arg_name_map
.and_then(|map| map.get(&arg.name).map(String::as_str))
.map(str::to_owned)
.unwrap_or_else(|| arg.name.to_lower_camel_case());
let request_var = format!("{}Request", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {request_var} = {request_type}({request_label}: {})",
arg.name
));
parts.push((idx, request_var));
} else {
parts.push((idx, arg.name.clone()));
}
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 literals = urls
.into_iter()
.map(|url| format!("\"{}\"", escape_swift(url)))
.collect::<Vec<_>>()
.join(", ");
setup_lines.push(format!("let {} = [{literals}]", arg.name));
parts.push((idx, arg.name.clone()));
continue;
}
}
if arg.arg_type == "handle" {
let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let config_val = input.get(field);
let has_config = config_val
.is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
if has_config {
if let Some(from_json_fn) = handle_config_fn {
let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
let escaped = escape_swift(&json_str);
let config_var = format!("{}Config", arg.name.to_lower_camel_case());
setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
setup_lines.push(format!("let {var_name} = try createEngine(config: {config_var})"));
} else {
setup_lines.push(format!("let {var_name} = try createEngine(config: nil)"));
}
} else {
setup_lines.push(format!("let {var_name} = try createEngine(config: nil)"));
}
parts.push((idx, var_name));
continue;
}
if arg.arg_type == "bytes" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
if later_emits[idx] {
parts.push((idx, "nil".to_string()));
}
}
None | Some(serde_json::Value::Null) => {
parts.push((idx, "[UInt8]()".to_string()));
}
Some(serde_json::Value::String(s)) => {
let escaped = escape_swift(s);
let var_name = format!("{}Bytes", arg.name.to_lower_camel_case());
let data_var = format!("{}Data", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
));
setup_lines.push(format!("let {var_name} = Array({data_var})"));
parts.push((idx, var_name));
}
Some(serde_json::Value::Array(arr)) => {
let bytes: Vec<String> = arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
parts.push((idx, format!("[UInt8]({})", bytes.join(", "))));
}
Some(other) => {
let json_str = serde_json::to_string(other).unwrap_or_default();
let escaped = escape_swift(&json_str);
let var_name = format!("{}Bytes", arg.name.to_lower_camel_case());
setup_lines.push(format!("let {var_name} = Array(\"{escaped}\".utf8)"));
parts.push((idx, var_name));
}
}
continue;
}
if arg.arg_type == "file_path" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
if later_emits[idx] {
parts.push((idx, "nil".to_string()));
}
}
None | Some(serde_json::Value::Null) => {
parts.push((idx, "\"\"".to_string()));
}
Some(serde_json::Value::String(s)) => {
let escaped = escape_swift(s);
parts.push((idx, format!("\"{}\"", escaped)));
}
Some(other) => {
let json_str = serde_json::to_string(other).unwrap_or_default();
let escaped = escape_swift(&json_str);
parts.push((idx, format!("\"{}\"", escaped)));
}
}
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 emission = super::stubs::emit_test_backend(trait_bridge, &methods, fixture, enums);
setup_lines.push(emission.setup_block);
parts.push((idx, emission.arg_expr));
continue;
}
panic!(
"Swift 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 Swift stub without a resolvable trait bridge",
fixture.id, arg.name, arg.trait_name
);
}
let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
if is_config_arg {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
let json_str = match val {
None | Some(serde_json::Value::Null) => "{}".to_string(),
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
};
let docs_files = fixture.docs_files_for_arg(&arg.field);
let json_expr = docs_json_expression(&json_str, &arg.name, &docs_files, &mut setup_lines);
let config_is_unnamed = unnamed_arg_indices.contains(&idx);
if config_is_unnamed {
parts.push((idx, json_expr));
} else {
let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
let from_json_fn = from_json_helper_for_arg(arg, options_type);
setup_lines.push(format!(
"let {var_name} = try {module_name}.{from_json_fn}({json_expr})"
));
parts.push((idx, var_name));
}
continue;
}
if arg.arg_type == "json_object"
&& arg.element_type.is_some()
&& !is_scalar_element_type(arg.element_type.as_deref())
{
let val = super::super::resolve_field(input, &arg.field);
let elem_type = arg.element_type.as_deref().unwrap_or("Unknown");
let from_json_fn = format!("{}FromJson", elem_type.to_lower_camel_case());
match val {
serde_json::Value::Object(_) => {
let json_str = serde_json::to_string(val).unwrap_or_else(|_| "{}".to_string());
let escaped = escape_swift(&json_str);
let json_expr = if crate::e2e::codegen::value_contains_mock_url_placeholder(val) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
let base_var = format!("{}MockBaseUrl", arg.name.to_lower_camel_case());
let json_var = format!("{}Json", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {base_var} = ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (AlefE2EMockServer.baseURL + \"/fixtures/{fixture_id}\")"
));
setup_lines.push(format!(
"let {json_var} = \"{escaped}\".replacingOccurrences(of: \"{}\", with: {base_var})",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
json_var
} else {
format!("\"{escaped}\"")
};
if unnamed_arg_indices.contains(&idx) {
parts.push((idx, json_expr));
} else {
let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {var_name} = try {module_name}.{from_json_fn}({json_expr})"
));
parts.push((idx, var_name));
}
}
serde_json::Value::Array(arr) => {
let var_name = format!("{}Array", arg.name.to_lower_camel_case());
if arr.is_empty() {
parts.push((idx, "[]".to_string()));
} else {
let json_strs: Vec<String> =
arr.iter().filter_map(|item| serde_json::to_string(item).ok()).collect();
let mock_base_var = if arr.iter().any(crate::e2e::codegen::value_contains_mock_url_placeholder)
{
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
let base_var = format!("{}MockBaseUrl", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {base_var} = ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (AlefE2EMockServer.baseURL + \"/fixtures/{fixture_id}\")"
));
Some(base_var)
} else {
None
};
let mut item_vars = Vec::new();
for (i, json_str) in json_strs.iter().enumerate() {
let escaped = escape_swift(json_str);
let item_var = format!("_item_{var_name}_{i}");
let json_expr = if let Some(base_var) = mock_base_var.as_deref()
&& json_str.contains(crate::e2e::codegen::MOCK_URL_PLACEHOLDER)
{
format!(
"\"{escaped}\".replacingOccurrences(of: \"{}\", with: {base_var})",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
)
} else {
format!("\"{escaped}\"")
};
setup_lines.push(format!(
"let {item_var} = try {module_name}.{from_json_fn}({json_expr})"
));
item_vars.push(item_var);
}
setup_lines.push(format!("let {var_name} = [{}]", item_vars.join(", ")));
parts.push((idx, var_name));
}
}
serde_json::Value::Null if arg.optional => {
if later_emits[idx] {
parts.push((idx, "nil".to_string()));
}
}
serde_json::Value::Null => {
parts.push((idx, "[]".to_string()));
}
_other => {
parts.push((idx, "[]".to_string()));
}
}
continue;
}
if arg.arg_type == "json_object" && arg.element_type.as_deref() == Some("String") {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
if let Some(serde_json::Value::Array(arr)) = input.get(field) {
let mock_base_var = if arr.iter().any(crate::e2e::codegen::value_contains_mock_url_placeholder) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
let base_var = format!("{}MockBaseUrl", arg.name.to_lower_camel_case());
setup_lines.push(format!(
"let {base_var} = ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (AlefE2EMockServer.baseURL + \"/fixtures/{fixture_id}\")"
));
Some(base_var)
} else {
None
};
let items: Vec<String> = arr
.iter()
.filter_map(|item| item.as_str())
.map(|raw| {
let escaped = escape_swift(raw);
if let Some(base_var) = mock_base_var.as_deref()
&& raw.contains(crate::e2e::codegen::MOCK_URL_PLACEHOLDER)
{
format!(
"\"{escaped}\".replacingOccurrences(of: \"{}\", with: {base_var})",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
)
} else {
format!("\"{escaped}\"")
}
})
.collect();
parts.push((idx, format!("[{}]", items.join(", "))));
continue;
}
}
if arg.arg_type == "json_object"
&& options_via == Some("from_json")
&& let Some(type_name) = options_type
{
let resolved_val = super::super::resolve_field(input, &arg.field);
let json_str = match resolved_val {
serde_json::Value::Null => "{}".to_string(),
v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
};
let docs_files = fixture.docs_files_for_arg(&arg.field);
let json_expr = docs_json_expression(&json_str, &arg.name, &docs_files, &mut setup_lines);
let var_name = format!("_{}", arg.name.to_lower_camel_case());
if let Some(handle_expr) = visitor_handle_expr {
let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
setup_lines.push(format!("let {handle_var} = {handle_expr}"));
setup_lines.push(format!(
"let {var_name} = try {module_name}.{with_visitor_fn}({json_expr}, {handle_var})"
));
} else {
let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
setup_lines.push(format!(
"let {var_name} = try {module_name}.{from_json_fn}({json_expr})"
));
}
parts.push((idx, var_name));
continue;
}
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
if later_emits[idx] {
parts.push((idx, "nil".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(),
_ => "nil".to_string(),
};
parts.push((idx, default_val));
}
Some(v) => {
if let Some(typed) = ir_typed_swift_expression(arg, idx, v, target_params, enums) {
parts.push((idx, typed));
continue;
}
parts.push((idx, json_to_swift(v)));
}
}
}
let is_register_call = function_name.starts_with("register") || function_name.starts_with("Register");
let args_str = parts
.into_iter()
.map(|(idx, val)| {
if is_method_call || is_register_call || unnamed_arg_indices.contains(&idx) {
val
} else {
let arg_name: &str = arg_name_map
.and_then(|m| m.get(&args[idx].name).map(String::as_str))
.unwrap_or(&args[idx].name);
let label = arg_name.to_lower_camel_case();
format!("{label}: {val}")
}
})
.collect::<Vec<_>>()
.join(", ");
(setup_lines, args_str)
}
fn ir_typed_swift_expression(
arg: &crate::e2e::config::ArgMapping,
index: usize,
value: &serde_json::Value,
target_params: TargetParams<'_>,
enums: &[EnumDef],
) -> Option<String> {
let TypeRef::Named(declared) = &target_params.param_for(&arg.name, index)?.ty else {
return None;
};
let text = value.as_str()?;
let enum_def = enums.iter().find(|enum_def| {
&enum_def.name == declared && enum_def.has_serde && enum_def.variants.iter().all(|v| v.fields.is_empty())
})?;
let variant = enum_def.variants.iter().find(|variant| {
crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
) == text
})?;
let case_name = crate::backends::swift::naming::swift_source_ident(&variant.name.to_lower_camel_case());
Some(format!("{declared}.{case_name}"))
}
fn docs_json_expression(
json: &str,
variable: &str,
files: &[crate::e2e::fixture::FixtureDocsFileInput],
setup_lines: &mut Vec<String>,
) -> String {
if files.is_empty() {
return format!("\"{}\"", escape_swift(json));
}
let mut value: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
let mut replacements = String::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());
setup_lines.push(
crate::e2e::template_env::render(
"swift/docs_file_read.jinja",
minijinja::context! { variable => variable, index => index, path => escape_swift(&file.path) },
)
.trim_end()
.to_string(),
);
setup_lines.push(
crate::e2e::template_env::render(
"swift/docs_file_json.jinja",
minijinja::context! { variable => variable, index => index },
)
.trim_end()
.to_string(),
);
replacements.push_str(&format!(
".replacingOccurrences(of: \"\\\\\\\"{marker}\\\\\\\"\", with: {variable}File{index}Json)"
));
}
format!(
"\"{}\"{replacements}",
escape_swift(&serde_json::to_string(&value).unwrap_or_default())
)
}
#[cfg(test)]
mod tests {
use super::docs_json_expression;
use crate::e2e::fixture::FixtureDocsFileInput;
#[test]
fn nested_typed_dto_files_become_runtime_byte_arrays() {
let mut setup = Vec::new();
let expression = docs_json_expression(
r#"{"content":"ignored"}"#,
"request",
&[FixtureDocsFileInput {
field: "/content".into(),
path: "document.pdf".into(),
}],
&mut setup,
);
assert_eq!(
setup[0],
"let requestFile0 = try Data(contentsOf: URL(fileURLWithPath: \"document.pdf\"))"
);
assert!(setup[1].contains("requestFile0.map(String.init)"));
assert!(expression.contains("replacingOccurrences"), "{expression}");
assert!(expression.contains("requestFile0Json"), "{expression}");
}
use super::build_args_and_setup;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, EnumVariant, ParamDef, TypeRef};
use crate::e2e::codegen::call_ir::TargetParams;
use crate::e2e::config::ArgMapping;
use crate::e2e::fixture::Fixture;
fn default_typed_arg(name: &str) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "string".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn mode_enum() -> EnumDef {
EnumDef {
name: "FilePurpose".to_string(),
has_serde: true,
serde_rename_all: Some("kebab-case".to_string()),
variants: vec![EnumVariant {
name: "FineTune".to_string(),
..EnumVariant::default()
}],
..EnumDef::default()
}
}
fn swift_args_for(enums: &[EnumDef], target_params: TargetParams<'_>) -> String {
let args = vec![default_typed_arg("purpose")];
let fixture = Fixture {
id: "typed".to_string(),
input: serde_json::json!({ "purpose": "fine-tune" }),
..Fixture::default()
};
let (_setup, args_str) = build_args_and_setup(
&fixture.input,
&args,
&fixture.id,
false,
"convert",
None,
None,
None,
None,
true,
"Sample",
&[],
&ResolvedCrateConfig::default(),
&[],
&fixture,
None,
None,
enums,
target_params,
);
args_str
}
#[test]
fn a_string_value_for_an_ir_enum_parameter_names_the_generated_case() {
let params = [ParamDef {
name: "purpose".to_string(),
ty: TypeRef::Named("FilePurpose".to_string()),
..ParamDef::default()
}];
assert_eq!(
swift_args_for(&[mode_enum()], TargetParams::Known(¶ms)),
"FilePurpose.fineTune"
);
}
#[test]
fn the_same_string_value_still_renders_as_a_literal_when_the_ir_is_absent() {
assert_eq!(swift_args_for(&[mode_enum()], TargetParams::IrAbsent), "\"fine-tune\"");
}
#[test]
fn an_unmatched_enum_wire_value_keeps_its_string_literal() {
let params = [ParamDef {
name: "purpose".to_string(),
ty: TypeRef::Named("FilePurpose".to_string()),
..ParamDef::default()
}];
let mut unmatched = mode_enum();
unmatched.variants[0].name = "Assistants".to_string();
assert_eq!(
swift_args_for(&[unmatched], TargetParams::Known(¶ms)),
"\"fine-tune\""
);
}
#[test]
fn a_non_serde_enum_keeps_the_existing_lowering() {
let params = [ParamDef {
name: "purpose".to_string(),
ty: TypeRef::Named("FilePurpose".to_string()),
..ParamDef::default()
}];
let mut non_serde = mode_enum();
non_serde.has_serde = false;
assert_eq!(
swift_args_for(&[non_serde], TargetParams::Known(¶ms)),
"\"fine-tune\""
);
}
#[test]
fn a_wrapped_named_parameter_is_left_to_the_existing_lowering() {
let params = [ParamDef {
name: "purpose".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("FilePurpose".to_string()))),
..ParamDef::default()
}];
assert_eq!(
swift_args_for(&[mode_enum()], TargetParams::Known(¶ms)),
"\"fine-tune\""
);
}
}