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
//! Kotlin argument construction and setup helpers.
use heck::ToUpperCamelCase;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::ArgMapping;
use crate::e2e::escape::escape_kotlin;
use crate::e2e::fixture::Fixture;
/// Build setup lines and the argument list for the function call.
///
/// Returns `(setup_lines, args_string)`.
///
/// `kotlin_android_style = true` switches the optional-`json_object` default
/// from `OptionsType.builder().build()` to `null`. The Java-facade-backed
/// JVM target emits a Java-style builder for every `json_object` type, but
/// the kotlin_android backend emits plain Kotlin data classes with no
/// `.builder()` companion (every field is declared without a default), so a
/// builder call would not compile. The Android facade signatures declare the
/// optional argument as `T? = null`, making `null` the idiomatic positional
/// default that matches the call arity.
pub(super) struct KotlinArgsContext<'a> {
pub(super) fixture: &'a Fixture,
pub(super) class_name: &'a str,
pub(super) options_type: Option<&'a str>,
pub(super) fixture_id: &'a str,
pub(super) kotlin_android_style: bool,
pub(super) config: &'a ResolvedCrateConfig,
pub(super) type_defs: &'a [crate::core::ir::TypeDef],
}
pub(super) fn build_args_and_setup(
input: &serde_json::Value,
args: &[ArgMapping],
context: KotlinArgsContext<'_>,
) -> (Vec<String>, String) {
let KotlinArgsContext {
fixture,
class_name,
options_type,
fixture_id,
kotlin_android_style,
config,
type_defs,
} = context;
if args.is_empty() {
return (Vec::new(), String::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() {
setup_lines.push(format!(
"val {} = System.getProperty(\"mockServer.{fixture_id}\", (System.getProperty(\"mockServerUrl\", System.getenv(\"MOCK_SERVER_URL\") ?: \"\") ?: \"\") + \"/fixtures/{fixture_id}\")",
arg.name,
));
} else {
setup_lines.push(format!(
"val {} = (System.getProperty(\"mockServerUrl\", System.getenv(\"MOCK_SERVER_URL\") ?: \"\") ?: \"\") + \"/fixtures/{fixture_id}\"",
arg.name,
));
}
parts.push(arg.name.clone());
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);
if config_value.is_null()
|| config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
{
setup_lines.push(format!("val {} = {class_name}.{constructor_name}(null)", arg.name,));
} else {
let json_str = serde_json::to_string(config_value).unwrap_or_default();
let name = &arg.name;
if let Some(config_type) = super::test_file::resolve_handle_config_type(arg, options_type, type_defs) {
setup_lines.push(format!(
"val {name}Config = MAPPER.readValue(\"{}\", {config_type}::class.java)",
escape_kotlin(&json_str),
));
setup_lines.push(format!(
"val {} = {class_name}.{constructor_name}({name}Config)",
arg.name,
name = name,
));
} else {
setup_lines.push(format!("val {} = {class_name}.{constructor_name}(null)", arg.name,));
}
}
parts.push(arg.name.clone());
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) {
// Collect methods from both the main trait and its super-trait (if present).
// The super-trait methods are needed so stubs implement the full interface.
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 there's a super-trait, also collect its methods.
if let Some(super_trait) = &trait_bridge.super_trait {
// Extract the simple name from the full path (e.g., "Plugin" from "sample_core::plugins::Plugin").
let super_trait_simple = super_trait.rsplit("::").next().unwrap_or(super_trait.as_str());
if let Some(super_type) = type_defs.iter().find(|t| t.name == super_trait_simple) {
for method in &super_type.methods {
// Only add if not already present (avoid duplicates).
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
}
// For kotlin_android, filter out methods whose return type or parameters
// reference types in the `exclude_types` list. The binding generator
// omits those methods from the generated interface, so the test stub
// must not attempt to implement them.
if kotlin_android_style {
let excluded: std::collections::HashSet<&str> = config
.kotlin_android
.as_ref()
.map(|c| c.exclude_types.iter().map(String::as_str).collect())
.unwrap_or_default();
if !excluded.is_empty() {
methods.retain(|m| {
!excluded.iter().any(|ex| m.return_type.references_named(ex))
&& m.params
.iter()
.all(|p| !excluded.iter().any(|ex| p.ty.references_named(ex)))
});
}
}
let lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let emission = crate::e2e::codegen::emit_test_backend(lang, trait_bridge, &methods, fixture);
setup_lines.push(emission.setup_block);
parts.push(emission.arg_expr);
continue;
}
}
let lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented(lang);
setup_lines.push(format!("// {}", emission.arg_expr));
parts.push("null".to_string());
continue;
}
// Use resolve_field so field = "input" resolves to the whole fixture input.
let val_resolved = crate::e2e::codegen::resolve_field(input, &arg.field);
let val: Option<&serde_json::Value> = if val_resolved.is_null() {
None
} else {
Some(val_resolved)
};
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
// Optional arg with no fixture value: emit positional default so the
// call has the right arity for the facade.
//
// For json_object optional args:
// - If options_type is set, use `OptionsType()` for kotlin_android (data class
// constructor with defaults) or `OptionsType.builder().build()` for Java facade.
// - If no options_type, infer the type from arg.name and emit default constructor
// (e.g., a configured default constructor for an options arg). This handles both Java facade
// (which requires non-null) and kotlin_android (which also declares non-null).
if arg.arg_type == "json_object" {
let default_constructor = if let Some(opts_type) = options_type {
if kotlin_android_style {
format!("{}()", opts_type)
} else {
format!("{}.builder().build()", opts_type)
}
} else {
// Infer the type from available config types in type_defs.
let inferred_type = super::test_file::resolve_handle_config_type(
&crate::e2e::config::ArgMapping {
name: arg.name.clone(),
field: arg.field.clone(),
arg_type: "handle".to_string(),
optional: arg.optional,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
None,
type_defs,
)
.unwrap_or_else(|| {
// Fallback: try the pattern "{field}Config"
let candidate = format!("{}Config", arg.name.to_upper_camel_case());
if type_defs.iter().any(|t| t.name == candidate) {
candidate
} else {
arg.name.to_upper_camel_case()
}
});
format!("{}()", inferred_type)
};
parts.push(default_constructor);
} else {
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(),
_ => "null".to_string(),
};
parts.push(default_val);
}
Some(v) => {
// Typed arrays carry `element_type` and are materialised as `listOf(...)`.
// For kotlin_android batch APIs the element type is a binding class
// (e.g. BatchBytesItem) that wraps multiple fields from JSON objects.
// For JVM bindings, when element_type is present, deserialize objects via Jackson
// instead of emitting raw JSON strings.
if arg.arg_type == "json_object" && v.is_array() && arg.element_type.is_some() {
let element_type = arg.element_type.as_deref().unwrap();
let items: Vec<String> = v
.as_array()
.map(|arr| {
arr.iter()
.map(|item| {
// For object items, deserialize via Jackson to the element type
if item.is_object() {
let normalized = crate::e2e::codegen::transform_json_keys_for_language(item, "snake_case");
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
format!(
"MAPPER.readValue(\"{}\", {element_type}::class.java)",
escape_kotlin(&json_str)
)
} else if element_type == "String" {
super::values::json_to_kotlin(item)
} else if let Some(path) = item.as_str() {
// For string items (file paths), construct the element with the path
if kotlin_android_style {
format!(
"{element_type}(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(\"{}\")), java.nio.charset.StandardCharsets.UTF_8)",
escape_kotlin(path)
)
} else {
// JVM version takes Path objects, not ByteArray
format!(
"{element_type}(java.nio.file.Paths.get(\"{}\"))",
escape_kotlin(path)
)
}
} else {
// Fallback for other literal types
super::values::json_to_kotlin(item)
}
})
.collect()
})
.unwrap_or_default();
parts.push(format!("listOf({})", items.join(", ")));
continue;
}
// For json_object args, deserialize via Jackson or use pre-deserialized variable.
if arg.arg_type == "json_object" {
if options_type.is_some() {
// Pre-deserialized variable via options_type
parts.push(arg.name.clone());
} else {
// Infer the config type and deserialize
let config_type = super::test_file::resolve_handle_config_type(
&crate::e2e::config::ArgMapping {
name: arg.name.clone(),
field: arg.field.clone(),
arg_type: "handle".to_string(),
optional: arg.optional,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
None,
type_defs,
)
.unwrap_or_else(|| {
// Fallback to derived type
let candidate = format!("{}Config", arg.name.to_upper_camel_case());
if type_defs.iter().any(|t| t.name == candidate) {
candidate
} else {
arg.name.to_upper_camel_case()
}
});
// Setup deserialization
let json_str = serde_json::to_string(v).unwrap_or_default();
let var_name = format!("{}_Config", arg.name);
setup_lines.push(format!(
"val {var_name} = MAPPER.readValue(\"{}\", {config_type}::class.java)",
crate::e2e::escape::escape_kotlin(&json_str)
));
parts.push(var_name);
}
continue;
}
// bytes args in Kotlin binding carry a relative file path (e.g. "docx/fake.docx")
// that the Kotlin API resolves and reads internally.
// - JVM binding: pass the path string directly
// - android binding: need to read bytes and wrap in ByteArray
if arg.arg_type == "bytes" {
let val = super::values::json_to_kotlin(v);
if kotlin_android_style {
// kotlin_android needs ByteArray, not String path
// Emit code to read the file as bytes
if v.is_string() {
parts.push(format!(
"java.nio.file.Files.readAllBytes(java.nio.file.Paths.get({val}))"
));
} else {
parts.push("byteArrayOf()".to_string());
}
} else {
parts.push(val);
}
continue;
}
// file_path args: Kotlin module wraps the Java facade (which takes Path),
// but kotlin_android has a different signature that takes a plain String.
if arg.arg_type == "file_path" {
let val = super::values::json_to_kotlin(v);
if kotlin_android_style {
// kotlin_android binding takes a plain String path
parts.push(val);
} else {
// Kotlin (JVM) binding re-exports Java facade which takes java.nio.file.Path
parts.push(format!("java.nio.file.Path.of({val})"));
}
continue;
}
parts.push(super::values::json_to_kotlin(v));
}
}
}
(setup_lines, parts.join(", "))
}