use super::*;
use crate::core::ir::{CoreWrapper, EnumDef, EnumVariant, FieldDef, TypeRef};
fn make_field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
version: Default::default(),
name: name.to_string(),
ty,
optional: false,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: CoreWrapper::None,
vec_inner_core_wrapper: CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
serde_with: None,
serde_skip_serializing_if: false,
serde_skip: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
}
fn make_variant(name: &str, serde_rename: Option<&str>, fields: Vec<FieldDef>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields,
doc: String::new(),
is_default: false,
serde_rename: serde_rename.map(str::to_string),
binding_excluded: false,
binding_exclusion_reason: None,
is_tuple: false,
originally_had_data_fields: false,
cfg: None,
version: Default::default(),
}
}
fn make_enum(
name: &str,
serde_tag: Option<&str>,
serde_untagged: bool,
serde_rename_all: Option<&str>,
variants: Vec<EnumVariant>,
) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: format!("crate::{name}"),
original_rust_path: format!("crate::{name}"),
variants,
methods: vec![],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: true,
serde_content: None,
serde_tag: serde_tag.map(str::to_string),
serde_untagged,
serde_rename_all: serde_rename_all.map(str::to_string),
rename_all_fields: None,
binding_excluded: false,
binding_exclusion_reason: None,
excluded_variants: vec![],
version: Default::default(),
has_default: false,
}
}
#[test]
fn emit_enum_tagged_sealed_class_emits_json_deserialize_annotation() {
let en = make_enum(
"Message",
Some("role"),
false,
None,
vec![
make_variant(
"System",
Some("system"),
vec![make_field("_0", TypeRef::Named("SystemMessage".to_string()))],
),
make_variant(
"User",
Some("user"),
vec![make_field("_0", TypeRef::Named("UserMessage".to_string()))],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = MessageDeserializer::class)"),
"missing @JsonDeserialize annotation on tagged sealed class; got:\n{out}",
);
assert!(
out.contains("private class MessageDeserializer"),
"missing MessageDeserializer class; got:\n{out}",
);
assert!(
out.contains("node.get(\"role\")"),
"deserializer must read the 'role' tag field; got:\n{out}",
);
assert!(
out.contains("\"system\" ->"),
"deserializer must dispatch on variant 'system'; got:\n{out}",
);
assert!(
out.contains("val tag = node.get(\"role\")?.asText()"),
"tagged deserializer must extract tag into separate variable; got:\n{out}",
);
assert!(
out.contains("val payload = (node.deepCopy() as com.fasterxml.jackson.databind.node.ObjectNode).apply { remove(\"role\") }"),
"tagged deserializer must strip tag field from payload via cast-safe deepCopy; got:\n{out}",
);
assert!(
out.contains("Message.System(ctx.readTreeAsValue<SystemMessage>(payload, SystemMessage::class.java))"),
"tagged deserializer must wrap readTreeAsValue<InnerType>(payload) in variant constructor for newtype; got:\n{out}",
);
}
#[test]
fn emit_enum_untagged_sealed_class_emits_json_deserialize_annotation() {
let en = make_enum(
"EmbeddingInput",
None,
true,
None,
vec![
make_variant("Single", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Multiple",
None,
vec![make_field("_0", TypeRef::Vec(Box::new(TypeRef::String)))],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains(
"@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = EmbeddingInputDeserializer::class)"
),
"missing @JsonDeserialize annotation on untagged sealed class; got:\n{out}",
);
assert!(
out.contains("private class EmbeddingInputDeserializer"),
"missing EmbeddingInputDeserializer class; got:\n{out}",
);
assert!(
out.contains("node.isTextual"),
"untagged deserializer must check isTextual for String variant; got:\n{out}",
);
assert!(
out.contains("node.isArray"),
"untagged deserializer must check isArray for List variant; got:\n{out}",
);
assert!(
out.contains("ctx.typeFactory.constructCollectionType(List::class.java, String::class.java)"),
"untagged deserializer must use constructCollectionType for List<String> variant; got:\n{out}",
);
assert!(
!out.contains("ctx.readTreeAsValue(node, List::class.java)"),
"untagged deserializer must NOT use raw List::class.java; got:\n{out}",
);
}
#[test]
fn tagged_deserializer_named_field_variant_no_double_wrap() {
let en = make_enum(
"InputDocument",
Some("type"),
false,
Some("snake_case"),
vec![
make_variant("Url", Some("url"), vec![make_field("url", TypeRef::String)]),
make_variant(
"Base64",
Some("base64"),
vec![
make_field("data", TypeRef::String),
make_field("media_type", TypeRef::Named("MediaType".to_string())),
],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains(
"\"base64\" -> ctx.readTreeAsValue<InputDocument.Base64>(payload, InputDocument.Base64::class.java)"
),
"tagged deserializer must return readTreeAsValue<T>(payload) directly for named-field variant; got:\n{out}",
);
assert!(
!out.contains("InputDocument.Base64(ctx.readTreeAsValue"),
"tagged deserializer must NOT wrap readTreeAsValue result in variant constructor; got:\n{out}",
);
}
#[test]
fn tagged_deserializer_newtype_variant_no_double_wrap() {
let en = make_enum(
"ContentPart",
Some("type"),
false,
Some("snake_case"),
vec![make_variant(
"Text",
Some("text"),
vec![make_field("_0", TypeRef::Named("TextContent".to_string()))],
)],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("ContentPart.Text(ctx.readTreeAsValue<TextContent>(payload, TextContent::class.java))"),
"tagged deserializer must wrap readTreeAsValue<InnerType>(payload) in variant constructor for newtype variant; got:\n{out}",
);
}
#[test]
fn untagged_deserializer_list_of_named_type_uses_java_type() {
let en = make_enum(
"UserContent",
None,
true,
None,
vec![
make_variant("Text", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Parts",
None,
vec![make_field(
"_0",
TypeRef::Vec(Box::new(TypeRef::Named("ContentPart".to_string()))),
)],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("ctx.typeFactory.constructCollectionType(List::class.java, ContentPart::class.java)"),
"untagged deserializer must use constructCollectionType for List<ContentPart>; got:\n{out}",
);
assert!(
!out.contains("ctx.readTreeAsValue(node, List::class.java)"),
"untagged deserializer must NOT use raw List::class.java for List<ContentPart>; got:\n{out}",
);
}
#[test]
fn emit_enum_unit_only_does_not_emit_json_deserialize() {
let en = make_enum(
"FinishReason",
None,
false,
None,
vec![make_variant("Stop", None, vec![]), make_variant("Length", None, vec![])],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
!out.contains("@JsonDeserialize") && !out.contains("Deserializer"),
"unit-only enum must not emit a deserializer; got:\n{out}",
);
assert!(
out.contains("enum class FinishReason"),
"must emit enum class; got:\n{out}"
);
}
#[test]
fn tagged_deserializer_strips_tag_field_from_payload() {
let en = make_enum(
"Message",
Some("role"),
false,
None,
vec![make_variant(
"System",
Some("system"),
vec![make_field("_0", TypeRef::Named("SystemMessage".to_string()))],
)],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("val tag = node.get(\"role\")?.asText()"),
"deserializer must extract tag into a local variable; got:\n{out}",
);
assert!(
out.contains(
"val payload = (node.deepCopy() as com.fasterxml.jackson.databind.node.ObjectNode).apply { remove(\"role\") }"
),
"deserializer must create tag-stripped payload via cast-safe deepCopy; got:\n{out}",
);
assert!(
out.contains("return when (tag)"),
"deserializer must dispatch on extracted tag variable; got:\n{out}",
);
assert!(
out.contains("readTreeAsValue<SystemMessage>(payload, SystemMessage::class.java)"),
"deserializer must pass tag-stripped payload to readTreeAsValue; got:\n{out}",
);
assert!(
!out.contains("readTreeAsValue<SystemMessage>(node, SystemMessage::class.java)"),
"deserializer must NOT pass un-stripped node to readTreeAsValue; got:\n{out}",
);
}
#[test]
fn sealed_class_variant_field_type_qualified_when_name_clashes_with_sibling_variant() {
let en = make_enum(
"ContentPart",
Some("type"),
false,
None,
vec![
make_variant("Text", Some("text"), vec![make_field("text", TypeRef::String)]),
make_variant(
"ImageUrl",
Some("image_url"),
vec![make_field("image_url", TypeRef::Named("ImageUrl".to_string()))],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "dev.sample_crate.samplellm.android", &[]);
assert!(
out.contains("val imageUrl: dev.sample_crate.samplellm.android.ImageUrl"),
"variant field type must be package-qualified when it clashes with a sibling variant name; got:\n{out}",
);
assert!(
out.contains("data class ImageUrl("),
"variant class declaration must still use simple name; got:\n{out}",
);
}
#[test]
fn sealed_class_variant_field_type_unqualified_when_no_clash() {
let en = make_enum(
"ContentPart",
Some("type"),
false,
None,
vec![make_variant(
"Document",
Some("document"),
vec![make_field("document", TypeRef::Named("DocumentContent".to_string()))],
)],
);
let mut out = String::new();
emit_enum(&en, &mut out, "dev.sample_crate.samplellm.android", &[]);
assert!(
out.contains("val document: DocumentContent"),
"non-clashing field type must remain unqualified; got:\n{out}",
);
assert!(
!out.contains("dev.sample_crate.samplellm.android.DocumentContent"),
"non-clashing field type must not be package-qualified; got:\n{out}",
);
}
#[test]
fn sealed_class_variant_data_classes_get_json_deserialize_reset_annotation() {
let en = make_enum(
"InputDocument",
Some("type"),
false,
Some("snake_case"),
vec![
make_variant("Url", Some("document_url"), vec![make_field("url", TypeRef::String)]),
make_variant(
"Base64",
Some("base64"),
vec![
make_field("data", TypeRef::String),
make_field("mediaType", TypeRef::String),
],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains(" @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None::class)\n @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.JsonSerializer.None::class)\n data class Url("),
"Url variant must have @JsonDeserialize(using=None) and @JsonSerialize(using=None) reset annotations; got:\n{out}",
);
assert!(
out.contains(" @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None::class)\n @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = com.fasterxml.jackson.databind.JsonSerializer.None::class)\n data class Base64("),
"Base64 variant must have @JsonDeserialize(using=None) and @JsonSerialize(using=None) reset annotations; got:\n{out}",
);
}
#[test]
fn untagged_sealed_class_vec_variant_serializer_uses_declared_type_serializer() {
let en = make_enum(
"UserContent",
None,
true,
None,
vec![
make_variant("Text", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Parts",
None,
vec![make_field(
"_0",
TypeRef::Vec(Box::new(TypeRef::Named("ContentPart".to_string()))),
)],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
!out.contains(" @com.fasterxml.jackson.databind.annotation.JsonDeserialize\n @com.fasterxml.jackson.databind.annotation.JsonSerialize\n data class Text("),
"Text newtype variant must NOT have reset annotations; got:\n{out}",
);
assert!(
out.contains("provider.findValueSerializer(ContentPart::class.java)"),
"Parts serializer must use provider.findValueSerializer(ContentPart::class.java); got:\n{out}",
);
assert!(
out.contains("is UserContent.Text -> mapper.writeValue(gen, value.value)"),
"Text serializer must use mapper.writeValue; got:\n{out}",
);
}
#[test]
fn untagged_serializer_tuple_variant_uses_payload_derived_field_name() {
let en = make_enum(
"EmbeddingInput",
None,
true,
None,
vec![
make_variant("Single", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Multiple",
None,
vec![make_field("_0", TypeRef::Vec(Box::new(TypeRef::String)))],
),
],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("-> mapper.writeValue(gen, value.value)"),
"untagged serializer must use payload-derived field name `value`; got:\n{out}",
);
assert!(
!out.contains("value.field0"),
"untagged serializer must NOT use hardcoded `field0`; got:\n{out}",
);
}
#[test]
fn tagged_serializer_named_field_variant_casts_to_concrete_type() {
let en = make_enum(
"InputDocument",
Some("type"),
false,
Some("snake_case"),
vec![make_variant(
"Url",
Some("document_url"),
vec![make_field("url", TypeRef::String)],
)],
);
let mut out = String::new();
emit_enum(&en, &mut out, "", &[]);
assert!(
out.contains("mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(value as InputDocument.Url) as com.fasterxml.jackson.databind.node.ObjectNode"),
"tagged serializer must cast value to concrete variant type; got:\n{out}",
);
assert!(
!out.contains("mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(value) as"),
"tagged serializer must NOT call valueToTree on un-cast parent-type value; got:\n{out}",
);
}
#[test]
fn file_level_suppress_includes_unused_parameter() {
let imports = std::collections::BTreeSet::new();
let body = "data class Foo(val x: Int)";
let result = crate::backends::kotlin::gen_bindings::shared::assemble_kt_file("com.example", &imports, body);
assert!(
result.contains("\"UnusedParameter\""),
"file-level @file:Suppress must include 'UnusedParameter' to suppress detekt for unused stub params; got:\n{result}",
);
assert!(
result.contains("@file:Suppress("),
"generated file must have @file:Suppress annotation; got:\n{result}",
);
}
#[test]
fn instance_method_params_camel_case_conversion() {
use heck::ToLowerCamelCase;
let param_names = vec!["max_size", "enabled", "chunk_config", "api_key"];
for name in ¶m_names {
let camel = name.to_lower_camel_case();
assert!(
!camel.contains("_"),
"camelCase param name must not contain underscores; '{}' -> '{}'",
name,
camel
);
assert!(
camel.chars().next().unwrap().is_lowercase(),
"camelCase param name must start with lowercase; '{}' -> '{}'",
name,
camel
);
}
}
#[test]
fn untagged_union_text_types_emits_text_accessor() {
let en = make_enum(
"AssistantContent",
None,
true,
None,
vec![
make_variant("Text", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Parts",
None,
vec![make_field("_0", TypeRef::Vec(Box::new(TypeRef::Json)))],
),
],
);
let mut out = String::new();
let text_types = vec!["AssistantContent".to_string()];
emit_enum(&en, &mut out, "", &text_types);
assert!(
out.contains(
"@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = AssistantContentDeserializer::class)"
),
"untagged sealed class must have @JsonDeserialize; got:\n{out}",
);
assert!(
out.contains(
"@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = AssistantContentSerializer::class)"
),
"untagged sealed class must have @JsonSerialize; got:\n{out}",
);
assert!(
out.contains("fun text(): String ="),
"untagged union in text_types must emit text() method; got:\n{out}",
);
assert!(
out.contains("is AssistantContent.Text -> this.value"),
"Text variant must return the string field directly via this.value; got:\n{out}",
);
assert!(
out.contains("is AssistantContent.Parts ->"),
"Parts variant must be handled in text() method; got:\n{out}",
);
assert!(
out.contains("typeNode?.asText() == \"text\""),
"text() must check type field equals 'text'; got:\n{out}",
);
assert!(
out.contains("sb.append(textNode.asText())"),
"text() must concatenate text field values; got:\n{out}",
);
}
#[test]
fn untagged_union_without_text_types_config_no_accessor() {
let en = make_enum(
"AssistantContent",
None,
true,
None,
vec![
make_variant("Text", None, vec![make_field("_0", TypeRef::String)]),
make_variant(
"Parts",
None,
vec![make_field("_0", TypeRef::Vec(Box::new(TypeRef::Json)))],
),
],
);
let mut out = String::new();
let text_types = vec![];
emit_enum(&en, &mut out, "", &text_types);
assert!(
!out.contains("fun text(): String"),
"untagged union without text_types config must not emit text() method; got:\n{out}",
);
assert!(
out.contains("sealed class AssistantContent"),
"sealed class must still be emitted; got:\n{out}",
);
assert!(
out.contains("data class Text(val value: String)"),
"Text variant must still be emitted; got:\n{out}",
);
}
#[test]
fn binding_excluded_field_is_omitted_from_dto() {
let mut hidden = make_field("tier_strategy", TypeRef::String);
hidden.binding_excluded = true;
hidden.binding_exclusion_reason = Some("exclude.fields".to_string());
let ty = crate::core::ir::TypeDef {
name: "ConversionOptions".to_string(),
rust_path: "crate::ConversionOptions".to_string(),
fields: vec![make_field("heading_style", TypeRef::String), hidden],
has_serde: true,
..Default::default()
};
let mut out = String::new();
let mut imports = std::collections::BTreeSet::new();
emit_type_with_imports(
&ty,
&mut out,
&mut imports,
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
None,
);
assert!(
out.contains("headingStyle"),
"non-excluded field must be present; got:\n{out}",
);
assert!(
!out.contains("tierStrategy") && !out.contains("tier_strategy"),
"binding_excluded field must be omitted entirely; got:\n{out}",
);
}
#[test]
fn default_constructible_excludes_types_with_a_bare_constructor_parameter() {
let counters = crate::core::ir::TypeDef {
name: "Counters".to_string(),
rust_path: "crate::Counters".to_string(),
fields: vec![make_field(
"count",
TypeRef::Primitive(crate::core::ir::PrimitiveType::I32),
)],
has_default: true,
..Default::default()
};
let constructible =
default_constructible_type_names(std::slice::from_ref(&counters), &std::collections::HashMap::new());
assert!(
!constructible.contains("Counters"),
"a type with a bare `val count: Int` parameter is not default-constructible; got {constructible:?}",
);
}
#[test]
fn default_constructible_keeps_types_whose_every_parameter_defaults() {
let mut maybe_name = make_field("name", TypeRef::String);
maybe_name.optional = true;
let settings = crate::core::ir::TypeDef {
name: "Settings".to_string(),
rust_path: "crate::Settings".to_string(),
fields: vec![make_field("tags", TypeRef::Vec(Box::new(TypeRef::String))), maybe_name],
has_default: true,
..Default::default()
};
let constructible =
default_constructible_type_names(std::slice::from_ref(&settings), &std::collections::HashMap::new());
assert!(
constructible.contains("Settings"),
"every parameter defaults, so the type is default-constructible; got {constructible:?}",
);
}
#[test]
fn default_constructible_propagates_removal_to_dependent_types() {
let inner = crate::core::ir::TypeDef {
name: "Inner".to_string(),
rust_path: "crate::Inner".to_string(),
fields: vec![make_field(
"count",
TypeRef::Primitive(crate::core::ir::PrimitiveType::I32),
)],
has_default: true,
..Default::default()
};
let mut nested = make_field("inner", TypeRef::Named("Inner".to_string()));
nested.typed_default = Some(crate::core::ir::DefaultValue::Empty);
let outer = crate::core::ir::TypeDef {
name: "Outer".to_string(),
rust_path: "crate::Outer".to_string(),
fields: vec![nested],
has_default: true,
..Default::default()
};
let constructible = default_constructible_type_names(&[inner, outer], &std::collections::HashMap::new());
assert!(
!constructible.contains("Inner"),
"Inner has a bare parameter; got {constructible:?}",
);
assert!(
!constructible.contains("Outer"),
"Outer defaulted to `Inner()`, so dropping Inner must drop Outer too; got {constructible:?}",
);
}
#[test]
fn kotlin_field_defaults_agree_with_the_swift_renderer() {
use crate::backends::swift::gen_bindings::dto::swift_typed_default_literal;
use crate::core::ir::{DefaultValue, PrimitiveType};
use std::collections::{HashMap, HashSet};
let fixture = [
(
"bool true",
TypeRef::Primitive(PrimitiveType::Bool),
DefaultValue::BoolLiteral(true),
),
(
"fractional f32",
TypeRef::Primitive(PrimitiveType::F32),
DefaultValue::FloatLiteral(0.7),
),
(
"whole-valued f64",
TypeRef::Primitive(PrimitiveType::F64),
DefaultValue::FloatLiteral(2.0),
),
(
"large integer",
TypeRef::Primitive(PrimitiveType::U64),
DefaultValue::IntLiteral(10_485_760),
),
(
"string",
TypeRef::String,
DefaultValue::StringLiteral("balanced".to_string()),
),
];
let enum_defaults = HashMap::new();
let constructible = HashSet::new();
for (label, ty, value) in fixture {
let swift = swift_typed_default_literal(&value).expect("the oracle renders every fixture value");
let kotlin = super::types::kotlin_field_default(&ty, false, Some(&value), &enum_defaults, &constructible);
let literal = kotlin
.strip_prefix(" = ")
.unwrap_or_else(|| panic!("kotlin dropped the default for `{label}`, rendering `{kotlin}`"));
assert_eq!(
literal.trim_end_matches(['f', 'L']),
swift,
"Kotlin and Swift disagree on the default for `{label}`: kotlin `{literal}`, swift `{swift}`"
);
}
}
#[test]
fn whole_valued_and_non_finite_float_defaults_stay_valid_kotlin() {
use crate::core::ir::{DefaultValue, PrimitiveType};
use std::collections::{HashMap, HashSet};
let enum_defaults = HashMap::new();
let constructible = HashSet::new();
assert_eq!(
super::types::kotlin_field_default(
&TypeRef::Primitive(PrimitiveType::F64),
false,
Some(&DefaultValue::FloatLiteral(1.0)),
&enum_defaults,
&constructible,
),
" = 1.0",
"a Double initializer needs a decimal point"
);
assert_eq!(
super::types::kotlin_field_default(
&TypeRef::Primitive(PrimitiveType::F32),
false,
Some(&DefaultValue::FloatLiteral(1.0)),
&enum_defaults,
&constructible,
),
" = 1.0f"
);
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let rendered = super::types::kotlin_field_default(
&TypeRef::Primitive(PrimitiveType::F64),
false,
Some(&DefaultValue::FloatLiteral(value)),
&enum_defaults,
&constructible,
);
assert_eq!(
rendered, "",
"a non-finite default has no Kotlin literal and must leave the parameter required"
);
}
}
#[test]
fn a_function_call_default_never_becomes_an_empty_kotlin_collection() {
use crate::core::ir::DefaultValue;
use std::collections::{HashMap, HashSet};
let enum_defaults = HashMap::new();
let constructible = HashSet::new();
for default in [
DefaultValue::FunctionCall("default_stop_words".to_string()),
DefaultValue::PublicFunctionCall("Settings::default_stop_words".to_string()),
] {
for ty in [
TypeRef::Vec(Box::new(TypeRef::String)),
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
] {
let rendered =
super::types::kotlin_field_default(&ty, false, Some(&default), &enum_defaults, &constructible);
assert_eq!(
rendered, "",
"a `{default:?}` on `{ty:?}` has no Kotlin literal and must leave the parameter required, \
not claim the empty collection"
);
}
}
}
#[test]
fn a_field_with_no_default_still_gets_the_empty_kotlin_collection() {
use std::collections::{HashMap, HashSet};
let enum_defaults = HashMap::new();
let constructible = HashSet::new();
assert_eq!(
super::types::kotlin_field_default(
&TypeRef::Vec(Box::new(TypeRef::String)),
false,
None,
&enum_defaults,
&constructible,
),
" = emptyList()"
);
assert_eq!(
super::types::kotlin_field_default(
&TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
None,
&enum_defaults,
&constructible,
),
" = emptyMap()"
);
}
#[test]
fn an_unresolved_default_never_becomes_a_kotlin_zero() {
use crate::core::ir::{DefaultValue, PrimitiveType};
use std::collections::{HashMap, HashSet};
let enum_defaults: HashMap<String, String> = [("Mode".to_string(), "Fast".to_string())].into();
let default_constructible: HashSet<String> = ["Settings".to_string()].into();
let unresolved = DefaultValue::Unresolved("Self::builder().build()".to_string());
let cases: Vec<(&str, TypeRef, bool)> = vec![
("bool", TypeRef::Primitive(PrimitiveType::Bool), false),
("i64", TypeRef::Primitive(PrimitiveType::I64), false),
("f64", TypeRef::Primitive(PrimitiveType::F64), false),
("string", TypeRef::String, false),
("vec", TypeRef::Vec(Box::new(TypeRef::String)), false),
(
"map",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
),
(
"optional scalar, not marked optional",
TypeRef::Optional(Box::new(TypeRef::String)),
false,
),
("optional field flag", TypeRef::Primitive(PrimitiveType::I64), true),
(
"named enum with a declared default",
TypeRef::Named("Mode".to_string()),
false,
),
(
"named default-constructible struct",
TypeRef::Named("Settings".to_string()),
false,
),
(
"named type with no default at all",
TypeRef::Named("Opaque".to_string()),
false,
),
];
for (label, ty, optional) in cases {
let rendered = super::types::kotlin_field_default(
&ty,
optional,
Some(&unresolved),
&enum_defaults,
&default_constructible,
);
assert_eq!(
rendered, "",
"an `Unresolved` default on `{label}` must leave the parameter required, not guess a zero: got `{rendered}`"
);
}
}
#[test]
fn empty_and_unresolved_render_differently_for_the_same_shapes() {
use crate::core::ir::{DefaultValue, PrimitiveType};
use std::collections::{HashMap, HashSet};
let enum_defaults = HashMap::new();
let constructible = HashSet::new();
let shapes = [
("bool", TypeRef::Primitive(PrimitiveType::Bool)),
("vec", TypeRef::Vec(Box::new(TypeRef::String))),
(
"map",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
),
("string", TypeRef::String),
];
for (label, ty) in shapes {
let empty =
super::types::kotlin_field_default(&ty, false, Some(&DefaultValue::Empty), &enum_defaults, &constructible);
let unresolved = super::types::kotlin_field_default(
&ty,
false,
Some(&DefaultValue::Unresolved("computed()".to_string())),
&enum_defaults,
&constructible,
);
assert_ne!(
empty, unresolved,
"`Empty` and `Unresolved` must render differently for `{label}`: `Empty` is a known zero, \
`Unresolved` is not — got `{empty}` for both"
);
assert!(
!empty.is_empty(),
"`Empty` on `{label}` is the type's own zero and must still render one: got `{empty}`"
);
assert_eq!(
unresolved, "",
"`Unresolved` on `{label}` must leave the parameter required: got `{unresolved}`"
);
}
}