use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, FunctionDef, TypeDef, TypeRef};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::fixture::{Assertion, Fixture};
use super::test_method::render_test_method;
const ENUM_MARKER: &str = ".getValue()";
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 stage_output_union() -> EnumDef {
EnumDef {
name: "StageOutput".to_string(),
variants: vec![EnumVariant {
name: "Text".to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
is_tuple: true,
..EnumVariant::default()
}],
serde_untagged: true,
..EnumDef::default()
}
}
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()
},
TypeDef {
name: "UnionResult".to_string(),
fields: vec![kind_field(TypeRef::Named("StageOutput".to_string()), false)],
..TypeDef::default()
},
];
let enums = vec![data_node_kind_enum(), stage_output_union()];
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()
},
FunctionDef {
name: "process_union".to_string(),
return_type: TypeRef::Named("UnionResult".to_string()),
..FunctionDef::default()
},
];
(type_defs, enums, functions)
}
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![Assertion {
assertion_type: "equals".to_string(),
field: Some("kind".to_string()),
value: Some(serde_json::Value::String("key_value".to_string())),
..Assertion::default()
}],
..Fixture::default()
}
}
fn e2e_config_for(call: &str, extra: impl FnOnce(&mut CallConfig)) -> E2eConfig {
let mut call_config = CallConfig {
function: call.to_string(),
result_var: "result".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
}
fn render(
fixture: &Fixture,
e2e_config: &E2eConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[FunctionDef],
) -> String {
let mut out = String::new();
render_test_method(
&mut out,
fixture,
"Facade",
"",
"",
&[],
None,
false,
e2e_config,
&std::collections::HashMap::new(),
false,
&ResolvedCrateConfig::default(),
type_defs,
enums,
functions,
)
.expect("render_test_method succeeds");
out
}
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, |_| {});
let fixture = fixture_calling(case.call);
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
let took_enum_branch = out.contains(ENUM_MARKER);
assert_eq!(
took_enum_branch, case.expect_enum_branch,
"{}: expected enum branch = {}, got:\n{out}",
case.name, case.expect_enum_branch
);
assert_eq!(
out.contains("assertEquals(\"key_value\", result.kind())"),
!case.expect_enum_branch,
"{}: a raw string comparison (no .getValue()) must be emitted only for the \
non-enum field, got:\n{out}",
case.name
);
}
}
#[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", |_| {});
let mut e2e_config = e2e_config;
e2e_config.fields_enum.insert("kind".to_string());
let fixture = fixture_calling("other");
let out = render(&fixture, &e2e_config, &type_defs, &enums, &functions);
assert!(
out.contains(ENUM_MARKER),
"explicit fields_enum config must still classify the field as enum, got:\n{out}"
);
}
#[test]
fn kotlin_android_enum_equals_assertion_uses_to_wire_not_name_lowercase() {
let (type_defs, enums, functions) = table_ir();
let e2e_config = e2e_config_for("process", |_| {});
let fixture = Fixture {
id: "kind_smoke".to_string(),
description: "Kind field smoke".to_string(),
call: Some("process".to_string()),
assertions: vec![Assertion {
assertion_type: "equals".to_string(),
field: Some("kind".to_string()),
value: Some(serde_json::Value::String("KeyValue".to_string())),
..Assertion::default()
}],
..Fixture::default()
};
let out = render_android(&fixture, &e2e_config, &type_defs, &enums, &functions);
assert!(
out.contains(".toWire()"),
"expected .toWire() for a kotlin_android enum field, got:\n{out}"
);
assert!(
out.contains("\"KeyValue\""),
"expected the fixture's wire literal verbatim (no case transform), got:\n{out}"
);
assert!(
!out.contains(".lowercase()"),
"kotlin_android enum equals assertions must not guess a case transform, got:\n{out}"
);
}
fn render_android(
fixture: &Fixture,
e2e_config: &E2eConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[FunctionDef],
) -> String {
let mut out = String::new();
render_test_method(
&mut out,
fixture,
"Facade",
"",
"",
&[],
None,
false,
e2e_config,
&std::collections::HashMap::new(),
true,
&ResolvedCrateConfig::default(),
type_defs,
enums,
functions,
)
.expect("render_test_method succeeds");
out
}
const JVM_UNIT_ENUM_ASSERTION: &str = " assertEquals(\"key_value\", result.kind().getValue())";
const ANDROID_UNIT_ENUM_ASSERTION: &str = " assertEquals(\"key_value\", result.kind.toWire())";
const UNION_SKIP_LINE: &str = " // skipped: enum field 'kind' is a payload-carrying union \
with no scalar wire accessor in this binding";
#[test]
fn a_unit_only_enum_field_still_lowers_to_its_exact_scalar_comparison_in_each_target() {
let (type_defs, enums, functions) = table_ir();
let unit_config = e2e_config_for("process", |_| {});
let unit_fixture = fixture_calling("process");
let jvm = render(&unit_fixture, &unit_config, &type_defs, &enums, &functions);
assert!(
jvm.contains(JVM_UNIT_ENUM_ASSERTION),
"expected exactly `{JVM_UNIT_ENUM_ASSERTION}`, got:\n{jvm}"
);
let android = render_android(&unit_fixture, &unit_config, &type_defs, &enums, &functions);
assert!(
android.contains(ANDROID_UNIT_ENUM_ASSERTION),
"expected exactly `{ANDROID_UNIT_ENUM_ASSERTION}`, got:\n{android}"
);
}
#[test]
fn a_payload_carrying_union_field_renders_a_registered_refusal_in_each_target() {
let (type_defs, enums, functions) = table_ir();
let union_config = e2e_config_for("process_union", |_| {});
let union_fixture = fixture_calling("process_union");
for (target, out) in [
(
"kotlin_android",
render_android(&union_fixture, &union_config, &type_defs, &enums, &functions),
),
(
"kotlin/JVM",
render(&union_fixture, &union_config, &type_defs, &enums, &functions),
),
] {
assert!(
out.contains(UNION_SKIP_LINE),
"{target}: expected exactly `{UNION_SKIP_LINE}`, got:\n{out}"
);
assert!(
!out.contains(".toWire()") && !out.contains(ENUM_MARKER),
"{target}: neither scalar accessor exists on the sealed class, got:\n{out}"
);
assert!(
!out.contains("\"key_value\""),
"{target}: the fixture literal must not be compared against anything — a wrapper \
object compared to a String is false at runtime for every fixture, got:\n{out}"
);
}
}
#[test]
fn an_externally_tagged_data_enum_is_refused_on_android_but_kept_on_the_jvm() {
use crate::e2e::codegen::payload_union_skip::{UnionLoweringTarget, lacks_scalar_wire_accessor};
use crate::e2e::field_access::FieldResolver;
let external = EnumDef {
name: "Payload".to_string(),
variants: vec![EnumVariant {
name: "Blob".to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
is_tuple: true,
..EnumVariant::default()
}],
..EnumDef::default()
};
let types = vec![TypeDef {
name: "Envelope".to_string(),
fields: vec![kind_field(TypeRef::Named("Payload".to_string()), false)],
..TypeDef::default()
}];
let enums = vec![external];
let resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
)
.with_ir_enum_map(
FieldResolver::ir_enum_fields(&types, &enums),
Some("Envelope".to_string()),
)
.with_java_wrapper_enum_names(
enums
.iter()
.filter(|enum_def| !crate::backends::java::gen_bindings::emits_get_value(enum_def))
.map(|enum_def| enum_def.name.clone())
.collect(),
);
assert!(
lacks_scalar_wire_accessor(&resolver, "kind", UnionLoweringTarget::KotlinAndroid),
"kotlin_android renders an externally tagged data enum as a sealed class with no toWire()"
);
assert!(
!lacks_scalar_wire_accessor(&resolver, "kind", UnionLoweringTarget::KotlinJvm),
"the Java facade folds an externally tagged data enum down to a plain enum that keeps \
getValue(), so the JVM target must NOT refuse it"
);
}