use crate::core::ir::{EnumDef, EnumVariant, FieldDef, FunctionDef, TypeDef, TypeRef};
use crate::e2e::codegen::call_ir::{CallIr, resolve_declared_result_type};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
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 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 call_config_for(call: &str) -> CallConfig {
CallConfig {
function: call.to_string(),
result_var: "result".to_string(),
..CallConfig::default()
}
}
fn field_resolver_for(
call_config: &CallConfig,
e2e_config: &E2eConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[FunctionDef],
) -> FieldResolver {
let call_root_type = resolve_declared_result_type(call_config, "ruby", CallIr { functions, type_defs });
FieldResolver::new(
e2e_config.effective_fields(call_config),
e2e_config.effective_fields_optional(call_config),
e2e_config.effective_result_fields(call_config),
e2e_config.effective_fields_array(call_config),
&std::collections::HashSet::new(),
)
.with_enum_fields(e2e_config.effective_fields_enum(call_config).clone())
.with_ir_enum_map(FieldResolver::ir_enum_fields(type_defs, enums), call_root_type)
}
struct Case {
name: &'static str,
call: &'static str,
expect_enum: 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: true,
},
Case {
name: "a same-named non-enum field on an unrelated type is not misclassified as enum",
call: "other",
expect_enum: false,
},
Case {
name: "an Option<Enum> field is classified as enum via the IR",
call: "process_optional",
expect_enum: true,
},
];
#[test]
fn enum_field_classification_table() {
let (type_defs, enums, functions) = table_ir();
for case in CASES {
let call_config = call_config_for(case.call);
let e2e_config = E2eConfig::default();
let resolver = field_resolver_for(&call_config, &e2e_config, &type_defs, &enums, &functions);
assert_eq!(
resolver.is_enum("kind"),
case.expect_enum,
"{}: expected is_enum(\"kind\") = {}",
case.name,
case.expect_enum
);
}
}
#[test]
fn an_explicit_fields_enum_config_entry_still_classifies_as_enum() {
let (type_defs, enums, functions) = table_ir();
let call_config = call_config_for("other");
let mut e2e_config = E2eConfig::default();
e2e_config.fields_enum.insert("kind".to_string());
let resolver = field_resolver_for(&call_config, &e2e_config, &type_defs, &enums, &functions);
assert!(
resolver.is_enum("kind"),
"explicit fields_enum config must still classify the field as enum"
);
}
#[test]
fn render_spec_file_emits_a_real_assertion_for_the_unconfigured_enum_field() {
let (type_defs, enums, functions) = table_ir();
let call_config = call_config_for("process");
let mut e2e_config = E2eConfig::default();
e2e_config.calls.insert("process".to_string(), call_config);
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("key_value".to_string())),
..Assertion::default()
}],
..Fixture::default()
};
let out = super::spec_file::render_spec_file(
"kind_smoke",
&[&fixture],
"Sample",
None,
"sample",
None,
&std::collections::HashMap::new(),
false,
&e2e_config,
false,
false,
&[],
&crate::core::config::ResolvedCrateConfig::default(),
&type_defs,
&[],
&enums,
&functions,
);
assert!(
out.contains("expect(result.kind.to_s).to eq('key_value')"),
"got:\n{out}"
);
assert!(!out.contains("skipped"), "got:\n{out}");
}
#[test]
fn render_spec_file_embeds_the_verbatim_pascalcase_fixture_value_for_a_no_rename_all_enum() {
let (type_defs, enums, functions) = table_ir();
let call_config = call_config_for("process");
let mut e2e_config = E2eConfig::default();
e2e_config.calls.insert("process".to_string(), call_config);
let fixture = Fixture {
id: "kind_wire_shape".to_string(),
description: "Kind field matches the real no-rename_all wire contract".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 = super::spec_file::render_spec_file(
"kind_wire_shape",
&[&fixture],
"Sample",
None,
"sample",
None,
&std::collections::HashMap::new(),
false,
&e2e_config,
false,
false,
&[],
&crate::core::config::ResolvedCrateConfig::default(),
&type_defs,
&[],
&enums,
&functions,
);
assert!(
out.contains("expect(result.kind.to_s).to eq('KeyValue')"),
"the expected literal must stay verbatim PascalCase, not be snake_cased by codegen:\n{out}"
);
assert!(!out.contains("skipped"), "got:\n{out}");
}