use std::collections::HashMap;
use heck::ToUpperCamelCase;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, TypeDef};
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
pub(super) fn render_snippet_body(
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
) -> String {
let mut call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
call = crate::e2e::codegen::select_best_matching_call(call, e2e_config, fixture);
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve("csharp", fixture, call, type_defs);
let overrides = recipe.override_config;
let class_name = crate::codegen::naming::csharp_wrapper_class_name(&config.name, "");
let mut function_name = overrides
.and_then(|value| value.function.as_deref())
.unwrap_or(&call.function)
.to_upper_camel_case();
let is_async = overrides.and_then(|value| value.r#async).unwrap_or(call.r#async);
if is_async && !function_name.ends_with("Async") {
function_name.push_str("Async");
}
let options_type = recipe.options_type.or_else(|| {
e2e_config
.call
.overrides
.get("csharp")
.and_then(|value| value.options_type.as_deref())
});
let options_via = overrides
.and_then(|value| value.options_via.as_deref())
.filter(|value| *value != "from_json");
let mut visitor_declarations = Vec::new();
let mut teardown_lines = Vec::new();
let (setup_lines, mut args) = super::setup::build_args_and_setup(
&fixture.input,
recipe.args,
&class_name,
options_type,
options_via,
&HashMap::new(),
&HashMap::new(),
fixture,
None,
config,
type_defs,
enums,
&mut visitor_declarations,
&mut teardown_lines,
);
if !recipe.extra_args.is_empty() {
args = if args.is_empty() {
recipe.extra_args.join(", ")
} else {
format!("{args}, {}", recipe.extra_args.join(", "))
};
}
let client_factory = overrides
.and_then(|value| value.client_factory.as_deref())
.or_else(|| {
e2e_config
.call
.overrides
.get("csharp")
.and_then(|value| value.client_factory.as_deref())
})
.map(ToUpperCamelCase::to_upper_camel_case);
let namespace = overrides
.and_then(|value| value.module.clone())
.or_else(|| config.csharp.as_ref().and_then(|value| value.namespace.clone()))
.unwrap_or_else(|| config.name.to_upper_camel_case());
let returns_void = call.returns_void
|| matches!(call.function.as_str(), "initialize" | "shutdown")
|| ["register_", "unregister_", "clear_"]
.iter()
.any(|prefix| call.function.starts_with(prefix));
let needs_json = setup_lines.iter().any(|line| line.contains("JsonSerializer")) || args.contains("JsonSerializer");
let needs_system = client_factory.is_some() || setup_lines.iter().any(|line| line.contains("Environment."));
let needs_collections = setup_lines
.iter()
.any(|line| line.contains("List<") || line.contains("Dictionary<"));
crate::e2e::template_env::render(
"csharp/snippet_body.jinja",
minijinja::context! {
namespace => namespace,
setup_lines => setup_lines,
client_factory => client_factory,
class_name => class_name,
function_name => function_name,
args => args,
result_var => call.result_var,
returns_void => returns_void,
is_async => is_async,
needs_json => needs_json,
needs_system => needs_system,
needs_collections => needs_collections,
fixture_id => fixture.id,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::{CallConfig, CallOverride};
#[test]
fn snippet_keeps_async_native_call_without_xunit_harness() {
let fixture = Fixture {
id: "quick_start".into(),
description: "Quick start".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "load_document".into(),
result_var: "document".into(),
r#async: true,
..CallConfig::default()
};
call.overrides.insert("csharp".into(), CallOverride::default());
let config = ResolvedCrateConfig {
name: "sample_core".into(),
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&config,
&[],
&[],
);
assert!(body.contains("await SampleCoreConverter.LoadDocumentAsync()"));
assert!(!body.contains("using System.Collections.Generic;"));
assert!(!body.contains("using System;"));
assert!(!body.contains("[Fact]"));
assert!(!body.contains("Assert."));
}
#[test]
fn snippet_constructs_known_dto_without_json_round_trip() {
let fixture = Fixture {
id: "typed_input".into(),
description: "Typed input".into(),
input: serde_json::json!({"payload": {"label": "sample"}}),
..Fixture::default()
};
let mut call = CallConfig {
function: "process".into(),
args: vec![crate::e2e::config::ArgMapping {
name: "payload".into(),
field: "input.payload".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: Some("SampleInput".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
};
call.overrides.insert(
"csharp".into(),
CallOverride {
options_via: Some("from_json".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[TypeDef {
name: "SampleInput".into(),
fields: vec![crate::core::ir::FieldDef {
name: "label".into(),
ty: crate::core::ir::TypeRef::String,
..Default::default()
}],
..TypeDef::default()
}],
&[],
);
assert!(body.contains("new SampleInput { Label = \"sample\" }"), "{body}");
assert!(!body.contains("FromJson"), "{body}");
assert!(!body.contains("JsonSerializer"), "{body}");
}
}