use std::collections::{BTreeMap, BTreeSet, VecDeque};
use log::debug;
use strum::IntoEnumIterator;
use crate::build::{
ContainedChild, Enum, NewtypeConstraints, NewtypeStruct, StructProperty, StructPropertyState,
TupleStruct, Type, TypeAlias, VariantDetails, all_named_types,
};
use crate::error::{Error, OffenderReason, PathStep, Relation, RequirementOrigin, TraitConflict};
use crate::settings::{ContainerType, OptionalNullable, Settings, path_text};
use crate::{Obligation, TraitProvision, TypespaceTrait, TypespaceTraitSet};
enum Edge<'a, Id> {
Plain(Relation, Id),
Wrapped {
relation: Relation,
option_id: Id,
container: &'a ContainerType,
value_id: Id,
},
}
fn classify_edges<'a, Id: Clone + Ord + std::fmt::Debug + std::fmt::Display>(
ty: &Type<Id>,
types: &BTreeMap<Id, Type<Id>>,
settings: &'a Settings,
) -> Vec<Edge<'a, Id>> {
ty.contained_children_related()
.into_iter()
.map(|child| {
match (
&settings.optional_nullable,
child.optional,
types.get(&child.id).unwrap(),
) {
(OptionalNullable::CustomType(container), true, Type::Option(value_id)) => {
Edge::Wrapped {
relation: child.relation,
option_id: child.id,
container,
value_id: value_id.clone(),
}
}
_ => Edge::Plain(child.relation, child.id),
}
})
.collect()
}
pub(crate) fn resolve_traits<Id>(
types: &mut BTreeMap<Id, Type<Id>>,
settings: &Settings,
default_checks: &crate::DefaultChecks<Id>,
) -> Result<(), Error<Id>>
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
required_resolution(types, settings, default_checks)?;
desired_resolution(types, settings, default_checks);
Ok(())
}
pub(crate) fn resolve_from_string_irrefutable<Id>(types: &mut BTreeMap<Id, Type<Id>>)
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
let answers = types
.iter()
.filter(|(_, ty)| ty.is_named())
.map(|(type_id, _)| (type_id.clone(), walk_wrapper_chain(types, type_id)))
.collect::<Vec<_>>();
for (type_id, answer) in answers {
let common = types
.get_mut(&type_id)
.expect("the id came from this map")
.common_mut()
.expect("a named type has common metadata");
common
.built
.as_mut()
.expect("build_commons ran before this pass")
.from_string_irrefutable = answer;
}
}
fn walk_wrapper_chain<Id>(types: &BTreeMap<Id, Type<Id>>, id: &Id) -> bool
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
let mut seen = BTreeSet::new();
let mut here = id.clone();
loop {
if !seen.insert(here.clone()) {
return false;
}
match types.get(&here).expect("every id names a type") {
Type::String => return true,
Type::NewtypeStruct(NewtypeStruct {
inner,
constraints: NewtypeConstraints::None,
..
}) => here = inner.clone(),
Type::TypeAlias(TypeAlias { target, .. }) => here = target.clone(),
_ => return false,
}
}
}
pub(crate) fn from_string_irrefutable<Id>(types: &BTreeMap<Id, Type<Id>>, id: &Id) -> bool
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
match types.get(id).expect("every id names a type") {
Type::String => true,
ty => ty.common().is_some_and(|common| {
common
.built
.as_ref()
.expect("build_commons ran before trait resolution")
.from_string_irrefutable
}),
}
}
fn irrefutable_variant<Id>(types: &BTreeMap<Id, Type<Id>>, enum_info: &Enum<Id>) -> Option<String>
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
enum_info
.variants
.iter()
.find(|variant| match &variant.details {
VariantDetails::Item(id) => from_string_irrefutable(types, id),
VariantDetails::Unit | VariantDetails::Tuple(_) | VariantDetails::Struct(_) => false,
})
.map(|variant| variant.rust_name.clone())
}
fn expand_supertraits(mut traits: TypespaceTraitSet) -> TypespaceTraitSet {
if traits.contains(&TypespaceTrait::Ord) {
traits.add(TypespaceTrait::PartialOrd);
traits.add(TypespaceTrait::Eq);
traits.add(TypespaceTrait::PartialEq);
}
if traits.contains(&TypespaceTrait::Eq) {
traits.add(TypespaceTrait::PartialEq);
}
if traits.contains(&TypespaceTrait::PartialOrd) {
traits.add(TypespaceTrait::PartialEq);
}
if traits.contains(&TypespaceTrait::Copy) {
traits.add(TypespaceTrait::Clone);
}
traits
}
const CONTAINER_UNSUPPORTED: &[TypespaceTrait] =
&[TypespaceTrait::Display, TypespaceTrait::FromStr];
pub(crate) fn leaf_provides<Id>(
ty: &Type<Id>,
trait_name: TypespaceTrait,
settings: &Settings,
) -> Option<bool> {
match ty {
Type::Integer(_) | Type::Boolean => Some(true),
Type::String => Some(trait_name != TypespaceTrait::Copy),
Type::Unit => Some(!CONTAINER_UNSUPPORTED.contains(&trait_name)),
Type::Never => Some(
!CONTAINER_UNSUPPORTED.contains(&trait_name) && trait_name != TypespaceTrait::Default,
),
Type::Float(_) => Some(!matches!(
trait_name,
TypespaceTrait::Ord | TypespaceTrait::Eq | TypespaceTrait::Hash
)),
Type::JsonValue => Some(!matches!(
(trait_name, settings.typify_compat),
(
TypespaceTrait::Ord | TypespaceTrait::PartialOrd | TypespaceTrait::Copy,
_
) |
(TypespaceTrait::FromStr | TypespaceTrait::Display, true)
)),
_ => None,
}
}
enum Feasibility<Id> {
IfAllChildren,
IfSomeChildren(Vec<Obligation<Id>>),
Impossible(OffenderReason),
}
fn one_hop<Id: Clone>(type_id: &Id, relation: Relation) -> Vec<PathStep<Id>> {
vec![PathStep {
type_id: type_id.clone(),
relation,
}]
}
fn type_kind<Id>(ty: &Type<Id>) -> &'static str {
match ty {
Type::Struct(_) => "struct",
Type::Enum(_) => "enum",
Type::NewtypeStruct(_) => "newtype struct",
Type::UnitStruct(_) => "unit struct",
Type::TupleStruct(_) => "tuple struct",
_ => unreachable!("type_kind is only called for named, non-alias types"),
}
}
fn feasibility<Id>(
types: &BTreeMap<Id, Type<Id>>,
type_id: &Id,
ty: &Type<Id>,
trait_name: TypespaceTrait,
settings: &Settings,
default_checks: &crate::DefaultChecks<Id>,
) -> Feasibility<Id>
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
let cannot_implement = || {
Feasibility::Impossible(OffenderReason::TypeCannotImplement {
kind: type_kind(ty),
})
};
match ty {
Type::TypeAlias(_) => Feasibility::IfAllChildren,
Type::Struct(struct_info) => {
match trait_name {
TypespaceTrait::Display | TypespaceTrait::FromStr => cannot_implement(),
TypespaceTrait::Default => {
if struct_info.common.default().is_some() {
Feasibility::IfSomeChildren(
default_checks
.whole_type
.get(type_id)
.cloned()
.unwrap_or_default(),
)
} else if struct_info
.properties
.iter()
.any(|prop| matches!(&prop.state, StructPropertyState::Required))
{
cannot_implement()
} else {
let obligations = struct_info
.properties
.iter()
.filter(|prop| matches!(prop.state, StructPropertyState::Default))
.map(|prop| Obligation {
required: TypespaceTrait::Default,
path: one_hop(type_id, Relation::Field(prop.rust_name.clone())),
target: prop.type_id.clone(),
})
.collect();
Feasibility::IfSomeChildren(obligations)
}
}
_ => Feasibility::IfAllChildren,
}
}
Type::TupleStruct(TupleStruct { common, .. }) => {
match trait_name {
TypespaceTrait::Display | TypespaceTrait::FromStr => cannot_implement(),
TypespaceTrait::Default if settings.typify_compat => cannot_implement(),
TypespaceTrait::Default => {
if common.default.is_some() {
Feasibility::IfSomeChildren(Vec::new())
} else {
Feasibility::IfAllChildren
}
}
_ => Feasibility::IfAllChildren,
}
}
Type::UnitStruct(_) => match trait_name {
TypespaceTrait::Display | TypespaceTrait::FromStr => cannot_implement(),
TypespaceTrait::Default if settings.typify_compat => cannot_implement(),
_ => Feasibility::IfAllChildren,
},
Type::NewtypeStruct(NewtypeStruct { common, .. }) => match trait_name {
TypespaceTrait::Default if settings.typify_compat => cannot_implement(),
TypespaceTrait::Default if common.default.is_some() => Feasibility::IfSomeChildren(
default_checks
.whole_type
.get(type_id)
.cloned()
.unwrap_or_default(),
),
_ => Feasibility::IfAllChildren,
},
Type::Enum(e) => match trait_name {
TypespaceTrait::Display => {
if e.all_tagged_unit_variants() {
Feasibility::IfSomeChildren(Vec::new())
} else if e.all_untagged_item_variants() {
Feasibility::IfSomeChildren(
ty.contained_children_related()
.into_iter()
.map(|ContainedChild { relation, id, .. }| Obligation {
required: trait_name,
path: one_hop(type_id, relation),
target: id,
})
.collect(),
)
} else {
cannot_implement()
}
}
TypespaceTrait::FromStr => {
if e.all_tagged_unit_variants() {
Feasibility::IfSomeChildren(Vec::new())
} else if !e.all_untagged_item_variants() {
cannot_implement()
} else {
match irrefutable_variant(types, e) {
Some(variant) => {
Feasibility::Impossible(OffenderReason::IrrefutableVariantPayload {
variant,
})
}
None => Feasibility::IfSomeChildren(
ty.contained_children_related()
.into_iter()
.map(|ContainedChild { relation, id, .. }| Obligation {
required: trait_name,
path: one_hop(type_id, relation),
target: id,
})
.collect(),
),
}
}
}
TypespaceTrait::Default => {
if e.common.default().is_some() {
Feasibility::IfSomeChildren(Vec::new())
} else {
cannot_implement()
}
}
_ => Feasibility::IfAllChildren,
},
_ => unreachable!("feasibility is only called for named types"),
}
}
fn serde_default_properties<Id>(ty: &Type<Id>) -> Vec<&StructProperty<Id>> {
let is_default =
|prop: &&StructProperty<Id>| matches!(prop.state, StructPropertyState::Default);
match ty {
Type::Struct(struct_info) => struct_info.properties.iter().filter(is_default).collect(),
Type::Enum(enum_info) => enum_info
.variants
.iter()
.flat_map(|variant| match &variant.details {
VariantDetails::Struct(properties) => {
properties.iter().filter(is_default).collect()
}
VariantDetails::Unit | VariantDetails::Item(_) | VariantDetails::Tuple(_) => {
Vec::new()
}
})
.collect(),
_ => Vec::new(),
}
}
fn required_resolution<Id>(
types: &mut BTreeMap<Id, Type<Id>>,
settings: &Settings,
default_checks: &crate::DefaultChecks<Id>,
) -> Result<(), Error<Id>>
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
struct WorkItem<Id> {
target: Id,
traits: TypespaceTraitSet,
origin: RequirementOrigin<Id>,
path: Vec<PathStep<Id>>,
}
impl<Id: Clone> WorkItem<Id> {
fn init_container(
parent_id: &Id,
relation: Relation,
child_id: &Id,
traits: &TypespaceTraitSet,
) -> Self {
Self {
target: child_id.clone(),
traits: expand_supertraits(traits.clone()),
origin: RequirementOrigin::ContainerParameter {
container: parent_id.clone(),
relation,
},
path: Default::default(),
}
}
fn init_default(parent_id: &Id, relation: Relation, child_id: &Id) -> Self {
let default_required =
expand_supertraits([TypespaceTrait::Default].into_iter().collect());
Self {
target: child_id.clone(),
traits: default_required,
origin: RequirementOrigin::PropertyDefault(parent_id.clone()),
path: vec![PathStep {
type_id: parent_id.clone(),
relation,
}],
}
}
fn init_deserialize(parent_id: &Id, child_id: &Id) -> Self {
let deserialize_required =
expand_supertraits([TypespaceTrait::Deserialize].into_iter().collect());
Self {
target: child_id.clone(),
traits: deserialize_required,
origin: RequirementOrigin::DefaultValue(parent_id.clone()),
path: Default::default(),
}
}
fn init_global(type_id: &Id, traits: TypespaceTraitSet) -> Self {
Self {
target: type_id.clone(),
traits,
origin: RequirementOrigin::GlobalSettings,
path: Default::default(),
}
}
}
let mut work = VecDeque::new();
for (type_id, ty) in types.iter() {
match ty {
Type::Map(key_id, value_id) => {
work.push_back(WorkItem::init_container(
type_id,
Relation::Key,
key_id,
settings.map_type.obligation(0),
));
work.push_back(WorkItem::init_container(
type_id,
Relation::Value,
value_id,
settings.map_type.obligation(1),
));
}
Type::Set(element_id) => {
work.push_back(WorkItem::init_container(
type_id,
Relation::Element,
element_id,
settings.set_type.obligation(0),
));
}
Type::Vec(element_id) => {
work.push_back(WorkItem::init_container(
type_id,
Relation::Element,
element_id,
settings.vec_type.obligation(0),
));
}
Type::Struct(_) | Type::Enum(_) => {
for edge in classify_edges(ty, types, settings) {
if let Edge::Wrapped {
option_id,
container,
value_id,
..
} = edge
{
work.push_back(WorkItem::init_container(
&option_id,
Relation::Element,
&value_id,
container.obligation(0),
));
}
}
}
Type::Native(native) => {
for (index, (param_id, obligation)) in native
.parameters()
.iter()
.zip(native.obligations())
.enumerate()
{
work.push_back(WorkItem::init_container(
type_id,
Relation::Parameter(index),
param_id,
obligation,
));
}
}
_ => {}
}
}
for (type_id, ty) in types.iter() {
for prop in serde_default_properties(ty) {
work.push_back(WorkItem::init_default(
type_id,
Relation::Field(prop.rust_name.clone()),
&prop.type_id,
));
}
}
for (native_id, owner_id) in &default_checks.deserialized {
work.push_back(WorkItem::init_deserialize(owner_id, native_id));
}
if !settings.required_traits.is_empty() {
let required = expand_supertraits(settings.required_traits.clone());
for (type_id, ty) in types.iter() {
if ty.is_named() {
work.push_back(WorkItem::init_global(type_id, required.clone()));
}
}
}
let mut conflicts = Vec::<TraitConflict<Id>>::new();
while let Some(WorkItem {
target,
traits,
origin,
path,
}) = work.pop_front()
{
let ty = types.get(&target).unwrap();
let mut conflict = |bad: Vec<TypespaceTrait>, reason: OffenderReason| {
conflicts.extend(bad.into_iter().map(|required| TraitConflict {
required,
origin: origin.clone(),
path: path.clone(),
offender: target.clone(),
reason: reason.clone(),
}));
};
let hop = |relation: Relation| {
let mut next = path.clone();
next.push(PathStep {
type_id: target.clone(),
relation,
});
next
};
if ty.is_named() {
let mut all_children = Vec::new();
let mut some_children = Vec::new();
let mut built_traits = ty.common().unwrap().built.as_ref().unwrap().traits.clone();
for trait_name in traits {
if built_traits.contains(&trait_name) {
continue;
}
match feasibility(types, &target, ty, trait_name, settings, default_checks) {
Feasibility::IfAllChildren => {
built_traits.add(trait_name);
all_children.push(trait_name);
}
Feasibility::IfSomeChildren(obligations) => {
built_traits.add(trait_name);
if !obligations.is_empty() {
some_children.push((trait_name, obligations));
}
}
Feasibility::Impossible(reason) => {
conflict(vec![trait_name], reason);
}
}
}
let children = classify_edges(ty, types, settings);
types
.get_mut(&target)
.unwrap()
.common_mut()
.unwrap()
.built
.as_mut()
.unwrap()
.traits = built_traits;
if !all_children.is_empty() {
let traits_for_all_children =
all_children.into_iter().collect::<TypespaceTraitSet>();
for edge in children {
match edge {
Edge::Plain(relation, child_id) => work.push_back(WorkItem {
target: child_id,
traits: traits_for_all_children.clone(),
origin: origin.clone(),
path: hop(relation),
}),
Edge::Wrapped {
relation,
option_id,
container,
value_id,
} => {
let (bad, pass) = container_split(container, &traits_for_all_children);
for bad_trait in bad {
conflicts.push(TraitConflict {
required: bad_trait,
origin: origin.clone(),
path: hop(relation.clone()),
offender: option_id.clone(),
reason: OffenderReason::Primitive {
type_name: path_text(container.path()),
},
});
}
if !pass.is_empty() {
let mut path = hop(relation);
path.push(PathStep {
type_id: option_id,
relation: Relation::Element,
});
work.push_back(WorkItem {
target: value_id,
traits: pass,
origin: origin.clone(),
path,
});
}
}
}
}
}
for (trait_name, obligations) in some_children {
assert!(
&[
TypespaceTrait::Display,
TypespaceTrait::FromStr,
TypespaceTrait::Default
]
.contains(&trait_name)
);
for obligation in obligations {
let mut path = path.clone();
path.extend(obligation.path);
work.push_back(WorkItem {
target: obligation.target,
traits: [obligation.required]
.into_iter()
.collect::<TypespaceTraitSet>(),
origin: origin.clone(),
path,
});
}
}
} else {
assert!(!ty.is_named(), "the branch above handles every named type");
let (bad, pass) = unnamed_split(ty, &traits, settings);
conflict(bad, unnamed_offender(ty, settings));
if !pass.is_empty() {
for (relation, child_id) in unnamed_children(ty) {
work.push_back(WorkItem {
target: child_id,
traits: pass.clone(),
origin: origin.clone(),
path: hop(relation),
});
}
}
}
}
if conflicts.is_empty() {
Ok(())
} else {
Err(Error::TraitConflicts { conflicts })
}
}
fn strip_dependents(trait_name: TypespaceTrait) -> impl Iterator<Item = TypespaceTrait> {
let dependents: &'static [TypespaceTrait] = match trait_name {
TypespaceTrait::PartialEq => &[
TypespaceTrait::Eq,
TypespaceTrait::PartialOrd,
TypespaceTrait::Ord,
],
TypespaceTrait::Eq | TypespaceTrait::PartialOrd => &[TypespaceTrait::Ord],
TypespaceTrait::Clone => &[TypespaceTrait::Copy],
_ => &[],
};
std::iter::once(trait_name).chain(dependents.iter().copied())
}
fn provision_applies(provision: TraitProvision, parameters: impl FnOnce() -> bool) -> bool {
match provision {
TraitProvision::Never | TraitProvision::Unknown => false,
TraitProvision::Always => true,
TraitProvision::IfParameters => parameters(),
}
}
fn container_provides(
declaration: &ContainerType,
trait_name: TypespaceTrait,
parameters: impl FnOnce() -> bool,
) -> bool {
provision_applies(declaration.provision(trait_name), parameters)
}
fn container_split(
declaration: &ContainerType,
traits: &TypespaceTraitSet,
) -> (Vec<TypespaceTrait>, TypespaceTraitSet) {
let (bad, pass) = provision_split(traits, |tt| declaration.provision(tt));
(bad, expand_supertraits(pass))
}
fn provision_split(
traits: &TypespaceTraitSet,
provision: impl Fn(TypespaceTrait) -> TraitProvision,
) -> (Vec<TypespaceTrait>, TypespaceTraitSet) {
let bad = traits
.iter()
.filter(|tt| matches!(provision(**tt), TraitProvision::Never))
.copied()
.collect::<Vec<_>>();
let pass = traits
.iter()
.filter(|tt| matches!(provision(**tt), TraitProvision::IfParameters))
.copied()
.collect::<TypespaceTraitSet>();
(bad, pass)
}
fn unnamed_split<Id>(
ty: &Type<Id>,
traits: &TypespaceTraitSet,
settings: &Settings,
) -> (Vec<TypespaceTrait>, TypespaceTraitSet) {
match ty {
Type::Native(native) => container_split(&native.container, traits),
Type::Vec(_) => container_split(&settings.vec_type, traits),
Type::Set(_) => container_split(&settings.set_type, traits),
Type::Map(..) => container_split(&settings.map_type, traits),
_ => provision_split(traits, |trait_name| {
unnamed_provision(ty, trait_name, settings)
}),
}
}
fn unnamed_offender<Id>(ty: &Type<Id>, settings: &Settings) -> OffenderReason {
let container_path = |container: &ContainerType| OffenderReason::ContainerMissingImpl {
type_name: path_text(container.path()),
};
let type_name = match ty {
Type::Native(native) => {
return OffenderReason::NativeMissingImpl {
type_name: path_text(native.path()),
};
}
Type::Vec(_) => return container_path(&settings.vec_type),
Type::Set(_) => return container_path(&settings.set_type),
Type::Map(..) => return container_path(&settings.map_type),
Type::Option(_) => "Option",
Type::Box(_) => "Box",
Type::Array(..) => "array",
Type::Tuple(_) => "tuple",
Type::Unit => "()",
Type::String => "String",
Type::Boolean => "bool",
Type::Integer(name) | Type::Float(name) => name,
Type::JsonValue => "serde_json::Value",
Type::Never => "json_serde::Never",
all_named_types!(_) => unreachable!("caller passes only unnamed types"),
};
OffenderReason::Primitive {
type_name: type_name.to_string(),
}
}
fn unnamed_children<Id: Clone>(ty: &Type<Id>) -> Vec<(Relation, Id)> {
match ty {
Type::Native(native) => native
.parameters()
.iter()
.enumerate()
.map(|(index, param_id)| (Relation::Parameter(index), param_id.clone()))
.collect(),
Type::Option(id) | Type::Array(id, _) | Type::Vec(id) | Type::Set(id) => {
vec![(Relation::Element, id.clone())]
}
Type::Box(id) => vec![(Relation::Boxed, id.clone())],
Type::Tuple(ids) => ids
.iter()
.map(|id| (Relation::Element, id.clone()))
.collect(),
Type::Map(key_id, value_id) => vec![
(Relation::Key, key_id.clone()),
(Relation::Value, value_id.clone()),
],
_ => Vec::new(),
}
}
fn unnamed_provision<Id>(
ty: &Type<Id>,
trait_name: TypespaceTrait,
settings: &Settings,
) -> TraitProvision {
match ty {
Type::Option(_) if CONTAINER_UNSUPPORTED.contains(&trait_name) => TraitProvision::Never,
Type::Option(_) if trait_name == TypespaceTrait::Default => TraitProvision::Always,
Type::Option(_) => TraitProvision::IfParameters,
Type::Array(..) | Type::Tuple(_) if CONTAINER_UNSUPPORTED.contains(&trait_name) => {
TraitProvision::Never
}
Type::Array(..) | Type::Tuple(_) => TraitProvision::IfParameters,
Type::Box(_)
if trait_name == TypespaceTrait::Copy
|| CONTAINER_UNSUPPORTED.contains(&trait_name) =>
{
TraitProvision::Never
}
Type::Box(_) => TraitProvision::IfParameters,
Type::Integer(_)
| Type::Boolean
| Type::String
| Type::Unit
| Type::Never
| Type::Float(_)
| Type::JsonValue => match leaf_provides(ty, trait_name, settings) {
Some(true) => TraitProvision::Always,
Some(false) => TraitProvision::Never,
None => unreachable!("every arm above is a leaf the table answers"),
},
Type::Enum(_)
| Type::Struct(_)
| Type::UnitStruct(_)
| Type::TupleStruct(_)
| Type::NewtypeStruct(_)
| Type::TypeAlias(_)
| Type::Native(_)
| Type::Vec(_)
| Type::Set(_)
| Type::Map(..) => {
unreachable!(
"unnamed_provision is not called for named types, Native, or the configurable containers"
)
}
}
}
pub(crate) fn unnamed_provides<Id>(
ty: &Type<Id>,
trait_name: TypespaceTrait,
settings: &Settings,
child_has: &mut dyn FnMut(&Id) -> bool,
) -> bool {
match ty {
all_named_types!(_) => unreachable!("caller passes only unnamed types"),
Type::Native(native) => container_provides(&native.container, trait_name, || {
native.parameters.iter().all(child_has)
}),
Type::Vec(item_id) => {
container_provides(&settings.vec_type, trait_name, || child_has(item_id))
}
Type::Set(item_id) => {
container_provides(&settings.set_type, trait_name, || child_has(item_id))
}
Type::Map(key_ref, value_ref) => container_provides(&settings.map_type, trait_name, || {
child_has(key_ref) && child_has(value_ref)
}),
Type::Option(inner_id) => {
provision_applies(unnamed_provision(ty, trait_name, settings), || {
child_has(inner_id)
})
}
Type::Box(inner_id) => {
provision_applies(unnamed_provision(ty, trait_name, settings), || {
child_has(inner_id)
})
}
Type::Array(item_id, _) => {
provision_applies(unnamed_provision(ty, trait_name, settings), || {
child_has(item_id)
})
}
Type::Tuple(field_ids) => {
provision_applies(unnamed_provision(ty, trait_name, settings), || {
field_ids.iter().all(child_has)
})
}
Type::Integer(_)
| Type::Boolean
| Type::String
| Type::Unit
| Type::Never
| Type::Float(_)
| Type::JsonValue => leaf_provides(ty, trait_name, settings)
.expect("every arm above is a leaf the table answers"),
}
}
fn provides<Id>(
types: &BTreeMap<Id, Type<Id>>,
type_id: &Id,
ty: &Type<Id>,
trait_name: TypespaceTrait,
has: &BTreeMap<Id, TypespaceTraitSet>,
settings: &Settings,
default_checks: &crate::DefaultChecks<Id>,
) -> bool
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
let mut child_has = |child: &Id| {
has.get(child)
.is_some_and(|traits| traits.contains(&trait_name))
};
if ty.is_named() {
match feasibility(types, type_id, ty, trait_name, settings, default_checks) {
Feasibility::Impossible(_) => false,
Feasibility::IfAllChildren => {
classify_edges(ty, types, settings)
.into_iter()
.all(|edge| match edge {
Edge::Wrapped {
container,
value_id,
..
} => container_provides(container, trait_name, || child_has(&value_id)),
Edge::Plain(_, child_id) => child_has(&child_id),
})
}
Feasibility::IfSomeChildren(obligations) => obligations.iter().all(
|Obligation {
required, target, ..
}| {
has.get(target)
.is_some_and(|traits| traits.contains(required))
},
),
}
} else {
unnamed_provides(ty, trait_name, settings, &mut child_has)
}
}
fn granted_traits<'a, Id>(
types: &'a BTreeMap<Id, Type<Id>>,
type_id: &Id,
) -> Option<&'a TypespaceTraitSet>
where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
types
.get(type_id)
.and_then(|ty| ty.common())
.and_then(|common| common.built.as_ref())
.map(|built| &built.traits)
}
struct Loss<Id> {
loser: Id,
trait_name: TypespaceTrait,
}
struct Poison<Id> {
has: BTreeMap<Id, TypespaceTraitSet>,
queue: VecDeque<Loss<Id>>,
}
impl<Id> Poison<Id>
where
Id: Clone + Ord + std::fmt::Display,
{
fn lose(
&mut self,
loser: &Id,
trait_name: TypespaceTrait,
blocker: &Id,
granted: Option<&TypespaceTraitSet>,
) {
let traits = self.has.get_mut(loser).unwrap();
for lost in strip_dependents(trait_name) {
if granted.is_some_and(|granted| granted.contains(&lost)) {
continue;
}
if traits.remove(lost) {
debug!("desired trait {lost} dropped from {loser}, blocked by {blocker}");
self.queue.push_back(Loss {
loser: loser.clone(),
trait_name: lost,
});
}
}
}
}
fn desired_resolution<Id>(
types: &mut BTreeMap<Id, Type<Id>>,
settings: &Settings,
default_checks: &crate::DefaultChecks<Id>,
) where
Id: Clone + Ord + std::fmt::Debug + std::fmt::Display,
{
let desired = expand_supertraits(settings.desired_traits.clone());
if desired.is_empty() {
return;
}
let referrers = types.iter().fold(
BTreeMap::<_, Vec<_>>::new(),
|mut referrers, (type_id, ty)| {
for child in ty.children() {
referrers.entry(child).or_default().push(type_id.clone());
}
referrers
},
);
let has = types
.keys()
.map(|type_id| {
let mut all_traits = desired.clone();
for t in TypespaceTrait::iter() {
if !all_traits.contains(&t)
&& crate::has_trait(types, settings, type_id, t, &mut BTreeSet::new())
{
all_traits.add(t);
}
}
(type_id.clone(), all_traits)
})
.collect();
let mut state = Poison {
has,
queue: VecDeque::new(),
};
let seeds = types
.iter()
.flat_map(|(type_id, ty)| {
desired
.iter()
.filter(|trait_name| {
!provides(
types,
type_id,
ty,
**trait_name,
&state.has,
settings,
default_checks,
)
})
.map(|trait_name| (type_id.clone(), *trait_name))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
for (type_id, trait_name) in seeds {
let granted = granted_traits(types, &type_id);
state.lose(&type_id, trait_name, &type_id, granted);
}
while let Some(Loss { loser, trait_name }) = state.queue.pop_front() {
for referrer in referrers.get(&loser).into_iter().flatten() {
let ty = types.get(referrer).unwrap();
if state.has[referrer].contains(&trait_name)
&& !provides(
types,
referrer,
ty,
trait_name,
&state.has,
settings,
default_checks,
)
{
let granted = granted_traits(types, referrer);
state.lose(referrer, trait_name, &loser, granted);
}
}
}
let grant_groups = settings
.desired_traits
.iter()
.map(|trait_name| expand_supertraits([*trait_name].into_iter().collect()))
.collect::<Vec<_>>();
for (type_id, survivors) in state.has {
let ty = types.get_mut(&type_id).unwrap();
let Some(common) = ty.common_mut() else {
continue;
};
let built = common.built.as_mut().unwrap();
for group in &grant_groups {
if group
.iter()
.all(|trait_name| survivors.contains(trait_name))
{
for trait_name in group.iter() {
built.traits.add(*trait_name);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::from_string_irrefutable;
use crate::{
Typespace, TypespaceBuilder, TypespaceTrait, TypespaceTraitSet,
build::{JsonValue, Native, NewtypeConstraints, NewtypeStruct, TupleStruct, Type},
error::{Error, OffenderReason, Relation, RequirementOrigin},
no_cycles,
settings::{ContainerType, Settings},
};
use typespace_test_macro::typespace_builder;
fn built_traits(typespace: &Typespace<String>, id: &str) -> TypespaceTraitSet {
typespace
.types
.get(id)
.unwrap()
.common()
.unwrap()
.built
.as_ref()
.unwrap()
.traits
.clone()
}
#[test]
fn required_propagates_through_struct_graph() {
let builder = typespace_builder!(Settings::typical(), {
struct Inner {
count: u32,
}
struct Outer {
name: String,
inner: Inner,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
let expected = [
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Serialize,
TypespaceTrait::Deserialize,
]
.into_iter()
.collect::<TypespaceTraitSet>();
assert_eq!(built_traits(&typespace, "Outer"), expected);
assert_eq!(built_traits(&typespace, "Inner"), expected);
}
#[test]
fn map_key_conflict_reports_path() {
let builder = typespace_builder!(Settings::minimal(), {
struct KeyStruct {
weight: f64,
}
type KeyStructMap = Map<KeyStruct, String>;
});
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 2, "conflicts: {conflicts:#?}");
for required in [TypespaceTrait::Eq, TypespaceTrait::Ord] {
let conflict = conflicts
.iter()
.find(|conflict| conflict.required == required)
.unwrap_or_else(|| panic!("no conflict for {required}"));
assert!(matches!(
&conflict.origin,
RequirementOrigin::ContainerParameter { container, relation }
if container == "Map<KeyStruct, String>"
&& matches!(relation, Relation::Key)
));
assert_eq!(conflict.offender, "f64");
assert!(matches!(
&conflict.reason,
OffenderReason::Primitive { type_name } if type_name == "f64"
));
assert_eq!(conflict.path.len(), 1, "path: {:#?}", conflict.path);
assert_eq!(conflict.path[0].type_id, "KeyStruct");
assert!(matches!(
&conflict.path[0].relation,
Relation::Field(name) if name == "weight"
));
}
}
#[test]
fn serde_default_property_conflicts_when_type_cannot_default() {
let builder = typespace_builder!(Settings::minimal(), {
struct Inner {
count: u32,
}
struct Outer {
#[default]
inner: Inner,
}
});
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::Default);
assert!(matches!(
&conflict.origin,
RequirementOrigin::PropertyDefault(id) if id == "Outer"
));
assert_eq!(conflict.offender, "Inner");
assert!(matches!(
&conflict.reason,
OffenderReason::TypeCannotImplement { kind } if *kind == "struct"
));
assert_eq!(conflict.path.len(), 1, "path: {:#?}", conflict.path);
assert_eq!(conflict.path[0].type_id, "Outer");
assert!(matches!(
&conflict.path[0].relation,
Relation::Field(name) if name == "inner"
));
}
#[test]
fn serde_default_property_in_variant_conflicts() {
let builder = typespace_builder!(Settings::minimal(), {
struct Inner {
count: u32,
}
enum Shape {
Rect {
#[default]
inner: Inner,
},
}
});
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::Default);
assert!(matches!(
&conflict.origin,
RequirementOrigin::PropertyDefault(id) if id == "Shape"
));
assert_eq!(conflict.offender, "Inner");
assert_eq!(conflict.path.len(), 1, "path: {:#?}", conflict.path);
assert_eq!(conflict.path[0].type_id, "Shape");
assert!(matches!(
&conflict.path[0].relation,
Relation::Field(name) if name == "inner"
));
}
#[test]
fn serde_default_property_grants_default_to_its_type() {
let builder = typespace_builder!(Settings::minimal(), {
struct Inner {
#[default]
count: u32,
}
struct Outer {
#[default]
inner: Inner,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Inner"),
trait_set([TypespaceTrait::Default])
);
assert_eq!(
built_traits(&typespace, "Outer"),
TypespaceTraitSet::empty()
);
}
#[test]
fn serde_default_property_requirement_stops_at_vec() {
let builder = typespace_builder!(Settings::minimal(), {
struct Tag {
id: u32,
}
struct S {
#[default]
tags: Vec<Tag>,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(built_traits(&typespace, "Tag"), TypespaceTraitSet::empty());
assert_eq!(built_traits(&typespace, "S"), TypespaceTraitSet::empty());
}
#[test]
fn required_display_on_struct_conflicts() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Display),
{
struct S {
s: String,
}
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::Display);
assert!(matches!(conflict.origin, RequirementOrigin::GlobalSettings));
assert_eq!(conflict.offender, "S");
assert!(conflict.path.is_empty(), "path: {:#?}", conflict.path);
assert!(matches!(
conflict.reason,
OffenderReason::TypeCannotImplement { kind: "struct" }
));
}
#[test]
fn required_display_on_option_conflicts() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Display),
{
struct Wrapper(Nullable<String>);
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::Display);
assert_eq!(conflict.offender, "Nullable<String>");
assert!(matches!(
&conflict.reason,
OffenderReason::Primitive { type_name } if type_name == "Option"
));
}
#[test]
fn required_fromstr_on_option_conflicts() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::FromStr),
{
struct Wrapper(Nullable<String>);
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::FromStr);
assert_eq!(conflict.offender, "Nullable<String>");
assert!(matches!(
&conflict.reason,
OffenderReason::Primitive { type_name } if type_name == "Option"
));
}
#[test]
fn required_display_on_box_conflicts() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Display),
{
struct Wrapper(Box<String>);
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let conflict = &conflicts[0];
assert_eq!(conflict.required, TypespaceTrait::Display);
assert!(matches!(
&conflict.reason,
OffenderReason::Primitive { type_name } if type_name == "Box"
));
}
#[test]
fn option_forwards_supported_trait_to_element() {
let builder = typespace_builder!(Settings::minimal(), {
enum Color {
Red,
Green,
}
type ColorSet = Set<Nullable<Color>>;
});
let typespace = builder.finalize(no_cycles).unwrap();
let expected = [
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
]
.into_iter()
.collect::<TypespaceTraitSet>();
assert_eq!(built_traits(&typespace, "Color"), expected);
}
#[test]
fn option_default_satisfied_without_reaching_element() {
let settings = Settings::minimal().with_set_type(
ContainerType::hash_set()
.with_obligations([[TypespaceTrait::Default].into_iter().collect()]),
);
let builder = typespace_builder!(settings, {
enum Color {
Red,
Green,
}
type ColorSet = Set<Nullable<Color>>;
});
let typespace = builder.finalize(no_cycles).unwrap();
assert!(built_traits(&typespace, "Color").is_empty());
}
#[test]
fn required_display_on_simple_enum_accepted() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::Display)
.with_required_trait(TypespaceTrait::FromStr),
{
enum Color {
Red,
Green,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let expected = [TypespaceTrait::Display, TypespaceTrait::FromStr]
.into_iter()
.collect::<TypespaceTraitSet>();
assert_eq!(built_traits(&typespace, "Color"), expected);
}
#[test]
fn never_field_satisfies_typical_settings() {
let builder = typespace_builder!(Settings::typical(), {
struct S {
gone: Optional<!>,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
let expected = [
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Serialize,
TypespaceTrait::Deserialize,
]
.into_iter()
.collect::<TypespaceTraitSet>();
assert_eq!(built_traits(&typespace, "S"), expected);
}
#[test]
fn required_display_conflicts_on_never_field() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Display),
{
struct Wrapper(!);
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::NeverInTransparentWrapper { wrapper, type_id } = err else {
panic!("expected NeverInTransparentWrapper, got: {err}");
};
assert_eq!(wrapper, "newtype struct");
assert_eq!(type_id, "Wrapper");
}
#[test]
fn required_ord_expands_to_supertraits() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Ord),
{
struct S {
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let expected = [
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
]
.into_iter()
.collect::<TypespaceTraitSet>();
assert_eq!(built_traits(&typespace, "S"), expected);
}
#[test]
fn native_traits_add_no_supertraits() {
let builder = typespace_builder!(Settings::minimal(), {
native ::chrono::NaiveDate: Eq + PartialEq + Ord + PartialOrd;
type DateMap = Map<::chrono::NaiveDate, String>;
});
builder.finalize(no_cycles).expect("finalization succeeds");
let builder = typespace_builder!(Settings::minimal(), {
native ::chrono::NaiveDate: Ord;
type DateMap = Map<::chrono::NaiveDate, String>;
});
let Err(Error::TraitConflicts { conflicts }) = builder.finalize(no_cycles) else {
panic!("expected finalization to report trait conflicts");
};
assert_eq!(conflicts.len(), 3, "conflicts: {conflicts:#?}");
for conflict in &conflicts {
assert_eq!(conflict.offender, "::chrono::NaiveDate");
assert!(matches!(
&conflict.reason,
OffenderReason::NativeMissingImpl { type_name }
if type_name == "::chrono::NaiveDate"
));
}
}
#[test]
#[ignore]
fn required_display_panics_at_render_not_finalize() {
let settings = Settings::minimal().with_required_trait(TypespaceTrait::Display);
let builder = typespace_builder!(settings, {
enum Color {
Red,
Blue,
}
});
let ts = builder
.finalize(no_cycles)
.expect("finalize should succeed per with_required_trait docs");
let _ = ts.to_codespace();
}
fn trait_set(traits: impl IntoIterator<Item = TypespaceTrait>) -> TypespaceTraitSet {
traits.into_iter().collect()
}
fn minimal_with_desired(traits: impl IntoIterator<Item = TypespaceTrait>) -> Settings {
traits
.into_iter()
.fold(Settings::minimal(), Settings::with_desired_trait)
}
#[test]
fn desired_granted_on_capable_struct() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Eq, TypespaceTrait::Hash]),
{
struct S {
count: u32,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Hash,
])
);
}
#[test]
fn desired_ord_expands_to_supertraits() {
let builder = typespace_builder!(minimal_with_desired([TypespaceTrait::Ord]), {
struct S {
name: String,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
}
#[test]
fn desired_trait_already_required_survives() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::Eq)
.with_desired_trait(TypespaceTrait::Eq),
{
struct Inner {
count: u32,
}
struct Outer {
inner: Inner,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let expected = trait_set([TypespaceTrait::Eq, TypespaceTrait::PartialEq]);
assert_eq!(built_traits(&typespace, "Outer"), expected);
assert_eq!(built_traits(&typespace, "Inner"), expected);
}
#[test]
fn an_obligation_sees_a_trait_that_is_required_rather_than_desired() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::Deserialize)
.with_desired_trait(TypespaceTrait::Default),
{
native ::ext::Readable: Clone + Deserialize;
#[default = { "x": "whatever" }]
struct Holder {
x: ::ext::Readable,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert!(
built_traits(&typespace, "Holder").contains(&TypespaceTrait::Default),
"the value's Deserialize obligation went unseen",
);
}
#[test]
fn desired_forwards_through_alias() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Capable {
count: u32,
}
struct Blocked {
weight: f64,
}
type CapableAlias = Capable;
type BlockedAlias = Blocked;
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let with_eq = trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
]);
assert_eq!(built_traits(&typespace, "Capable"), with_eq);
assert_eq!(built_traits(&typespace, "CapableAlias"), with_eq);
let without_eq = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "Blocked"), without_eq);
assert_eq!(built_traits(&typespace, "BlockedAlias"), without_eq);
}
#[test]
fn desired_evaluates_container_structure() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct S {
list: Vec<u32>,
lookup: Map<String, u32>,
unique: Set<u32>,
maybe: Nullable<u32>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
}
#[test]
fn desired_blocked_by_container_parameter() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct InVec {
list: Vec<f64>,
}
struct InMapValue {
lookup: Map<String, f64>,
}
struct InOption {
maybe: Nullable<f64>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "InVec"), only_clone);
assert_eq!(built_traits(&typespace, "InMapValue"), only_clone);
assert_eq!(built_traits(&typespace, "InOption"), only_clone);
}
#[test]
fn float_field_strips_ordering_traits_keeps_rest() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::Ord,
TypespaceTrait::Hash,
]),
{
struct S {
weight: f64,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
])
);
}
#[test]
fn json_value_field_strips_ordering_traits() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::Ord,
TypespaceTrait::Hash,
]),
{
struct S {
blob: JsonValue,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::PartialEq,
TypespaceTrait::Eq,
TypespaceTrait::Hash,
])
);
}
#[test]
fn stripping_removes_supertrait_dependents() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::Ord,
TypespaceTrait::Hash,
]),
{
native weird::Weird: Clone + Debug + Eq + Ord + Hash;
struct S {
odd: weird::Weird,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Hash,
])
);
}
#[test]
fn blocked_leaf_strips_through_two_hops() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Bottom {
weight: f64,
}
struct Middle {
bottom: Bottom,
}
struct Top {
middle: Middle,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "Bottom"), only_clone);
assert_eq!(built_traits(&typespace, "Middle"), only_clone);
assert_eq!(built_traits(&typespace, "Top"), only_clone);
}
#[test]
fn desired_survives_cycle_through_box() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct A {
b: Box<B>,
count: u32,
}
struct B {
a: Box<A>,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let with_eq = trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
]);
assert_eq!(built_traits(&typespace, "A"), with_eq);
assert_eq!(built_traits(&typespace, "B"), with_eq);
}
#[test]
fn cycle_with_blocker_strips_every_scc_member() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct A {
b: Box<B>,
}
struct B {
a: Box<A>,
weight: f64,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "A"), only_clone);
assert_eq!(built_traits(&typespace, "B"), only_clone);
}
#[test]
fn infeasible_desired_is_silent_where_required_conflicts() {
let required = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Eq),
{
struct S {
weight: f64,
}
}
);
let Err(err) = required.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
let desired = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct S {
weight: f64,
}
}
);
let typespace = desired
.finalize(no_cycles)
.expect("a desired trait that cannot be realized is dropped, not reported");
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_on_struct_dropped_silently() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Display,
TypespaceTrait::FromStr,
]),
{
struct S {
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_default_on_enum_without_value_dropped() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Plain {
Red,
Green,
}
#[default = "Red"]
enum WithValue {
Red,
Green,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Plain"),
trait_set([TypespaceTrait::Clone])
);
assert_eq!(
built_traits(&typespace, "WithValue"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
}
#[test]
fn desired_default_from_attached_value_needs_nothing_of_fields() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
#[default = { count: 0, color: "Red" }]
struct S {
count: u32,
color: Color,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
assert_eq!(
built_traits(&typespace, "Color"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_default_from_attached_value_exempts_omitted_optional() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
#[default = { count: 0 }]
struct S {
count: u32,
color: Optional<Color>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
assert_eq!(
built_traits(&typespace, "Color"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_imposes_no_requirement_on_native() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Ord,
]),
{
native plain::Plain: Clone + Debug;
struct S {
plain: plain::Plain,
}
}
);
let typespace = builder
.finalize(no_cycles)
.expect("a desired trait never becomes a requirement on a native");
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Debug])
);
}
#[test]
fn required_trait_passes_native_unknown() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Ord),
{
native ::opaque::Opaque: Clone + Debug + ?Ord + ?PartialOrd + ?Eq + ?PartialEq;
struct S {
key: ::opaque::Opaque,
}
}
);
let typespace = builder
.finalize(no_cycles)
.expect("a native's unknown trait satisfies a requirement for it");
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
}
#[test]
fn required_map_key_passes_native_rest_unknown() {
let builder = typespace_builder!(Settings::typical(), {
native ::chrono::naive::NaiveDate:
Clone + Debug + Serialize + Deserialize + ..;
type DateMap = Map<::chrono::naive::NaiveDate, String>;
});
builder
.finalize(no_cycles)
.expect("an unknown map key trait is not a conflict");
}
#[test]
fn desired_trait_not_granted_by_native_unknown() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Ord,
]),
{
native ::opaque::Opaque: Clone + Debug + ..;
struct S {
odd: ::opaque::Opaque,
}
}
);
let typespace = builder
.finalize(no_cycles)
.expect("a desired trait never becomes a requirement on a native");
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Debug])
);
}
#[test]
fn required_trait_conflicts_on_native_known_missing() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Hash),
{
native ::opaque::Opaque: Clone + Debug;
struct S {
key: ::opaque::Opaque,
}
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Hash);
assert_eq!(conflicts[0].offender, "::opaque::Opaque");
assert!(matches!(
&conflicts[0].reason,
OffenderReason::NativeMissingImpl { type_name }
if type_name == "::opaque::Opaque"
));
}
#[test]
fn required_trait_conflicts_on_native_excepted_from_rest_unknown() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Ord),
{
native ::opaque::Opaque: Clone + Debug + .. + !Ord;
struct S {
key: ::opaque::Opaque,
}
}
);
let Err(err) = builder.finalize(no_cycles) else {
panic!("finalization unexpectedly succeeded");
};
let Error::TraitConflicts { conflicts } = err else {
panic!("expected TraitConflicts, got: {err}");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Ord);
assert_eq!(conflicts[0].offender, "::opaque::Opaque");
assert!(matches!(
&conflicts[0].reason,
OffenderReason::NativeMissingImpl { type_name }
if type_name == "::opaque::Opaque"
));
}
#[test]
fn required_trait_forwards_through_native_if_parameters() {
let settings = || Settings::minimal().with_required_trait(TypespaceTrait::Clone);
let builder = typespace_builder!(settings(), {
struct Inner {
count: u32,
}
native ::foo::Wrapper<Inner>: Debug + *Clone;
struct Holder {
wrapped: ::foo::Wrapper<Inner>,
}
});
let typespace = builder
.finalize(no_cycles)
.expect("Wrapper<Inner> is Clone because Inner is");
assert!(
built_traits(&typespace, "Holder").contains(&TypespaceTrait::Clone),
"Holder should derive Clone through Wrapper<Inner>"
);
let builder = typespace_builder!(settings(), {
native ::bar::NoClone: Debug;
native ::foo::Wrapper<::bar::NoClone>: Debug + *Clone;
struct Holder {
wrapped: ::foo::Wrapper<::bar::NoClone>,
}
});
let Err(Error::TraitConflicts { conflicts }) = builder.finalize(no_cycles) else {
panic!("expected finalization to report a trait conflict at NoClone");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Clone);
assert_eq!(conflicts[0].offender, "::bar::NoClone");
assert!(matches!(
&conflicts[0].reason,
OffenderReason::NativeMissingImpl { type_name }
if type_name == "::bar::NoClone"
));
assert!(
conflicts[0]
.path
.iter()
.any(|step| matches!(step.relation, Relation::Parameter(0))),
"expected a hop through the native's parameter 0: {:#?}",
conflicts[0].path
);
}
#[test]
fn desired_trait_forwards_through_native_if_parameters() {
let settings = || minimal_with_desired([TypespaceTrait::Debug, TypespaceTrait::Clone]);
let builder = typespace_builder!(settings(), {
struct Inner {
count: u32,
}
native ::foo::Wrapper<Inner>: Debug + *Clone;
struct Holder {
wrapped: ::foo::Wrapper<Inner>,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Holder"),
trait_set([TypespaceTrait::Debug, TypespaceTrait::Clone])
);
let builder = typespace_builder!(settings(), {
native ::bar::NoClone: Debug;
native ::foo::Wrapper<::bar::NoClone>: Debug + *Clone;
struct Holder {
wrapped: ::foo::Wrapper<::bar::NoClone>,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Holder"),
trait_set([TypespaceTrait::Debug]),
"Clone should be dropped: NoClone never declares it"
);
}
#[test]
fn native_obligation_is_seeded_unconditionally() {
let mut builder = TypespaceBuilder::<String>::new(Settings::minimal());
builder
.insert(
"Plain".to_string(),
Type::Native(Native::new(
"plain::Plain",
TypespaceTraitSet::empty(),
Vec::new(),
)),
)
.unwrap();
builder
.insert(
"Set".to_string(),
Type::Native(
Native::new(
"set::Set",
trait_set([TypespaceTrait::Debug]),
vec!["Plain".to_string()],
)
.with_obligations([trait_set([TypespaceTrait::Hash])]),
),
)
.unwrap();
let Err(Error::TraitConflicts { conflicts }) = builder.finalize(no_cycles) else {
panic!("expected the unmet obligation to conflict");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Hash);
assert_eq!(conflicts[0].offender, "Plain");
assert!(matches!(
&conflicts[0].origin,
RequirementOrigin::ContainerParameter { container, relation: Relation::Parameter(0) }
if container == "Set"
));
}
#[test]
fn native_obligation_count_mismatch_is_rejected_at_insertion() {
let mut builder = TypespaceBuilder::<String>::new(Settings::minimal());
let err = builder
.insert(
"Bad".to_string(),
Type::Native(
Native::<String>::new("bad::Bad", TypespaceTraitSet::empty(), Vec::new())
.with_obligations([trait_set([TypespaceTrait::Hash])]),
),
)
.unwrap_err();
assert!(matches!(
err,
Error::NativeParameterCount {
declared: 1,
parameters: 0,
..
}
));
}
#[test]
fn native_parameter_is_a_child_reference() {
let mut builder = TypespaceBuilder::<String>::new(Settings::minimal());
builder
.insert(
"Wrapper".to_string(),
Type::Native(Native::new(
"foo::Wrapper",
TypespaceTraitSet::empty(),
vec!["Missing".to_string()],
)),
)
.unwrap();
let Err(err) = builder.finalize(no_cycles) else {
panic!("expected finalization to reject the dangling parameter id");
};
assert!(matches!(
err,
Error::UnknownTypeId { type_id, child_id }
if type_id == "Wrapper" && child_id == "Missing"
));
}
#[test]
fn desired_blocked_by_set_element() {
let mut builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Hash]),
{
struct S {
unique: Set<Ordered>,
}
}
);
builder
.insert(
"Ordered".to_string(),
Type::Native(Native::new(
"ordered::Ordered",
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
]),
Vec::new(),
)),
)
.unwrap();
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_does_not_obligate_newtype_inner() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
struct Inner {
count: u32,
}
struct Wrapper(Inner);
}
);
let typespace = builder
.finalize(no_cycles)
.expect("a desired trait never becomes a requirement on a field");
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "Wrapper"), only_clone);
assert_eq!(built_traits(&typespace, "Inner"), only_clone);
}
#[test]
fn all_traits_preset_over_float_struct() {
let builder = typespace_builder!(Settings::maximal(), {
struct S {
weight: f64,
name: String,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Debug,
TypespaceTrait::Serialize,
TypespaceTrait::Deserialize,
TypespaceTrait::JsonSchema,
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
])
);
}
#[test]
fn desired_supertrait_survives_only_when_desired_directly() {
let supertrait_only = typespace_builder!(minimal_with_desired([TypespaceTrait::Eq]), {
struct S {
weight: f64,
}
});
let typespace = supertrait_only.finalize(no_cycles).unwrap();
assert_eq!(built_traits(&typespace, "S"), TypespaceTraitSet::empty());
let desired_directly = typespace_builder!(
minimal_with_desired([TypespaceTrait::Eq, TypespaceTrait::PartialEq]),
{
struct S {
weight: f64,
}
}
);
let typespace = desired_directly.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::PartialEq])
);
}
#[test]
fn desired_partial_eq_survives_via_other_desired_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Eq, TypespaceTrait::PartialOrd]),
{
struct S {
weight: f64,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::PartialOrd, TypespaceTrait::PartialEq])
);
}
#[test]
fn request_survives_the_death_of_a_dependent_request() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::PartialEq, TypespaceTrait::Ord]),
{
struct S {
weight: f64,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::PartialEq])
);
}
#[test]
fn supertrait_borrowed_by_one_request_leaves_with_it() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::PartialEq,
TypespaceTrait::Ord,
]),
{
native ::weird::Weird: Clone + PartialEq + Eq + PartialOrd;
struct Inner {
odd: ::weird::Weird,
}
struct Outer {
inner: Inner,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let expected = trait_set([TypespaceTrait::Clone, TypespaceTrait::PartialEq]);
assert_eq!(built_traits(&typespace, "Inner"), expected);
assert_eq!(built_traits(&typespace, "Outer"), expected);
}
#[test]
fn required_trait_survives_desired_strip_in_same_group() {
let mut builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::Eq)
.with_desired_trait(TypespaceTrait::Ord),
{
struct S {
stamp: PartialOnly,
}
}
);
builder
.insert(
"PartialOnly".to_string(),
Type::Native(Native::new(
"partial::PartialOnly",
trait_set([TypespaceTrait::Eq, TypespaceTrait::PartialEq]),
Vec::new(),
)),
)
.unwrap();
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Eq, TypespaceTrait::PartialEq])
);
}
#[test]
fn required_partial_eq_supports_desired_eq() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::PartialEq)
.with_desired_trait(TypespaceTrait::Eq),
{
struct S {
count: u32,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Eq, TypespaceTrait::PartialEq])
);
}
#[test]
fn phase_one_grant_outlives_a_dead_desired_request() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::PartialEq)
.with_desired_trait(TypespaceTrait::Ord),
{
struct S {
weight: f64,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::PartialEq])
);
}
#[test]
fn map_key_grant_is_untouched_by_a_desired_drop() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Key {
name: String,
}
struct S {
lookup: Map<Key, f64>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Key"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Ord,
TypespaceTrait::PartialOrd,
])
);
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn required_conflict_reported_alongside_desired_traits() {
let builder = typespace_builder!(
Settings::minimal()
.with_required_trait(TypespaceTrait::Display)
.with_desired_trait(TypespaceTrait::Eq),
{
struct S {
name: String,
}
}
);
let Err(Error::TraitConflicts { conflicts }) = builder.finalize(no_cycles) else {
panic!("expected finalization to report trait conflicts");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Display);
}
#[test]
fn required_display_through_option_conflicts() {
let builder = typespace_builder!(
Settings::minimal().with_required_trait(TypespaceTrait::Display),
{
struct Wrapper(Nullable<String>);
}
);
let Err(Error::TraitConflicts { conflicts }) = builder.finalize(no_cycles) else {
panic!("expected finalization to report trait conflicts");
};
assert_eq!(conflicts.len(), 1, "conflicts: {conflicts:#?}");
assert_eq!(conflicts[0].required, TypespaceTrait::Display);
assert_eq!(conflicts[0].offender, "Nullable<String>");
}
#[test]
fn option_has_no_display_or_from_str() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Display,
TypespaceTrait::FromStr,
]),
{
type MaybeName = Nullable<String>;
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "MaybeName"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_dropped_through_vec_inner() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
struct Wrapper(Vec<String>);
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Wrapper"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn box_has_no_display_or_from_str() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Display,
TypespaceTrait::FromStr,
]),
{
struct Wrapper(Box<String>);
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Wrapper"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_not_available_through_tuple() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
struct Wrapper((String, String));
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Wrapper"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_forwards_to_simple_enum_inner() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
enum Color {
Red,
Green,
}
struct Wrapper(Color);
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let with_display = trait_set([TypespaceTrait::Clone, TypespaceTrait::Display]);
assert_eq!(built_traits(&typespace, "Wrapper"), with_display);
assert_eq!(built_traits(&typespace, "Color"), with_display);
}
#[test]
fn desired_granted_on_unit_struct() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Eq,
TypespaceTrait::Hash,
TypespaceTrait::Display,
]),
{
#[json = "marker"]
struct Marker;
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Marker"),
trait_set([
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Hash,
])
);
}
#[test]
fn desired_partial_group_survives_float_in_vec() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::PartialEq, TypespaceTrait::Eq]),
{
struct S {
list: Vec<f64>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::PartialEq])
);
}
#[test]
fn variant_payload_blocks_desired_trait() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::PartialEq,
TypespaceTrait::Eq,
]),
{
enum Reading {
Missing,
Weight(f64),
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Reading"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::PartialEq])
);
}
#[test]
fn derived_default_needs_every_field() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
struct S {
color: Color,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "S"), only_clone);
assert_eq!(built_traits(&typespace, "Color"), only_clone);
}
#[test]
fn desired_default_blocked_by_array_element() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
struct S {
swatch: [Color; 3],
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_default_survives_vec_of_default_less_element() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
struct S {
#[default]
swatch: Vec<Color>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
}
#[test]
fn option_provides_default_whatever_it_wraps() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum Color {
Red,
Green,
}
struct S {
#[default]
color: Nullable<Color>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
assert_eq!(
built_traits(&typespace, "Color"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_default_absorbed_by_container_hop() {
let builder = typespace_builder!(minimal_with_desired([TypespaceTrait::Default]), {
enum NoDefault {
Red,
Green,
}
struct S {
#[default]
list: Vec<NoDefault>,
#[default]
maybe: Nullable<NoDefault>,
}
});
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Default])
);
assert_eq!(
built_traits(&typespace, "NoDefault"),
TypespaceTraitSet::empty()
);
}
#[test]
fn desired_loss_climbs_nested_containers() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct S {
deep: Map<String, Vec<Nullable<f64>>>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_evaluates_nested_container_structure() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Capable {
deep: Vec<Map<String, Nullable<Vec<u32>>>>,
}
struct Blocked {
deep: Vec<Map<String, Nullable<Vec<f64>>>>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Capable"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
assert_eq!(
built_traits(&typespace, "Blocked"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_never_field_keeps_derived_std_traits() {
let builder = typespace_builder!(
minimal_with_desired([
TypespaceTrait::Eq,
TypespaceTrait::Hash,
TypespaceTrait::Default,
]),
{
struct S {
gone: Optional<!>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "S"),
trait_set([
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
TypespaceTrait::Hash,
TypespaceTrait::Default,
])
);
}
#[test]
fn desired_display_on_untagged_item_variants() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
#[untagged]
enum U {
Text(String),
Count(u32),
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "U"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Display])
);
}
#[test]
fn untagged_enum_display_needs_single_payload_variants() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
#[untagged]
enum E {
One(String),
Pair(String, String),
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "E"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn desired_display_rejected_on_untagged_struct_variant() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
#[untagged]
enum U {
Text(String),
Pair { left: String, right: String },
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "U"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn self_referential_type_keeps_desired_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Node {
next: Box<Node>,
count: u32,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Node"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
}
#[test]
fn desired_survives_cycle_through_vec() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Node {
kids: Vec<Node>,
name: String,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Node"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
}
#[test]
fn desired_stripped_in_cycle_through_vec() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Node {
kids: Vec<Node>,
weight: f64,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "Node"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn cycle_entered_from_the_blocking_member_strips_both() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct A {
b: Box<B>,
weight: f64,
}
struct B {
a: Box<A>,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "A"), only_clone);
assert_eq!(built_traits(&typespace, "B"), only_clone);
}
#[test]
fn three_member_cycle_strips_every_member() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct A {
b: Box<B>,
}
struct B {
c: Box<C>,
}
struct C {
a: Box<A>,
weight: f64,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for id in ["A", "B", "C"] {
assert_eq!(built_traits(&typespace, id), only_clone, "{id}");
}
}
#[test]
fn type_above_a_blocked_cycle_loses_the_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Above {
member: Cycle1,
}
struct Cycle1 {
other: Box<Cycle2>,
}
struct Cycle2 {
other: Box<Cycle1>,
weight: f64,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for id in ["Above", "Cycle1", "Cycle2"] {
assert_eq!(built_traits(&typespace, id), only_clone, "{id}");
}
}
#[test]
fn type_above_a_clean_cycle_keeps_the_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Clean1 {
other: Box<Clean2>,
count: u32,
}
struct Clean2 {
other: Box<Clean1>,
}
struct Holder {
member: Clean1,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let with_eq = trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
]);
for id in ["Clean1", "Clean2", "Holder"] {
assert_eq!(built_traits(&typespace, id), with_eq, "{id}");
}
}
#[test]
fn alias_chain_forwards_desired_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Blocked {
weight: f64,
}
type Near = Blocked;
type Far = Near;
}
);
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for id in ["Blocked", "Near", "Far"] {
assert_eq!(built_traits(&typespace, id), only_clone, "{id}");
}
}
static SKIP_LOG: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
struct SkipLogCapture;
impl log::Log for SkipLogCapture {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
SKIP_LOG.lock().unwrap().push(record.args().to_string());
}
fn flush(&self) {}
}
static SKIP_LOG_CAPTURE: SkipLogCapture = SkipLogCapture;
fn capture_skip_log() {
log::set_logger(&SKIP_LOG_CAPTURE).ok();
log::set_max_level(log::LevelFilter::Debug);
}
fn skip_line(trait_name: TypespaceTrait, loser: &str, blocker: &str) -> String {
format!("desired trait {trait_name} dropped from {loser}, blocked by {blocker}")
}
fn skip_logged(line: &str) -> bool {
SKIP_LOG.lock().unwrap().iter().any(|entry| entry == line)
}
#[test]
fn desired_skip_log_names_the_blocker_at_each_hop() {
capture_skip_log();
let builder = typespace_builder!(minimal_with_desired([TypespaceTrait::Eq]), {
struct ChainBottom {
weight: f64,
}
struct ChainMiddle {
bottom: ChainBottom,
}
struct ChainTop {
middle: ChainMiddle,
}
});
builder.finalize(no_cycles).unwrap();
for (loser, blocker) in [
("ChainBottom", "f64"),
("ChainMiddle", "ChainBottom"),
("ChainTop", "ChainMiddle"),
] {
let line = skip_line(TypespaceTrait::Eq, loser, blocker);
assert!(skip_logged(&line), "missing skip line: {line}");
}
}
#[test]
fn desired_skip_log_covers_strip_dependents_removals() {
capture_skip_log();
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Eq, TypespaceTrait::Ord]),
{
native weird::StripWeird: Eq + Ord;
struct StripHolder {
odd: weird::StripWeird,
}
}
);
let typespace = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&typespace, "StripHolder"),
TypespaceTraitSet::empty()
);
for trait_name in [
TypespaceTrait::PartialEq,
TypespaceTrait::PartialOrd,
TypespaceTrait::Eq,
TypespaceTrait::Ord,
] {
let line = skip_line(trait_name, "StripHolder", "weird::StripWeird");
assert!(skip_logged(&line), "missing skip line: {line}");
}
}
#[test]
fn desired_removal_crosses_a_deep_chain() {
use crate::TypespaceBuilder;
use crate::build::NewtypeStruct;
const DEPTH: usize = 32;
let mut builder = TypespaceBuilder::<String>::new(minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
]));
builder
.insert("Leaf".to_string(), Type::Float("f64".to_string()))
.unwrap();
for level in 0..DEPTH {
let inner = match level + 1 == DEPTH {
true => "Leaf".to_string(),
false => format!("L{}", level + 1),
};
let wrapper = NewtypeStruct::new(inner)
.name(format!("L{level}"))
.build()
.unwrap();
builder.insert(format!("L{level}"), wrapper).unwrap();
}
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for level in 0..DEPTH {
assert_eq!(
built_traits(&typespace, &format!("L{level}")),
only_clone,
"level {level}"
);
}
}
#[test]
fn deep_containment_chain_resolves() {
use crate::build::{Struct, StructProperty};
const DEPTH: usize = 2000;
let mut builder = crate::TypespaceBuilder::new(minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
]));
for level in 0..DEPTH {
let next = match level + 1 == DEPTH {
true => "f64".to_string(),
false => format!("Level{}", level + 1),
};
builder
.insert(
format!("Level{level}"),
Struct::<String>::new()
.name(format!("Level{level}"))
.properties([StructProperty::new("next", next)])
.build()
.unwrap(),
)
.unwrap();
}
builder
.insert("f64".to_string(), Type::Float("f64".to_string()))
.unwrap();
let typespace = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
assert_eq!(built_traits(&typespace, "Level0"), only_clone);
assert_eq!(
built_traits(&typespace, &format!("Level{}", DEPTH - 1)),
only_clone
);
}
#[test]
fn probe_all_traits_simple_enum_renders() {
let builder = typespace_builder!(Settings::maximal(), {
enum Color {
Red,
Green,
}
});
let ts = builder.finalize(no_cycles).unwrap();
assert!(built_traits(&ts, "Color").contains(&TypespaceTrait::Display));
let out = ts.to_codespace();
println!("{}", out.into_stream());
}
#[test]
fn probe_diamond_and_duplicate_edges() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Bottom {
w: f64,
}
struct Left {
b: Bottom,
b2: Bottom,
}
struct Right {
b: Bottom,
}
struct Top {
l: Left,
r: Right,
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for id in ["Bottom", "Left", "Right", "Top"] {
assert_eq!(built_traits(&ts, id), only_clone, "{id}");
}
}
#[test]
fn probe_named_firebreak_stops_loss() {
let builder = typespace_builder!(minimal_with_desired([TypespaceTrait::Default]), {
enum NoDef {
A,
B,
}
#[default = { x: "A" }]
struct Middle {
x: NoDef,
}
struct Top {
#[default]
m: Middle,
}
});
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(built_traits(&ts, "NoDef"), TypespaceTraitSet::empty());
assert_eq!(
built_traits(&ts, "Middle"),
trait_set([TypespaceTrait::Default])
);
assert_eq!(
built_traits(&ts, "Top"),
trait_set([TypespaceTrait::Default]),
"Top should keep Default: Middle absorbs the loss"
);
}
#[test]
fn probe_chained_manual_obligations() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
struct Inner {
s: String,
}
struct Wrapper(Inner);
#[untagged]
enum U {
W(Wrapper),
T(String),
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
let only_clone = trait_set([TypespaceTrait::Clone]);
for id in ["Inner", "Wrapper", "U"] {
assert_eq!(built_traits(&ts, id), only_clone, "{id}");
}
}
#[test]
fn probe_chained_manual_obligations_capable() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
struct Wrapper(String);
#[untagged]
enum U {
W(Wrapper),
T(u32),
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
let with_display = trait_set([TypespaceTrait::Clone, TypespaceTrait::Display]);
for id in ["Wrapper", "U"] {
assert_eq!(built_traits(&ts, id), with_display, "{id}");
}
}
#[test]
fn probe_cycle_through_newtype() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct A(Box<B>);
struct B(A);
}
);
let ts = builder.finalize(no_cycles).unwrap();
let with_eq = trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
]);
for id in ["A", "B"] {
assert_eq!(built_traits(&ts, id), with_eq, "{id}");
}
}
#[test]
fn probe_untagged_self_cycle_display() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Display]),
{
#[untagged]
enum U {
Nested(Box<U>),
Leaf(String),
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(built_traits(&ts, "U"), trait_set([TypespaceTrait::Clone]));
}
#[test]
fn probe_grant_supports_desired_elsewhere() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Eq]),
{
struct Key {
name: String,
}
struct HoldsMap {
lookup: Map<Key, f64>,
}
struct HoldsKey {
k: Key,
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&ts, "HoldsKey"),
trait_set([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
TypespaceTrait::PartialEq,
])
);
assert_eq!(
built_traits(&ts, "HoldsMap"),
trait_set([TypespaceTrait::Clone])
);
}
#[test]
fn probe_tuple_struct_rest_blocks() {
let mut builder = crate::TypespaceBuilder::new(minimal_with_desired([
TypespaceTrait::Clone,
TypespaceTrait::Eq,
]));
builder
.insert(
"floats".to_string(),
crate::build::Type::Vec("f64".to_string()),
)
.unwrap();
builder
.insert(
"f64".to_string(),
crate::build::Type::Float("f64".to_string()),
)
.unwrap();
builder
.insert("String".to_string(), crate::build::Type::String)
.unwrap();
builder
.insert(
"T".to_string(),
TupleStruct::new()
.name("T")
.fields(["String".to_string()])
.rest("floats".to_string())
.build()
.unwrap(),
)
.unwrap();
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(built_traits(&ts, "T"), trait_set([TypespaceTrait::Clone]));
}
#[test]
fn probe_alias_to_container_default() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
enum NoDef {
A,
B,
}
type L = Vec<NoDef>;
}
);
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&ts, "L"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
}
#[test]
fn test_optional_and_default_result_in_default_trait() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
struct Foo {
a: Optional<String>,
#[default = 12]
b: u32,
#[default]
c: String,
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&ts, "Foo"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
}
#[test]
fn test_no_obligation_with_default_value() {
let builder = typespace_builder!(
minimal_with_desired([TypespaceTrait::Clone, TypespaceTrait::Default]),
{
#[default = {}]
struct Outer {
#[default = { a: "x"}]
inner: Inner,
}
struct Inner {
a: String,
}
}
);
let ts = builder.finalize(no_cycles).unwrap();
assert_eq!(
built_traits(&ts, "Outer"),
trait_set([TypespaceTrait::Clone, TypespaceTrait::Default])
);
assert_eq!(
built_traits(&ts, "Inner"),
trait_set([TypespaceTrait::Clone])
);
}
fn is_irrefutable(typespace: &Typespace<String>, id: &str) -> bool {
from_string_irrefutable(&typespace.types, &id.to_string())
}
#[test]
fn irrefutable_holds_for_string_alone_among_primitives() {
let builder = typespace_builder!(Settings::minimal(), {
struct Holder {
text: String,
count: u32,
}
});
let ts = builder.finalize(no_cycles).unwrap();
assert!(is_irrefutable(&ts, "String"));
assert!(!is_irrefutable(&ts, "u32"));
assert!(!is_irrefutable(&ts, "Holder"));
}
#[test]
fn irrefutable_follows_unconstrained_newtypes() {
let builder = typespace_builder!(Settings::minimal(), {
struct Wrapper(String);
struct Rewrapper(Wrapper);
struct Port(u32);
});
let ts = builder.finalize(no_cycles).unwrap();
assert!(is_irrefutable(&ts, "Wrapper"));
assert!(is_irrefutable(&ts, "Rewrapper"));
assert!(!is_irrefutable(&ts, "Port"));
}
#[test]
fn irrefutable_follows_type_aliases() {
let builder = typespace_builder!(Settings::minimal(), {
type Text = String;
type Count = u32;
type Wrapped = Wrapper;
struct Wrapper(String);
});
let ts = builder.finalize(no_cycles).unwrap();
assert!(is_irrefutable(&ts, "Text"));
assert!(is_irrefutable(&ts, "Wrapped"));
assert!(!is_irrefutable(&ts, "Count"));
}
#[test]
fn irrefutable_stops_at_every_constraint_kind() {
let mut builder = TypespaceBuilder::new(Settings::minimal());
builder.insert("string".to_string(), Type::String).unwrap();
let newtype = |name: &str, constraints: NewtypeConstraints| {
Type::NewtypeStruct(
NewtypeStruct::new("string".to_string())
.name(name)
.constraints(constraints),
)
};
builder
.insert(
"Plain".to_string(),
newtype("Plain", NewtypeConstraints::None),
)
.unwrap();
builder
.insert(
"Patterned".to_string(),
newtype(
"Patterned",
NewtypeConstraints::String {
min: None,
max: None,
patterns: vec![".*".to_string()],
},
),
)
.unwrap();
builder
.insert(
"Allowed".to_string(),
newtype(
"Allowed",
NewtypeConstraints::AllowList(vec![JsonValue::new(serde_json::json!("yes"))]),
),
)
.unwrap();
builder
.insert(
"Denied".to_string(),
newtype(
"Denied",
NewtypeConstraints::DenyList(vec![JsonValue::new(serde_json::json!("no"))]),
),
)
.unwrap();
let ts = builder.finalize(no_cycles).unwrap();
assert!(is_irrefutable(&ts, "Plain"));
assert!(!is_irrefutable(&ts, "Patterned"));
assert!(!is_irrefutable(&ts, "Allowed"));
assert!(!is_irrefutable(&ts, "Denied"));
}
#[test]
fn irrefutable_excludes_natives_and_containers() {
let mut builder = TypespaceBuilder::new(Settings::minimal());
builder.insert("string".to_string(), Type::String).unwrap();
builder
.insert(
"::string::Str".to_string(),
Type::Native(Native::new_string_like("::string::Str")),
)
.unwrap();
builder
.insert("Box<String>".to_string(), Type::Box("string".to_string()))
.unwrap();
builder
.insert(
"Nullable<String>".to_string(),
Type::Option("string".to_string()),
)
.unwrap();
builder
.insert("Vec<String>".to_string(), Type::Vec("string".to_string()))
.unwrap();
let ts = builder.finalize(no_cycles).unwrap();
assert!(is_irrefutable(&ts, "string"));
assert!(!is_irrefutable(&ts, "::string::Str"));
assert!(!is_irrefutable(&ts, "Box<String>"));
assert!(!is_irrefutable(&ts, "Nullable<String>"));
assert!(!is_irrefutable(&ts, "Vec<String>"));
}
#[test]
fn irrefutable_terminates_on_a_cycle() {
let builder = typespace_builder!(Settings::minimal(), {
struct Loop(Knot);
struct Knot(Loop);
struct Plain(String);
});
let ts = builder
.finalize(|id: &String| format!("Box<{id}>"))
.unwrap();
assert!(!is_irrefutable(&ts, "Loop"));
assert!(!is_irrefutable(&ts, "Knot"));
assert!(is_irrefutable(&ts, "Plain"));
}
}