1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
use crate::core::config::ResolvedCrateConfig;
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)]
/// Build setup lines and the argument list for the function call.
///
/// Swift-bridge wrappers require strongly-typed values that don't have implicit
/// Swift literal conversions:
///
/// - `bytes` args become `RustVec<UInt8>` — fixture supplies a relative file path
/// string which is read at test time and pushed into a `RustVec<UInt8>` setup
/// variable. A literal byte array is base64-decoded or UTF-8 encoded inline.
/// - `json_object` args become opaque config/request instances — a JSON string is
/// decoded via the matching `{Type}FromJson(...)` helper in a setup line.
/// - Optional args missing from the fixture must still appear at the call site
/// as `nil` whenever a later positional arg is present, otherwise Swift slots
/// subsequent values into the wrong parameter.
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],
) -> (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();
// Pre-compute, for each arg index, whether any later arg has a fixture-provided
// value (or is required and will emit a default). When an optional arg is empty
// but a later arg WILL emit, we must keep the slot with `nil` so positional
// alignment is preserved.
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 url_expr = 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));
// Streaming adapters with request metadata take a request DTO instead
// of a bare mock URL. Use the adapter's request_type and this arg's
// configured name as the DTO field label.
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 == "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())));
// Swift binding's engine factory declares `createEngine(config: ConfigType?)`,
// so calls require the `config:` argument label even when passing `nil`.
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;
}
// bytes args: behavior depends on whether this is an e2e async wrapper (e.g. extractBytes
// with unnamed_arg_indices) or a regular binding function. Swift's extractBytes/extractBytesSync
// e2e wrappers take [UInt8] bytes (not path strings). When the fixture provides a path string,
// read the file to bytes. Regular bindings also emit [UInt8] arrays from path strings.
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) => {
// Empty byte array
parts.push((idx, "[UInt8]()".to_string()));
}
Some(serde_json::Value::String(s)) => {
let escaped = escape_swift(s);
// Both unnamed and named bytes args: read file to bytes
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)) => {
// Inline byte array literal
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) => {
// Fallback: encode the JSON serialisation as UTF-8 bytes.
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;
}
// file_path args: pass path strings directly (for extract_file, extract_file_sync, etc.)
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) => {
// Fallback: convert to JSON string
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 {
if 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;
}
}
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("swift");
setup_lines.push(format!("// {}", emission.arg_expr));
parts.push((idx, "nil".to_string()));
continue;
}
// json_object "config" args: behavior depends on whether this is an e2e wrapper or regular binding.
// E2e wrappers (all args in unnamed_arg_indices) take JSON strings and deserialize internally.
// Regular bindings (config arg not unnamed) expect deserialized objects (via options_via or default helper).
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 escaped = escape_swift(&json_str);
// Detect if config arg is unnamed (index `idx` in unnamed_arg_indices).
// E2e wrappers keep config unnamed and receive JSON strings.
let config_is_unnamed = unnamed_arg_indices.contains(&idx);
if config_is_unnamed {
// E2e wrapper: pass JSON string directly (positional, no label).
parts.push((idx, format!("\"{}\"", escaped)));
} else {
// Regular binding: deserialize to an opaque object.
let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
let from_json_fn = from_json_helper_for_arg(arg, options_type);
// Qualify with module name to avoid ambiguity when both SampleCrate and RustBridge are imported.
setup_lines.push(format!(
"let {var_name} = try {module_name}.{from_json_fn}(\"{escaped}\")"
));
parts.push((idx, var_name));
}
continue;
}
// json_object non-config args with array values: construct Swift data-enum objects
// from the JSON array using the {TypeName}FromJson helper. This handles cases like
// interact(actions: [PageAction]) where we deserialize JSON into enum instances.
if arg.arg_type == "json_object"
&& arg.element_type.is_some()
&& !is_scalar_element_type(arg.element_type.as_deref())
{
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
let elem_type = arg.element_type.as_deref().unwrap_or("Unknown");
// Convert element type to camelCase for the from-json helper name
let from_json_fn = format!("{}FromJson", elem_type.to_lower_camel_case());
match val {
Some(serde_json::Value::Array(arr)) => {
let var_name = format!("{}Array", arg.name.to_lower_camel_case());
if arr.is_empty() {
// Empty array literal
parts.push((idx, "[]".to_string()));
} else {
// For each JSON item in the array, call the helper to deserialize it
let json_strs: Vec<String> =
arr.iter().filter_map(|item| serde_json::to_string(item).ok()).collect();
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}");
// Call the wrapper-module's `{type}FromJson` helper rather than the
// raw `RustBridge` one so the resulting element is the
// wrapper-module's `PageAction` (etc.), matching the type the
// function signature expects. The wrapper internally delegates to
// `RustBridge.{type}FromJson` which understands the
// serde(tag = "type") format.
setup_lines.push(format!(
"let {item_var} = try {module_name}.{from_json_fn}(\"{escaped}\")"
));
item_vars.push(item_var);
}
// Construct the final array from all item variables
setup_lines.push(format!("let {var_name} = [{}]", item_vars.join(", ")));
parts.push((idx, var_name));
}
}
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) => {
// Required but missing — emit empty array
parts.push((idx, "[]".to_string()));
}
Some(_other) => {
// Non-array value — emit empty array (shouldn't happen)
parts.push((idx, "[]".to_string()));
}
}
continue;
}
// json_object non-config args with options_via = "from_json":
// Use the generated `{typeCamelCase}FromJson(_:)` helper so the fixture JSON is
// deserialised into the opaque swift-bridge type rather than passed as a raw string.
// When arg.field == "input", the entire fixture input IS the request object.
// When a visitor handle is present, use `{typeCamelCase}FromJsonWithVisitor(json, handle)`
// instead to attach the visitor to the options in one step.
if arg.arg_type == "json_object" && options_via == Some("from_json") {
if 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 escaped = escape_swift(&json_str);
let var_name = format!("_{}", arg.name.to_lower_camel_case());
if let Some(handle_expr) = visitor_handle_expr {
// Use the visitor-aware helper: `{typeCamelCase}FromJsonWithVisitor(json, handle)`.
// The handle expression builds a VisitorHandle from the local class instance.
// The function name mirrors emit_options_field_options_helper: camelCase of
// `{options_snake}_from_json_with_visitor`.
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}(\"{escaped}\", {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}(\"{escaped}\")"
));
}
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 => {
// Optional arg with no fixture value: keep the slot with `nil`
// when a later arg will emit, so positional alignment matches
// the swift-bridge wrapper signature.
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) => {
parts.push((idx, json_to_swift(v)));
}
}
}
// Method calls on the configured client handle (e.g. `_client.chat(req)`) use
// anonymous Swift argument labels (`func chat(_ req:)`), so omit `name:` prefixes.
// Free-function calls (e.g. `process(source:, config:)`) keep labelled args.
// Registration functions also use positional args.
// Swift argument labels must be camelCase, so convert from snake_case.
// Some APIs like detectMimeTypeFromBytes take unnamed first parameters —
// omit labels for indices listed in unnamed_arg_indices.
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 {
// Apply per-language argument renames before emitting the call.
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)
}