use crate::core::config::ResolvedCrateConfig;
use crate::e2e::escape::escape_csharp;
use heck::{ToLowerCamelCase, ToUpperCamelCase};
use std::collections::HashMap;
use super::stubs::emit_test_backend_with_class_name;
use super::{classify_bytes_value_csharp, json_to_csharp, resolve_handle_config_type};
#[allow(clippy::too_many_arguments)]
pub(super) fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
class_name: &str,
options_type: Option<&str>,
options_via: Option<&str>,
enum_fields: &HashMap<String, String>,
nested_types: &HashMap<String, String>,
fixture: &crate::e2e::fixture::Fixture,
adapter_request_type: Option<&str>,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
class_decls: &mut Vec<String>,
teardown_lines: &mut Vec<String>,
) -> (Vec<String>, String) {
let fixture_id = &fixture.id;
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 == "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 => {
parts.push("null".to_string());
}
None | Some(serde_json::Value::Null) => {
parts.push("System.Array.Empty<byte>()".to_string());
}
Some(v) => {
if let Some(s) = v.as_str() {
let bytes_code = classify_bytes_value_csharp(s);
parts.push(bytes_code);
} else {
let cs_str = json_to_csharp(v);
parts.push(format!("System.Text.Encoding.UTF8.GetBytes({cs_str})"));
}
}
}
continue;
}
if arg.arg_type == "mock_url" {
if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
setup_lines.push(format!(
"var _pfUrl_{name} = Environment.GetEnvironmentVariable(\"{env_key}\");",
name = arg.name,
));
setup_lines.push(format!(
"var {} = !string.IsNullOrEmpty(_pfUrl_{name}) ? _pfUrl_{name} : Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
arg.name,
name = arg.name,
));
} else {
setup_lines.push(format!(
"var {} = Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
arg.name,
));
}
if let Some(req_type) = adapter_request_type {
let req_var = format!("{}Req", arg.name);
setup_lines.push(format!("var {req_var} = new {req_type} {{ Url = {} }};", arg.name));
parts.push(req_var);
} else {
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 = if let Some(v) = input.get(field).filter(|v| !v.is_null()) {
v.clone()
} else {
crate::e2e::codegen::resolve_urls_field(input, &arg.field).clone()
};
let paths: Vec<String> = if let Some(arr) = val.as_array() {
arr.iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{}\"", escape_csharp(s))))
.collect()
} else {
Vec::new()
};
let paths_literal = paths.join(", ");
let name = &arg.name;
setup_lines.push(format!(
"var _pfBase_{name} = Environment.GetEnvironmentVariable(\"{env_key}\");"
));
setup_lines.push(format!(
"var _base_{name} = !string.IsNullOrEmpty(_pfBase_{name}) ? _pfBase_{name} : Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";"
));
setup_lines.push(format!(
"var {name} = new System.Collections.Generic.List<string>(new string[] {{ {paths_literal} }}.Select(p => p.StartsWith(\"http\") ? p : _base_{name} + p));"
));
parts.push(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())
{
let config_type = resolve_handle_config_type(arg, options_type, type_defs);
let default_config = if let Some(ctype) = &config_type {
if is_default_constructible(ctype, type_defs) {
format!("new {ctype}()")
} else {
"null".to_string()
}
} else {
"null".to_string()
};
setup_lines.push(format!(
"var {} = {class_name}.{constructor_name}({default_config});",
arg.name,
));
} else {
let sorted = sort_discriminator_first(config_value.clone());
let json_str = serde_json::to_string(&sorted).unwrap_or_default();
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 = JsonSerializer.Deserialize<{config_type}>(\"{}\", ConfigOptions)!;",
escape_csharp(&json_str),
));
setup_lines.push(format!(
"var {} = {class_name}.{constructor_name}({name}Config);",
arg.name,
name = name,
));
} else {
setup_lines.push(format!("var {} = {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) {
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 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 {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
}
let enum_names: std::collections::HashSet<&str> = enums.iter().map(|e| e.name.as_str()).collect();
let excluded_named = crate::e2e::codegen::recipe::trait_bridge_excluded_type_names_with_enums(
config,
type_defs,
&methods,
&enum_names,
);
let emission =
emit_test_backend_with_class_name(trait_bridge, &methods, fixture, class_name, &excluded_named);
class_decls.push(emission.setup_block);
parts.push(emission.arg_expr);
if !emission.teardown_block.is_empty() {
teardown_lines.push(emission.teardown_block);
}
continue;
}
}
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("csharp");
setup_lines.push(format!("// {}", emission.arg_expr));
parts.push("null".to_string());
continue;
}
let val: Option<&serde_json::Value> = if arg.field == "input" {
Some(input)
} else {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
input.get(field)
};
match val {
None | Some(serde_json::Value::Null) => {
let default_val = match arg.arg_type.as_str() {
"string" if arg.optional => "null".to_string(),
"string" => "\"\"".to_string(),
"int" | "integer" => "0".to_string(),
"float" | "number" => "0.0d".to_string(),
"bool" | "boolean" => "false".to_string(),
"json_object" => {
if options_via == Some("from_json") {
if let Some(opts_type) = options_type {
format!("{opts_type}.FromJson(\"{{}}\")")
} else {
resolve_json_object_default(
options_type,
&arg.element_type,
&arg.name,
type_defs,
options_via,
)
}
} else {
resolve_json_object_default(
options_type,
&arg.element_type,
&arg.name,
type_defs,
options_via,
)
}
}
_ => "null".to_string(),
};
parts.push(default_val);
}
Some(v) => {
if arg.arg_type == "json_object" {
if options_via == Some("from_json")
&& let Some(opts_type) = options_type
{
let sorted = sort_discriminator_first(v.clone());
let json_str = serde_json::to_string(&sorted).unwrap_or_default();
let escaped = escape_csharp(&json_str);
parts.push(format!("{opts_type}.FromJson(\"{escaped}\")",));
continue;
}
if let Some(arr) = v.as_array() {
parts.push(json_array_to_csharp_list(arr, arg.element_type.as_deref()));
continue;
}
if let Some(opts_type) = options_type {
if let Some(obj) = v.as_object() {
parts.push(csharp_object_initializer(
obj,
opts_type,
enum_fields,
nested_types,
type_defs,
));
continue;
}
}
}
parts.push(json_to_csharp(v));
}
}
}
(setup_lines, parts.join(", "))
}
fn is_default_constructible(type_name: &str, type_defs: &[crate::core::ir::TypeDef]) -> bool {
type_defs.iter().find(|ty| ty.name == type_name).is_some_and(|ty| {
ty.fields.is_empty() || ty.fields.iter().all(|field| field.optional || field.default.is_some())
})
}
fn resolve_json_object_default(
options_type: Option<&str>,
element_type: &Option<String>,
param_name: &str,
type_defs: &[crate::core::ir::TypeDef],
options_via: Option<&str>,
) -> String {
if let Some(opts_type) = options_type {
if is_default_constructible(opts_type, type_defs) {
if options_via == Some("from_json") {
return format!("{opts_type}.FromJson(\"{{}}\")");
}
return format!("new {opts_type}()");
}
}
if let Some(elem_type) = element_type {
if is_default_constructible(elem_type, type_defs) {
if options_via == Some("from_json") {
return format!("{elem_type}.FromJson(\"{{}}\")");
}
return format!("new {elem_type}()");
}
}
let name_upper = param_name.to_upper_camel_case();
let candidates = [
name_upper.clone(),
format!("{name_upper}Config"),
format!("{name_upper}Options"),
format!("{name_upper}Settings"),
];
let format_with_via = |type_name: &str| {
if options_via == Some("from_json") {
format!("{type_name}.FromJson(\"{{}}\")")
} else {
format!("new {type_name}()")
}
};
if let Some(inferred) = candidates
.iter()
.find(|cand| is_default_constructible(cand, type_defs))
.cloned()
{
return format_with_via(&inferred);
}
if let Some(opts_type) = options_type {
return format_with_via(opts_type);
}
"null".to_string()
}
fn json_array_to_csharp_list(arr: &[serde_json::Value], element_type: Option<&str>) -> String {
match element_type {
Some("f32") => {
let items: Vec<String> = arr.iter().map(|v| format!("(float){}", json_to_csharp(v))).collect();
format!("new List<float>() {{ {} }}", items.join(", "))
}
Some("(String, String)") => {
let items: Vec<String> = arr
.iter()
.map(|v| {
let strs: Vec<String> = v
.as_array()
.map_or_else(Vec::new, |a| a.iter().map(json_to_csharp).collect());
format!("new List<string>() {{ {} }}", strs.join(", "))
})
.collect();
format!("new List<List<string>>() {{ {} }}", items.join(", "))
}
Some(et) if et != "f32" && et != "(String, String)" && et != "string" => {
let items: Vec<String> = arr
.iter()
.map(|v| {
let json_str = serde_json::to_string(v).unwrap_or_default();
let escaped = escape_csharp(&json_str);
format!("JsonSerializer.Deserialize<{et}>(\"{escaped}\", ConfigOptions)!")
})
.collect();
format!("new List<{et}>() {{ {} }}", items.join(", "))
}
_ => {
let items: Vec<String> = arr.iter().map(json_to_csharp).collect();
format!("new List<string>() {{ {} }}", items.join(", "))
}
}
}
fn sort_discriminator_first(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut sorted = serde_json::Map::with_capacity(map.len());
if let Some(type_val) = map.get("type") {
sorted.insert("type".to_string(), sort_discriminator_first(type_val.clone()));
}
for (k, v) in map {
if k != "type" {
sorted.insert(k, sort_discriminator_first(v));
}
}
serde_json::Value::Object(sorted)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(sort_discriminator_first).collect())
}
other => other,
}
}
fn csharp_object_initializer(
obj: &serde_json::Map<String, serde_json::Value>,
type_name: &str,
enum_fields: &HashMap<String, String>,
nested_types: &HashMap<String, String>,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
if obj.is_empty() {
return format!("new {type_name}()");
}
static IMPLICIT_ENUM_FIELDS: &[(&str, &str)] = &[("output_format", "OutputFormat")];
let props: Vec<String> = obj
.iter()
.map(|(key, val)| {
let pascal_key = key.to_upper_camel_case();
let implicit_enum_type = IMPLICIT_ENUM_FIELDS
.iter()
.find(|(k, _)| *k == key.as_str())
.map(|(_, t)| *t);
let camel_key = key.to_lower_camel_case();
let cs_val = if let Some(enum_type) = enum_fields
.get(key.as_str())
.or_else(|| enum_fields.get(camel_key.as_str()))
.map(String::as_str)
.or(implicit_enum_type)
{
if val.is_null() {
"null".to_string()
} else {
let member = val
.as_str()
.map(|s| s.to_upper_camel_case())
.unwrap_or_else(|| "null".to_string());
format!("{enum_type}.{member}")
}
} else if let Some(field_type) = resolve_csharp_field_type_from_struct(type_name, key, type_defs) {
let normalized = normalize_csharp_enum_values(val, enum_fields);
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
let escaped = escape_csharp(&json_str);
format!("JsonSerializer.Deserialize<{field_type}>(\"{escaped}\", ConfigOptions)!")
} else if let Some(nested_type) = nested_types
.get(key.as_str())
.or_else(|| nested_types.get(camel_key.as_str()))
{
let normalized = normalize_csharp_enum_values(val, enum_fields);
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
let escaped = escape_csharp(&json_str);
format!("JsonSerializer.Deserialize<{nested_type}>(\"{escaped}\", ConfigOptions)!")
} else if let Some(arr) = val.as_array() {
let items: Vec<String> = arr.iter().map(json_to_csharp).collect();
format!("new List<string> {{ {} }}", items.join(", "))
} else {
json_to_csharp(val)
};
format!("{pascal_key} = {cs_val}")
})
.collect();
format!("new {} {{ {} }}", type_name, props.join(", "))
}
fn resolve_csharp_field_type_from_struct(
struct_name: &str,
field_key: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
let struct_def = type_defs.iter().find(|td| td.name == struct_name)?;
let field_name = field_key;
let field = struct_def.fields.iter().find(|f| f.name == field_name)?;
match &field.ty {
crate::core::ir::TypeRef::Named(name) => Some(name.clone()),
crate::core::ir::TypeRef::Optional(inner) => match inner.as_ref() {
crate::core::ir::TypeRef::Named(name) => Some(name.clone()),
_ => None,
},
_ => None,
}
}
fn normalize_csharp_enum_values(value: &serde_json::Value, enum_fields: &HashMap<String, String>) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut result = map.clone();
for (key, val) in result.iter_mut() {
let camel_key = key.to_lower_camel_case();
if enum_fields.contains_key(key) || enum_fields.contains_key(camel_key.as_str()) {
if let Some(s) = val.as_str() {
*val = serde_json::Value::String(s.to_lowercase());
}
}
}
serde_json::Value::Object(result)
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{FieldDef, TypeDef, TypeRef};
#[test]
fn test_resolve_json_object_default_with_default_constructible_type() {
let my_config = TypeDef {
name: "MyConfig".to_string(),
rust_path: "crate::MyConfig".to_string(),
fields: vec![
FieldDef {
name: "timeout".to_string(),
ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::U32),
optional: true,
default: None,
doc: String::new(),
..FieldDef::default()
},
FieldDef {
name: "enabled".to_string(),
ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool),
optional: false,
default: Some("true".to_string()),
doc: String::new(),
..FieldDef::default()
},
],
..TypeDef::default()
};
let type_defs = vec![my_config];
let result = resolve_json_object_default(None, &None, "my", &type_defs, None);
assert_eq!(result, "new MyConfig()", "Expected default construction of MyConfig");
}
#[test]
fn test_resolve_json_object_default_with_from_json_factory() {
let extraction_config = TypeDef {
name: "ExtractionConfig".to_string(),
rust_path: "crate::ExtractionConfig".to_string(),
fields: vec![],
..TypeDef::default()
};
let type_defs = vec![extraction_config];
let result =
resolve_json_object_default(Some("ExtractionConfig"), &None, "config", &type_defs, Some("from_json"));
assert_eq!(
result, "ExtractionConfig.FromJson(\"{}\")",
"Expected factory method for from_json"
);
let result2 = resolve_json_object_default(Some("ExtractionConfig"), &None, "config", &type_defs, None);
assert_eq!(
result2, "new ExtractionConfig()",
"Expected default constructor without from_json"
);
}
#[test]
fn test_resolve_json_object_default_with_non_default_constructible_type() {
let required_config = TypeDef {
name: "RequiredConfig".to_string(),
rust_path: "crate::RequiredConfig".to_string(),
fields: vec![FieldDef {
name: "api_key".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
doc: String::new(),
..FieldDef::default()
}],
..TypeDef::default()
};
let type_defs = vec![required_config];
let result = resolve_json_object_default(None, &None, "config", &type_defs, None);
assert_eq!(result, "null", "Expected null for non-default-constructible type");
}
#[test]
fn test_resolve_json_object_default_prefers_explicit_type() {
let my_config = TypeDef {
name: "MyConfig".to_string(),
rust_path: "crate::MyConfig".to_string(),
fields: vec![],
..TypeDef::default()
};
let fallback_config = TypeDef {
name: "Config".to_string(),
rust_path: "crate::Config".to_string(),
fields: vec![],
..TypeDef::default()
};
let type_defs = vec![my_config, fallback_config];
let result = resolve_json_object_default(Some("MyConfig"), &None, "config", &type_defs, None);
assert_eq!(result, "new MyConfig()", "Expected explicit MyConfig");
}
#[test]
fn test_resolve_json_object_default_with_element_type() {
let elem_config = TypeDef {
name: "ElemConfig".to_string(),
rust_path: "crate::ElemConfig".to_string(),
fields: vec![],
..TypeDef::default()
};
let type_defs = vec![elem_config];
let result = resolve_json_object_default(None, &Some("ElemConfig".to_string()), "other", &type_defs, None);
assert_eq!(result, "new ElemConfig()", "Expected ElemConfig from element_type");
}
}