use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, FunctionDef, TypeDef, TypeRef};
use crate::e2e::config::{CallConfig, E2eConfig, StreamingConfig};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
use std::collections::HashMap;
fn data_node_kind_enum() -> EnumDef {
EnumDef {
name: "DataNodeKind".to_string(),
variants: vec![
EnumVariant {
name: "KeyValue".to_string(),
..EnumVariant::default()
},
EnumVariant {
name: "Sequence".to_string(),
..EnumVariant::default()
},
],
..EnumDef::default()
}
}
fn kind_field(ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: "kind".to_string(),
ty,
optional,
..FieldDef::default()
}
}
fn equals_kind_assertion(expected: &str) -> Assertion {
Assertion {
assertion_type: "equals".to_string(),
field: Some("kind".to_string()),
value: Some(serde_json::Value::String(expected.to_string())),
..Assertion::default()
}
}
fn fixture_calling(call: &str) -> Fixture {
Fixture {
id: "kind_smoke".to_string(),
description: "Kind field smoke".to_string(),
call: Some(call.to_string()),
assertions: vec![equals_kind_assertion("KeyValue")],
..Fixture::default()
}
}
#[allow(clippy::too_many_arguments)]
fn render(
fixture: &Fixture,
e2e_config: &E2eConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[FunctionDef],
) -> String {
let field_resolver = FieldResolver::new(
&HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let mut out = String::new();
let mut visitor_class_decls: Vec<String> = Vec::new();
super::render_test_method(
&mut out,
&mut visitor_class_decls,
fixture,
"Sample",
"Process",
"SampleException",
"result",
&[],
&field_resolver,
false,
false,
e2e_config,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&[],
&config,
type_defs,
enums,
functions,
&[],
);
out
}
fn table_ir() -> (Vec<TypeDef>, Vec<EnumDef>, Vec<FunctionDef>) {
let type_defs = vec![
TypeDef {
name: "ProcessResult".to_string(),
fields: vec![kind_field(TypeRef::Named("DataNodeKind".to_string()), false)],
..TypeDef::default()
},
TypeDef {
name: "OtherResult".to_string(),
fields: vec![kind_field(TypeRef::String, false)],
..TypeDef::default()
},
TypeDef {
name: "OptionalResult".to_string(),
fields: vec![kind_field(
TypeRef::Optional(Box::new(TypeRef::Named("DataNodeKind".to_string()))),
true,
)],
..TypeDef::default()
},
];
let enums = vec![data_node_kind_enum()];
let functions = vec![
FunctionDef {
name: "process".to_string(),
return_type: TypeRef::Named("ProcessResult".to_string()),
..FunctionDef::default()
},
FunctionDef {
name: "other".to_string(),
return_type: TypeRef::Named("OtherResult".to_string()),
..FunctionDef::default()
},
FunctionDef {
name: "process_optional".to_string(),
return_type: TypeRef::Named("OptionalResult".to_string()),
..FunctionDef::default()
},
];
(type_defs, enums, functions)
}
fn e2e_config_for(call: &str, function: &str, extra: impl FnOnce(&mut CallConfig)) -> E2eConfig {
let mut call_config = CallConfig {
function: function.to_string(),
..CallConfig::default()
};
extra(&mut call_config);
let mut e2e_config = E2eConfig::default();
e2e_config.calls.insert(call.to_string(), call_config);
e2e_config
}
struct Case {
name: &'static str,
call: &'static str,
expect_enum_branch: bool,
}
const CASES: &[Case] = &[
Case {
name: "an enum-typed field with no fields_enum config is classified as enum via the IR",
call: "process",
expect_enum_branch: true,
},
Case {
name: "a same-named non-enum field on an unrelated type is not misclassified as enum",
call: "other",
expect_enum_branch: false,
},
Case {
name: "an Option<Enum> field is classified as enum via the IR",
call: "process_optional",
expect_enum_branch: true,
},
];
#[test]
fn enum_field_classification_table() {
let (type_defs, enums, functions) = table_ir();
for case in CASES {
let e2e_config = e2e_config_for(case.call, case.call, |_| {});
let fixture = fixture_calling(case.call);
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
let took_enum_branch = out.contains("System.Text.Json.JsonSerializer.Serialize");
assert_eq!(
took_enum_branch, case.expect_enum_branch,
"{}: expected enum branch = {}, got:\n{out}",
case.name, case.expect_enum_branch
);
}
}
#[test]
fn enum_equals_assertion_compares_the_real_wire_value_unmodified() {
let (type_defs, enums, functions) = table_ir();
let e2e_config = e2e_config_for("process", "process", |_| {});
let fixture = fixture_calling("process");
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
assert!(
out.contains(r#"Assert.Equal("KeyValue", "#),
"expected the fixture's wire literal verbatim (no lowercasing), got:\n{out}"
);
assert!(
!out.contains("\"keyvalue\""),
"must not lowercase the expected literal into a value the real wire format never produces, got:\n{out}"
);
assert!(
!out.contains("JsonNamingPolicy"),
"must not guess a naming-policy transform; it must serialize through the enum's own converter, got:\n{out}"
);
assert!(
out.contains(
"System.Text.Json.JsonSerializer.Serialize(result.Kind, new System.Text.Json.JsonSerializerOptions { \
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }).Trim('\"')"
),
"expected the actual value serialized through the enum's own JsonConverter with \
HTML-safe escaping disabled (otherwise wire values containing `+ ' < > &` mismatch \
the plain-string expectation), got:\n{out}"
);
}
#[test]
fn an_explicit_fields_enum_config_entry_still_classifies_as_enum() {
let (type_defs, enums, functions) = table_ir();
let e2e_config = e2e_config_for("other", "other", |call| {
call.overrides.insert(
"csharp".to_string(),
crate::e2e::config::CallOverride {
enum_fields: [("kind".to_string(), "DataNodeKind".to_string())].into_iter().collect(),
..crate::e2e::config::CallOverride::default()
},
);
});
let fixture = fixture_calling("other");
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
assert!(
out.contains("System.Text.Json.JsonSerializer.Serialize"),
"explicit fields_enum config must still classify the field as enum, got:\n{out}"
);
}
#[test]
fn a_forced_streaming_call_still_routes_to_the_streaming_branch_unaffected_by_enum_wiring() {
let (type_defs, enums, functions) = table_ir();
let e2e_config = e2e_config_for("process", "process", |call| {
call.streaming = Some(StreamingConfig::Enabled(true));
});
let fixture = fixture_calling("process");
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
assert!(
!out.contains("System.Text.Json.JsonSerializer.Serialize") && !out.contains("result.Kind"),
"a forced-streaming call must not emit the non-streaming field-assertion path, got:\n{out}"
);
}