use crate::core::ir::{
ApiSurface, DefaultValue, EnumDef, FieldDef, NewtypeContainer, NewtypeWrapper, NewtypeWrapperMetadata, TypeRef,
UnsupportedPublicItem,
};
use ahash::{AHashMap, AHashSet};
use super::SerdeDefaultsByType;
mod transparent_string;
use transparent_string::{extracted_named_paths, select_unambiguous_candidates, validate_transparent_string_methods};
pub(super) fn enum_default_variant_names(enums: &[EnumDef]) -> AHashMap<String, String> {
enums
.iter()
.filter(|enum_def| enum_def.has_default)
.filter_map(|enum_def| {
let variant = enum_def.variants.iter().find(|variant| {
variant.is_default && variant.fields.is_empty() && !variant.originally_had_data_fields
})?;
Some((enum_def.name.clone(), variant.name.clone()))
})
.collect()
}
pub(super) fn resolve_public_default_functions(surface: &mut ApiSurface) {
let public_methods: AHashMap<(String, String), String> = surface
.types
.iter()
.flat_map(|typ| {
typ.methods
.iter()
.filter(|method| method.is_static && method.params.is_empty() && !method.binding_excluded)
.map(|method| {
(
(typ.name.clone(), method.name.clone()),
format!("{}::{}", typ.rust_path.replace('-', "_"), method.name),
)
})
})
.collect();
for typ in &mut surface.types {
for field in &mut typ.fields {
let Some(DefaultValue::FunctionCall(path)) = &field.typed_default else {
continue;
};
let segments: Vec<_> = path.split("::").collect();
let [.., owner, method] = segments.as_slice() else {
continue;
};
if let Some(resolved_path) = public_methods.get(&(owner.to_string(), method.to_string())) {
field.typed_default = Some(DefaultValue::PublicFunctionCall(resolved_path.clone()));
}
}
}
}
pub(super) fn resolve_enum_field_defaults(surface: &mut ApiSurface) {
let enum_default_variants: Vec<(String, String, String)> = surface
.enums
.iter()
.filter(|enum_def| enum_def.has_default)
.filter_map(|enum_def| {
let variant = enum_def.variants.iter().find(|variant| {
variant.is_default && variant.fields.is_empty() && !variant.originally_had_data_fields
})?;
Some((
enum_def.name.clone(),
enum_def.rust_path.replace('-', "_"),
variant.name.clone(),
))
})
.collect();
if enum_default_variants.is_empty() {
return;
}
let non_enum_names: AHashSet<String> = surface
.types
.iter()
.map(|typ| typ.name.clone())
.chain(surface.errors.iter().map(|error| error.name.clone()))
.collect();
for typ in &mut surface.types {
for field in &mut typ.fields {
if field.optional {
continue;
}
let resolves_field_type_default = matches!(&field.typed_default, Some(DefaultValue::Empty))
|| (field.typed_default.is_none() && field.has_bare_serde_default());
if !resolves_field_type_default {
continue;
}
let TypeRef::Named(name) = &field.ty else {
continue;
};
let variant = if let Some(field_path) = field.type_rust_path.as_deref() {
enum_default_variants
.iter()
.find(|(_, enum_path, _)| enum_path == field_path)
.map(|(_, _, variant)| variant)
} else if non_enum_names.contains(name.as_str()) {
None
} else {
let mut matches = enum_default_variants
.iter()
.filter(|(enum_name, _, _)| enum_name == name)
.map(|(_, _, variant)| variant);
let first = matches.next();
if matches.next().is_none() { first } else { None }
};
if let Some(variant) = variant {
field.typed_default = Some(DefaultValue::EnumVariant(variant.clone()));
}
}
}
}
fn is_simple_type(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json
)
}
pub(super) fn resolve_newtypes(surface: &mut ApiSurface) {
let extracted_names = extracted_named_paths(surface);
let mut candidates: AHashMap<String, Vec<(String, TypeRef, ResolvedNewtype)>> = AHashMap::new();
let mut diagnostics = Vec::new();
for typ in &surface.types {
if typ.fields.len() != 1 || typ.fields[0].name != "_0" || !is_simple_type(&typ.fields[0].ty) {
continue;
}
let decoded = match typ.fields[0].newtype_wrapper.as_deref().map(NewtypeWrapper::decode) {
Some(Ok(wrapper)) => Some(wrapper),
Some(Err(reason)) => {
diagnostics.push(UnsupportedPublicItem {
item_kind: "struct".to_string(),
item_path: typ.rust_path.clone(),
reason,
suggested_fix: "regenerate the transparent newtype metadata from a valid alef annotation"
.to_string(),
});
continue;
}
None => None,
};
if let Some(wrapper) = &decoded
&& let Err(reason) = wrapper.validate_for_type(&typ.fields[0].ty, typ.fields[0].optional)
{
diagnostics.push(UnsupportedPublicItem {
item_kind: "struct".to_string(),
item_path: typ.rust_path.clone(),
reason,
suggested_fix: "make the conversion metadata path match the wrapper's resolved inner type".to_string(),
});
continue;
}
if typ.binding_exclusion_reason.as_deref() == Some("alef(transparent_string)") {
let Some(wrapper) = decoded.as_ref().filter(|wrapper| !wrapper.explicit_paths().is_empty()) else {
continue;
};
let method_validation = validate_transparent_string_methods(typ, wrapper.explicit_paths());
let matching_cfg_definition_is_valid = method_validation.is_err()
&& surface.types.iter().any(|candidate| {
candidate.rust_path == typ.rust_path
&& validate_transparent_string_methods(candidate, wrapper.explicit_paths()).is_ok()
});
if let Err(reason) = method_validation
&& !matching_cfg_definition_is_valid
{
diagnostics.push(UnsupportedPublicItem {
item_kind: "struct".to_string(),
item_path: typ.rust_path.clone(),
reason,
suggested_fix: "provide the exact public conversion methods named by the annotation".to_string(),
});
continue;
}
}
let wrapper = decoded.map_or_else(
|| ResolvedNewtype::legacy(typ.rust_path.replace('-', "_")),
|wrapper| ResolvedNewtype {
legacy: wrapper.tuple_path().map(str::to_string),
explicit: wrapper.explicit_paths_with_clone(typ.is_clone),
},
);
candidates.entry(typ.name.clone()).or_default().push((
typ.rust_path.clone(),
typ.fields[0].ty.clone(),
wrapper,
));
}
let (newtype_map, resolved_paths) = select_unambiguous_candidates(candidates, &extracted_names, &mut diagnostics);
surface.unsupported_public_items.extend(diagnostics);
if newtype_map.is_empty() {
return;
}
surface.types.retain(|typ| !resolved_paths.contains(&typ.rust_path));
for typ in &mut surface.types {
for field in &mut typ.fields {
let mut resolved = resolve_typeref(&newtype_map, &mut field.ty, true);
resolved.prepend_outer_optional(field.optional);
field.newtype_wrapper = resolved.encode();
}
for method in &mut typ.methods {
for param in &mut method.params {
let mut resolved = resolve_typeref(&newtype_map, &mut param.ty, false);
resolved.prepend_outer_optional(param.optional);
param.newtype_wrapper = resolved.encode();
}
method.return_newtype_wrapper = resolve_typeref(&newtype_map, &mut method.return_type, false).encode();
}
}
for func in &mut surface.functions {
for param in &mut func.params {
let mut resolved = resolve_typeref(&newtype_map, &mut param.ty, false);
resolved.prepend_outer_optional(param.optional);
param.newtype_wrapper = resolved.encode();
}
func.return_newtype_wrapper = resolve_typeref(&newtype_map, &mut func.return_type, false).encode();
}
for enum_def in &mut surface.enums {
for variant in &mut enum_def.variants {
for field in &mut variant.fields {
let mut resolved = resolve_typeref(&newtype_map, &mut field.ty, false);
resolved.legacy = None;
resolved.prepend_outer_optional(field.optional);
field.newtype_wrapper = resolved.encode();
}
}
}
}
fn resolve_typeref(
newtype_map: &AHashMap<String, (TypeRef, ResolvedNewtype)>,
ty: &mut TypeRef,
legacy_field_container: bool,
) -> ResolvedNewtype {
match ty {
TypeRef::Named(name) => {
if let Some((inner, wrapper)) = newtype_map.get(name.as_str()) {
*ty = inner.clone();
return wrapper.clone();
}
ResolvedNewtype::default()
}
TypeRef::Optional(inner) => resolve_typeref(newtype_map, inner, false)
.retain_legacy(legacy_field_container)
.prepend(NewtypeContainer::Optional),
TypeRef::Vec(inner) => resolve_typeref(newtype_map, inner, false)
.retain_legacy(legacy_field_container)
.prepend(NewtypeContainer::Vec),
TypeRef::Map(k, v) => {
let key_wrapper = resolve_typeref(newtype_map, k, false)
.retain_legacy(false)
.prepend(NewtypeContainer::MapKey);
let value_wrapper = resolve_typeref(newtype_map, v, false)
.retain_legacy(false)
.prepend(NewtypeContainer::MapValue);
key_wrapper.merge(value_wrapper)
}
_ => ResolvedNewtype::default(),
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub(super) struct ResolvedNewtype {
legacy: Option<String>,
explicit: Vec<NewtypeWrapperMetadata>,
}
impl ResolvedNewtype {
fn legacy(path: String) -> Self {
Self {
legacy: Some(path),
explicit: vec![],
}
}
fn retain_legacy(mut self, retain: bool) -> Self {
if !retain {
self.legacy = None;
}
self
}
fn prepend(mut self, container: NewtypeContainer) -> Self {
for metadata in &mut self.explicit {
metadata.containers.insert(0, container);
}
self
}
fn prepend_outer_optional(&mut self, optional: bool) {
if optional {
for metadata in &mut self.explicit {
metadata.containers.insert(0, NewtypeContainer::Optional);
}
}
}
fn merge(mut self, other: Self) -> Self {
self.explicit.extend(other.explicit);
self
}
fn encode(self) -> Option<String> {
if self.explicit.is_empty() {
self.legacy
} else {
Some(NewtypeWrapper::encode_explicit(&self.explicit))
}
}
}
pub(super) fn resolve_trait_sources(surface: &mut ApiSurface) {
let mut trait_method_map: AHashMap<String, Vec<(String, String)>> = AHashMap::new();
let mut trait_methods_set: AHashMap<String, Vec<String>> = AHashMap::new();
for typ in &surface.types {
if !typ.is_trait {
continue;
}
let method_names: Vec<String> = typ.methods.iter().map(|m| m.name.clone()).collect();
trait_methods_set.insert(typ.name.clone(), method_names.clone());
for method_name in &method_names {
trait_method_map
.entry(method_name.clone())
.or_default()
.push((typ.name.clone(), typ.rust_path.replace('-', "_")));
}
}
if trait_method_map.is_empty() {
return;
}
for typ in &mut surface.types {
if typ.is_trait {
continue;
}
let unresolved_names: Vec<String> = typ
.methods
.iter()
.filter(|m| m.trait_source.is_none())
.map(|m| m.name.clone())
.collect();
for method in &mut typ.methods {
if method.trait_source.is_some() {
continue;
}
let Some(candidates) = trait_method_map.get(&method.name) else {
continue;
};
if candidates.len() == 1 {
method.trait_source = Some(candidates[0].1.clone());
} else {
let best = candidates.iter().max_by_key(|(trait_name, _)| {
trait_methods_set
.get(trait_name)
.map(|trait_methods| {
trait_methods
.iter()
.filter(|method_name| unresolved_names.contains(method_name))
.count()
})
.unwrap_or(0)
});
if let Some((_, rust_path)) = best {
method.trait_source = Some(rust_path.clone());
}
}
}
}
}
fn is_fully_known(value: &DefaultValue) -> bool {
match value {
DefaultValue::Unresolved(_) | DefaultValue::FunctionCall(_) | DefaultValue::PublicFunctionCall(_) => false,
DefaultValue::BoolLiteral(_)
| DefaultValue::StringLiteral(_)
| DefaultValue::IntLiteral(_)
| DefaultValue::FloatLiteral(_)
| DefaultValue::EnumVariant(_)
| DefaultValue::Empty
| DefaultValue::None => true,
DefaultValue::TupleVariant(_, args) => args.iter().all(is_fully_known),
DefaultValue::StructVariant(_, fields) => fields.iter().all(|(_, value)| is_fully_known(value)),
DefaultValue::ListLiteral(items) => items.iter().all(is_fully_known),
}
}
fn denotes_type_zero(value: &DefaultValue) -> bool {
matches!(
value,
DefaultValue::Empty | DefaultValue::None | DefaultValue::BoolLiteral(false) | DefaultValue::IntLiteral(0)
) || matches!(value, DefaultValue::StringLiteral(s) if s.is_empty())
|| matches!(value, DefaultValue::FloatLiteral(f) if *f == 0.0)
}
fn agrees_via_enum_default(
serde_default: &DefaultValue,
actual_default: &DefaultValue,
field_type: &TypeRef,
enum_default_variants: &AHashMap<String, String>,
) -> bool {
let DefaultValue::EnumVariant(actual_variant) = actual_default else {
return false;
};
if !matches!(serde_default, DefaultValue::Empty) {
return false;
}
let TypeRef::Named(type_name) = field_type else {
return false;
};
enum_default_variants
.get(type_name)
.is_some_and(|default_variant| default_variant == actual_variant)
}
pub(crate) fn warn_on_default_disagreement(
rust_path: &str,
fields: &[FieldDef],
serde_defaults: &AHashMap<String, DefaultValue>,
enum_default_variants: &AHashMap<String, String>,
) {
for field in fields {
let Some(serde_default) = serde_defaults.get(&field.name) else {
continue;
};
let Some(actual_default) = &field.typed_default else {
continue;
};
if !is_fully_known(serde_default) || !is_fully_known(actual_default) {
tracing::debug!(
target: "alef::extract::defaults",
rust_type = rust_path,
field = %field.name,
"field default comparison skipped: serde default or resolved default is not fully known"
);
continue;
}
let agrees = serde_default == actual_default
|| (denotes_type_zero(serde_default) && denotes_type_zero(actual_default))
|| agrees_via_enum_default(serde_default, actual_default, &field.ty, enum_default_variants);
if !agrees {
tracing::warn!(
target: "alef::extract::defaults",
rust_type = rust_path,
field = %field.name,
serde_default = ?serde_default,
resolved_default = ?actual_default,
"field's `#[serde(default)]` value disagrees with its `#[derive(Default)]`/`impl Default` value"
);
}
}
}
pub(super) fn warn_on_default_disagreements(surface: &ApiSurface, pending_serde_defaults: &SerdeDefaultsByType) {
if pending_serde_defaults.is_empty() {
return;
}
let enum_default_variants = enum_default_variant_names(&surface.enums);
for typ in &surface.types {
let Some(serde_defaults) = pending_serde_defaults.get(&typ.rust_path) else {
continue;
};
warn_on_default_disagreement(&typ.rust_path, &typ.fields, serde_defaults, &enum_default_variants);
}
}
#[cfg(test)]
mod default_disagreement_tests {
use super::*;
use tracing_test::traced_test;
fn field(name: &str, typed_default: Option<DefaultValue>) -> FieldDef {
FieldDef {
name: name.to_string(),
typed_default,
..Default::default()
}
}
fn serde_defaults(entries: &[(&str, DefaultValue)]) -> AHashMap<String, DefaultValue> {
entries
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone()))
.collect()
}
#[test]
#[traced_test]
fn genuine_disagreement_between_two_known_values_is_reported() {
let fields = vec![field("retries", Some(DefaultValue::IntLiteral(5)))];
let defaults = serde_defaults(&[("retries", DefaultValue::IntLiteral(3))]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("my_crate::Config"));
assert!(logs_contain("retries"));
assert!(logs_contain("IntLiteral(3)"));
assert!(logs_contain("IntLiteral(5)"));
}
#[test]
#[traced_test]
fn derived_defaults_empty_disagreeing_with_a_known_serde_default_is_reported() {
let fields = vec![field("host", Some(DefaultValue::Empty))];
let defaults = serde_defaults(&[("host", DefaultValue::StringLiteral("localhost".to_string()))]);
warn_on_default_disagreement("my_crate::Client", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("my_crate::Client"));
assert!(logs_contain("host"));
assert!(logs_contain("StringLiteral(\"localhost\")"));
assert!(logs_contain("Empty"));
}
#[test]
#[traced_test]
fn agreeing_known_defaults_are_not_reported() {
let fields = vec![field("retries", Some(DefaultValue::IntLiteral(3)))];
let defaults = serde_defaults(&[("retries", DefaultValue::IntLiteral(3))]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn matching_empty_defaults_on_both_sides_are_not_reported() {
let fields = vec![field("count", Some(DefaultValue::Empty))];
let defaults = serde_defaults(&[("count", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
fn enum_typed_field(name: &str, typed_default: Option<DefaultValue>, enum_type_name: &str) -> FieldDef {
FieldDef {
name: name.to_string(),
typed_default,
ty: TypeRef::Named(enum_type_name.to_string()),
..Default::default()
}
}
fn enum_default_variants(entries: &[(&str, &str)]) -> AHashMap<String, String> {
entries
.iter()
.map(|(enum_name, variant_name)| ((*enum_name).to_string(), (*variant_name).to_string()))
.collect()
}
#[test]
#[traced_test]
fn empty_serde_default_agreeing_with_the_enum_default_variant_is_not_reported() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Auto".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
let enums = enum_default_variants(&[("TierStrategy", "Auto")]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &enums);
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn empty_serde_default_disagreeing_with_a_non_default_enum_variant_is_reported() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Tier2".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
let enums = enum_default_variants(&[("TierStrategy", "Auto")]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &enums);
assert!(logs_contain("disagrees"));
assert!(logs_contain("tier_strategy"));
}
#[test]
#[traced_test]
fn empty_serde_default_against_an_unknown_enum_is_reported_conservatively() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Auto".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn unresolved_resolved_default_is_not_reported() {
let fields = vec![field(
"level",
Some(DefaultValue::Unresolved("Self::builder().level(9).build()".to_string())),
)];
let defaults = serde_defaults(&[("level", DefaultValue::IntLiteral(9))]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn unresolved_serde_default_is_not_reported() {
let fields = vec![field("level", Some(DefaultValue::IntLiteral(9)))];
let defaults = serde_defaults(&[("level", DefaultValue::Unresolved("compute_default()".to_string()))]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn nested_unresolved_inside_a_tuple_variant_is_not_reported() {
let fields = vec![field(
"mode",
Some(DefaultValue::TupleVariant(
"Custom".to_string(),
vec![DefaultValue::Unresolved("compute()".to_string())],
)),
)];
let defaults = serde_defaults(&[(
"mode",
DefaultValue::TupleVariant("Custom".to_string(), vec![DefaultValue::IntLiteral(5)]),
)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn nested_unresolved_inside_a_struct_variant_is_not_reported() {
let fields = vec![field(
"kind",
Some(DefaultValue::StructVariant(
"Curated".to_string(),
vec![("label".to_string(), DefaultValue::Unresolved("compute()".to_string()))],
)),
)];
let defaults = serde_defaults(&[(
"kind",
DefaultValue::StructVariant(
"Curated".to_string(),
vec![("label".to_string(), DefaultValue::StringLiteral("balanced".to_string()))],
),
)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn function_call_default_is_not_compared_even_against_a_differing_literal() {
let fields = vec![field("token", Some(DefaultValue::StringLiteral("abc".to_string())))];
let defaults = serde_defaults(&[("token", DefaultValue::FunctionCall("generate_token".to_string()))]);
warn_on_default_disagreement("my_crate::Auth", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn field_absent_from_serde_defaults_is_skipped() {
let fields = vec![field("untouched", Some(DefaultValue::IntLiteral(1)))];
let defaults = serde_defaults(&[]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn a_zero_valued_serde_default_matching_the_rust_default_does_not_warn() {
let fields = vec![field("count", Some(DefaultValue::IntLiteral(0)))];
let defaults = serde_defaults(&[("count", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn zero_value_equivalence_across_default_value_spellings_is_evaluated_correctly() {
struct Case {
type_name: &'static str,
serde_default: DefaultValue,
actual_default: DefaultValue,
should_warn: bool,
}
let cases = [
Case {
type_name: "case::EmptySerdeZeroIntActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::IntLiteral(0),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeZeroFloatActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::FloatLiteral(0.0),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeFalseActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::BoolLiteral(false),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeEmptyStringActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::StringLiteral(String::new()),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeNoneActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::None,
should_warn: false,
},
Case {
type_name: "case::IntZeroSerdeEmptyActual",
serde_default: DefaultValue::IntLiteral(0),
actual_default: DefaultValue::Empty,
should_warn: false,
},
Case {
type_name: "case::EmptySerdeNonZeroIntActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::IntLiteral(5),
should_warn: true,
},
Case {
type_name: "case::NonZeroIntSerdeNonZeroIntActual",
serde_default: DefaultValue::IntLiteral(3),
actual_default: DefaultValue::IntLiteral(5),
should_warn: true,
},
Case {
type_name: "case::EmptySerdeNonEmptyStringActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::StringLiteral("localhost".to_string()),
should_warn: true,
},
];
for case in cases {
let fields = vec![field("value", Some(case.actual_default.clone()))];
let defaults = serde_defaults(&[("value", case.serde_default.clone())]);
warn_on_default_disagreement(case.type_name, &fields, &defaults, &AHashMap::new());
let warned = logs_contain(case.type_name) && logs_contain("disagrees");
assert_eq!(
warned, case.should_warn,
"case {} (serde={:?}, actual={:?}) expected should_warn={} but got {}",
case.type_name, case.serde_default, case.actual_default, case.should_warn, warned
);
}
}
}