use crate::codegen::naming::{csharp_type_name, to_csharp_name, wire_variant_value};
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, TypeRef};
use crate::e2e::codegen::call_ir::TargetParams;
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::values::{
json_array_to_csharp_list, resolve_csharp_field_element_type_from_struct, resolve_csharp_field_type_from_struct,
};
use super::{classify_bytes_value_csharp, json_to_csharp, resolve_handle_config_type};
fn json_object_csharp_type<'a>(
arg: &'a crate::e2e::config::ArgMapping,
options_type: Option<&'a str>,
value: &serde_json::Value,
) -> Option<&'a str> {
crate::e2e::codegen::recipe::json_object_constructor_type(arg, options_type, value)
}
#[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],
target_params: TargetParams<'_>,
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 (index, arg) in args.iter().enumerate() {
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" {
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) {
setup_lines.push(format!("var {} = \"{}\";", arg.name, escape_csharp(url)));
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 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()
};
if let Some(urls) = crate::e2e::codegen::preserved_url_list(fixture.preserve_input_urls, &val) {
let literals: Vec<String> = urls.iter().map(|url| format!("\"{}\"", escape_csharp(url))).collect();
let name = &arg.name;
setup_lines.push(format!(
"var {name} = new System.Collections.Generic.List<string>(new string[] {{ {} }});",
literals.join(", ")
));
parts.push(name.clone());
continue;
}
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
&& 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;
}
panic!(
"C# 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 C# stub without a resolvable trait bridge",
fixture.id, arg.name, arg.trait_name
);
}
let val: Option<&serde_json::Value> = if arg.field == "input" {
Some(input.get("extract_input").unwrap_or(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) =
json_object_csharp_type(arg, options_type, &serde_json::Value::Null)
{
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" {
let json_object_type = json_object_csharp_type(arg, options_type, v);
if options_via == Some("from_json")
&& let Some(opts_type) = json_object_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);
if crate::e2e::codegen::value_contains_mock_url_placeholder(&sorted) {
let env_key = crate::e2e::codegen::mock_url_env_key(fixture_id);
let base_var = format!("{}_MockBaseUrl", arg.name.to_upper_camel_case());
let json_var = format!("{}_Json", arg.name.to_upper_camel_case());
setup_lines.push(format!(
"var {base_var} = Environment.GetEnvironmentVariable(\"{env_key}\") ?? Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";"
));
setup_lines.push(format!(
"var {json_var} = \"{escaped}\".Replace(\"{}\", {base_var});",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
parts.push(format!("{opts_type}.FromJson({json_var})"));
} else {
parts.push(format!("{opts_type}.FromJson(\"{escaped}\")",));
}
continue;
}
if let Some(arr) = v.as_array() {
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!("{}_MockBaseUrl", arg.name.to_upper_camel_case());
let json_var = format!("{}_Json", arg.name.to_upper_camel_case());
let json_str = serde_json::to_string(v).unwrap_or_default();
let escaped = escape_csharp(&json_str);
let element_type = arg.element_type.as_deref().unwrap_or("object");
setup_lines.push(format!(
"var {base_var} = Environment.GetEnvironmentVariable(\"{env_key}\") ?? Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";"
));
setup_lines.push(format!(
"var {json_var} = \"{escaped}\".Replace(\"{}\", {base_var});",
crate::e2e::codegen::MOCK_URL_PLACEHOLDER
));
parts.push(format!(
"JsonSerializer.Deserialize<List<{element_type}>>({json_var}, ConfigOptions)!"
));
continue;
}
parts.push(json_array_to_csharp_list(arr, arg.element_type.as_deref()));
continue;
}
if let Some(opts_type) = json_object_type
&& let Some(obj) = v.as_object()
{
parts.push(csharp_object_initializer(
obj,
opts_type,
enum_fields,
nested_types,
type_defs,
&fixture.docs_files_for_arg(&arg.field),
"",
));
continue;
}
}
if let Some(typed) = ir_typed_csharp_expression(arg, index, v, target_params, type_defs, enums) {
parts.push(typed);
continue;
}
parts.push(json_to_csharp(v));
}
}
}
(setup_lines, parts.join(", "))
}
fn ir_typed_csharp_expression(
arg: &crate::e2e::config::ArgMapping,
index: usize,
value: &serde_json::Value,
target_params: TargetParams<'_>,
type_defs: &[crate::core::ir::TypeDef],
enums: &[EnumDef],
) -> Option<String> {
let TypeRef::Named(declared) = &target_params.param_for(&arg.name, index)?.ty else {
return None;
};
if let Some(text) = value.as_str()
&& let Some(enum_def) = enums
.iter()
.find(|enum_def| &enum_def.name == declared && enum_def.variants.iter().all(|v| v.fields.is_empty()))
{
let variant = enum_def
.variants
.iter()
.find(|variant| csharp_enum_wire_value(enum_def, variant) == text)?;
return Some(format!(
"{}.{}",
csharp_type_name(declared),
to_csharp_name(&variant.name)
));
}
if value.is_object() && type_defs.iter().any(|type_def| &type_def.name == declared) {
let json = serde_json::to_string(value).unwrap_or_default();
return Some(format!(
"JsonSerializer.Deserialize<{}>(\"{}\", ConfigOptions)!",
csharp_type_name(declared),
escape_csharp(&json)
));
}
None
}
fn csharp_enum_wire_value(enum_def: &EnumDef, variant: &crate::core::ir::EnumVariant) -> String {
variant
.serde_rename
.clone()
.unwrap_or_else(|| wire_variant_value(&variant.name, None, enum_def.serde_rename_all.as_deref()))
}
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
&& 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
&& 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 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,
}
}
pub(super) 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],
files: &[crate::e2e::fixture::FixtureDocsFileInput],
pointer: &str,
) -> String {
if obj.is_empty() {
return format!("new {}()", csharp_type_name(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 field_pointer = format!("{pointer}/{key}");
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 files.iter().any(|file| file.field == field_pointer) {
format!(
"System.IO.File.ReadAllBytes(\"{}\")",
escape_csharp(val.as_str().unwrap_or_default())
)
} else 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) {
if let Some(object) = val.as_object()
&& type_defs.iter().any(|definition| definition.name == field_type)
{
csharp_object_initializer(
object,
&field_type,
enum_fields,
nested_types,
type_defs,
files,
&field_pointer,
)
} else {
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);
let field_type = csharp_type_name(&field_type);
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 element_type = resolve_csharp_field_element_type_from_struct(type_name, key, type_defs);
json_array_to_csharp_list(arr, element_type.as_deref())
} else {
json_to_csharp(val)
};
format!("{pascal_key} = {cs_val}")
})
.collect();
format!("new {} {{ {} }}", csharp_type_name(type_name), props.join(", "))
}
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 an_empty_bytes_field_in_an_object_initializer_renders_a_byte_array_not_a_string_list() {
let type_defs = vec![TypeDef {
name: "ExtractConfig".to_string(),
fields: vec![FieldDef {
name: "payload".to_string(),
ty: TypeRef::Bytes,
..FieldDef::default()
}],
..TypeDef::default()
}];
let obj = serde_json::json!({ "payload": [] });
let obj = obj.as_object().expect("object").clone();
let rendered = csharp_object_initializer(
&obj,
"ExtractConfig",
&HashMap::new(),
&HashMap::new(),
&type_defs,
&[],
"",
);
assert_eq!(rendered, "new ExtractConfig { Payload = new byte[] { } }");
assert!(!rendered.contains("List<string>"), "{rendered}");
}
#[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");
}
}