bevy_reflect: TypeInfo casting methods (#13320)

# Objective

There are times when we might know the type of a `TypeInfo` ahead of
time. Or we may have already checked it one way or another.

In such cases, it's a bit cumbersome to have to pattern match every time
we want to access the nested info:

```rust
if let TypeInfo::List(info) = <Vec<i32>>::type_info() {
  // ...
} else {
  panic!("expected list info");
}
```

Ideally, there would be a way to simply perform the cast down to
`ListInfo` since we already know it will succeed.

Or even if we don't, perhaps we just want a cleaner way of exiting a
function early (i.e. with the `?` operator).

## Solution

Taking a bit from
[`mirror-mirror`](https://docs.rs/mirror-mirror/latest/mirror_mirror/struct.TypeDescriptor.html#implementations),
`TypeInfo` now has methods for attempting a cast into the variant's info
type.

```rust
let info = <Vec<i32>>::type_info().as_list().unwrap();
// ...
```

These new conversion methods return a `Result` where the error type is a
new `TypeInfoError` enum.

A `Result` was chosen as the return type over `Option` because if we do
choose to `unwrap` it, the error message will give us some indication of
what went wrong. In other words, it can truly replace those instances
where we were panicking in the `else` case.

### Open Questions

1. Should the error types instead be a struct? I chose an enum for
future-proofing, but right now it only has one error state.
Alternatively, we could make it a reflect-wide casting error so it could
be used for similar methods on `ReflectRef` and friends.
2. I was going to do it in a separate PR but should I just go ahead and
add similar methods to `ReflectRef`, `ReflectMut`, and `ReflectOwned`? 🤔
3. Should we name these `try_as_***` instead of `as_***` since they
return a `Result`?

## Testing

You can test locally by running:

```
cargo test --package bevy_reflect
```

---

## Changelog

### Added

- `TypeInfoError` enum
- `TypeInfo::kind` method
- `TypeInfo::as_struct` method
- `TypeInfo::as_tuple_struct` method
- `TypeInfo::as_tuple` method
- `TypeInfo::as_list` method
- `TypeInfo::as_array` method
- `TypeInfo::as_map` method
- `TypeInfo::as_enum` method
- `TypeInfo::as_value` method
- `VariantInfoError` enum
- `VariantInfo::variant_type` method
- `VariantInfo::as_unit_variant` method
- `VariantInfo::as_tuple_variant` method
- `VariantInfo::as_struct_variant` method
This commit is contained in:
Gino Valente 2024-07-14 13:10:31 -07:00 committed by GitHub
parent 9f376df2d5
commit e512cb602c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 237 additions and 138 deletions

View file

@ -2,7 +2,9 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes};
use crate::{NamedField, UnnamedField};
use bevy_utils::HashMap;
use std::slice::Iter;
use std::sync::Arc;
use thiserror::Error;
/// Describes the form of an enum variant.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -35,6 +37,19 @@ pub enum VariantType {
Unit,
}
/// A [`VariantInfo`]-specific error.
#[derive(Debug, Error)]
pub enum VariantInfoError {
/// Caused when a variant was expected to be of a certain [type], but was not.
///
/// [type]: VariantType
#[error("variant type mismatch: expected {expected:?}, received {received:?}")]
TypeMismatch {
expected: VariantType,
received: VariantType,
},
}
/// A container for compile-time enum variant info.
#[derive(Clone, Debug)]
pub enum VariantInfo {
@ -85,6 +100,17 @@ impl VariantInfo {
}
}
/// Returns the [type] of this variant.
///
/// [type]: VariantType
pub fn variant_type(&self) -> VariantType {
match self {
Self::Struct(_) => VariantType::Struct,
Self::Tuple(_) => VariantType::Tuple,
Self::Unit(_) => VariantType::Unit,
}
}
impl_custom_attribute_methods!(
self,
match self {
@ -96,6 +122,29 @@ impl VariantInfo {
);
}
macro_rules! impl_cast_method {
($name:ident : $kind:ident => $info:ident) => {
#[doc = concat!("Attempts a cast to [`", stringify!($info), "`].")]
#[doc = concat!("\n\nReturns an error if `self` is not [`VariantInfo::", stringify!($kind), "`].")]
pub fn $name(&self) -> Result<&$info, VariantInfoError> {
match self {
Self::$kind(info) => Ok(info),
_ => Err(VariantInfoError::TypeMismatch {
expected: VariantType::$kind,
received: self.variant_type(),
}),
}
}
};
}
/// Conversion convenience methods for [`VariantInfo`].
impl VariantInfo {
impl_cast_method!(as_struct_variant: Struct => StructVariantInfo);
impl_cast_method!(as_tuple_variant: Tuple => TupleVariantInfo);
impl_cast_method!(as_unit_variant: Unit => UnitVariantInfo);
}
/// Type info for struct variants.
#[derive(Clone, Debug)]
pub struct StructVariantInfo {
@ -304,3 +353,28 @@ impl UnitVariantInfo {
impl_custom_attribute_methods!(self.custom_attributes, "variant");
}
#[cfg(test)]
mod tests {
use super::*;
use crate as bevy_reflect;
use crate::{Reflect, Typed};
#[test]
fn should_return_error_on_invalid_cast() {
#[derive(Reflect)]
enum Foo {
Bar,
}
let info = Foo::type_info().as_enum().unwrap();
let variant = info.variant_at(0).unwrap();
assert!(matches!(
variant.as_tuple_variant(),
Err(VariantInfoError::TypeMismatch {
expected: VariantType::Tuple,
received: VariantType::Unit
})
));
}
}

View file

@ -1550,18 +1550,14 @@ mod tests {
bar: usize,
}
let info = MyStruct::type_info();
if let TypeInfo::Struct(info) = info {
assert!(info.is::<MyStruct>());
assert_eq!(MyStruct::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path());
assert_eq!(TypeId::of::<i32>(), info.field("foo").unwrap().type_id());
assert!(info.field("foo").unwrap().is::<i32>());
assert_eq!("foo", info.field("foo").unwrap().name());
assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path());
} else {
panic!("Expected `TypeInfo::Struct`");
}
let info = MyStruct::type_info().as_struct().unwrap();
assert!(info.is::<MyStruct>());
assert_eq!(MyStruct::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path());
assert_eq!(TypeId::of::<i32>(), info.field("foo").unwrap().type_id());
assert!(info.field("foo").unwrap().is::<i32>());
assert_eq!("foo", info.field("foo").unwrap().name());
assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path());
let value: &dyn Reflect = &MyStruct { foo: 123, bar: 321 };
let info = value.get_represented_type_info().unwrap();
@ -1574,16 +1570,13 @@ mod tests {
bar: usize,
}
let info = <MyGenericStruct<i32>>::type_info();
if let TypeInfo::Struct(info) = info {
assert!(info.is::<MyGenericStruct<i32>>());
assert_eq!(MyGenericStruct::<i32>::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path());
assert_eq!("foo", info.field("foo").unwrap().name());
assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path());
} else {
panic!("Expected `TypeInfo::Struct`");
}
let info = <MyGenericStruct<i32>>::type_info().as_struct().unwrap();
assert!(info.is::<MyGenericStruct<i32>>());
assert_eq!(MyGenericStruct::<i32>::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path());
assert_eq!("foo", info.field("foo").unwrap().name());
assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path());
let value: &dyn Reflect = &MyGenericStruct {
foo: String::from("Hello!"),
@ -1596,27 +1589,21 @@ mod tests {
#[derive(Reflect)]
struct MyTupleStruct(usize, i32, MyStruct);
let info = MyTupleStruct::type_info();
if let TypeInfo::TupleStruct(info) = info {
assert!(info.is::<MyTupleStruct>());
assert_eq!(MyTupleStruct::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field_at(1).unwrap().type_path());
assert!(info.field_at(1).unwrap().is::<i32>());
} else {
panic!("Expected `TypeInfo::TupleStruct`");
}
let info = MyTupleStruct::type_info().as_tuple_struct().unwrap();
assert!(info.is::<MyTupleStruct>());
assert_eq!(MyTupleStruct::type_path(), info.type_path());
assert_eq!(i32::type_path(), info.field_at(1).unwrap().type_path());
assert!(info.field_at(1).unwrap().is::<i32>());
// Tuple
type MyTuple = (u32, f32, String);
let info = MyTuple::type_info();
if let TypeInfo::Tuple(info) = info {
assert!(info.is::<MyTuple>());
assert_eq!(MyTuple::type_path(), info.type_path());
assert_eq!(f32::type_path(), info.field_at(1).unwrap().type_path());
} else {
panic!("Expected `TypeInfo::Tuple`");
}
let info = MyTuple::type_info().as_tuple().unwrap();
assert!(info.is::<MyTuple>());
assert_eq!(MyTuple::type_path(), info.type_path());
assert_eq!(f32::type_path(), info.field_at(1).unwrap().type_path());
let value: &dyn Reflect = &(123_u32, 1.23_f32, String::from("Hello!"));
let info = value.get_represented_type_info().unwrap();
@ -1625,15 +1612,12 @@ mod tests {
// List
type MyList = Vec<usize>;
let info = MyList::type_info();
if let TypeInfo::List(info) = info {
assert!(info.is::<MyList>());
assert!(info.item_is::<usize>());
assert_eq!(MyList::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.item_type_path_table().path());
} else {
panic!("Expected `TypeInfo::List`");
}
let info = MyList::type_info().as_list().unwrap();
assert!(info.is::<MyList>());
assert!(info.item_is::<usize>());
assert_eq!(MyList::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.item_type_path_table().path());
let value: &dyn Reflect = &vec![123_usize];
let info = value.get_represented_type_info().unwrap();
@ -1644,15 +1628,11 @@ mod tests {
{
type MySmallVec = smallvec::SmallVec<[String; 2]>;
let info = MySmallVec::type_info();
if let TypeInfo::List(info) = info {
assert!(info.is::<MySmallVec>());
assert!(info.item_is::<String>());
assert_eq!(MySmallVec::type_path(), info.type_path());
assert_eq!(String::type_path(), info.item_type_path_table().path());
} else {
panic!("Expected `TypeInfo::List`");
}
let info = MySmallVec::type_info().as_list().unwrap();
assert!(info.is::<MySmallVec>());
assert!(info.item_is::<String>());
assert_eq!(MySmallVec::type_path(), info.type_path());
assert_eq!(String::type_path(), info.item_type_path_table().path());
let value: MySmallVec = smallvec::smallvec![String::default(); 2];
let value: &dyn Reflect = &value;
@ -1663,16 +1643,12 @@ mod tests {
// Array
type MyArray = [usize; 3];
let info = MyArray::type_info();
if let TypeInfo::Array(info) = info {
assert!(info.is::<MyArray>());
assert!(info.item_is::<usize>());
assert_eq!(MyArray::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.item_type_path_table().path());
assert_eq!(3, info.capacity());
} else {
panic!("Expected `TypeInfo::Array`");
}
let info = MyArray::type_info().as_array().unwrap();
assert!(info.is::<MyArray>());
assert!(info.item_is::<usize>());
assert_eq!(MyArray::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.item_type_path_table().path());
assert_eq!(3, info.capacity());
let value: &dyn Reflect = &[1usize, 2usize, 3usize];
let info = value.get_represented_type_info().unwrap();
@ -1681,13 +1657,10 @@ mod tests {
// Cow<'static, str>
type MyCowStr = Cow<'static, str>;
let info = MyCowStr::type_info();
if let TypeInfo::Value(info) = info {
assert!(info.is::<MyCowStr>());
assert_eq!(std::any::type_name::<MyCowStr>(), info.type_path());
} else {
panic!("Expected `TypeInfo::Value`");
}
let info = MyCowStr::type_info().as_value().unwrap();
assert!(info.is::<MyCowStr>());
assert_eq!(std::any::type_name::<MyCowStr>(), info.type_path());
let value: &dyn Reflect = &Cow::<'static, str>::Owned("Hello!".to_string());
let info = value.get_represented_type_info().unwrap();
@ -1696,18 +1669,15 @@ mod tests {
// Cow<'static, [u8]>
type MyCowSlice = Cow<'static, [u8]>;
let info = MyCowSlice::type_info();
if let TypeInfo::List(info) = info {
assert!(info.is::<MyCowSlice>());
assert!(info.item_is::<u8>());
assert_eq!(std::any::type_name::<MyCowSlice>(), info.type_path());
assert_eq!(
std::any::type_name::<u8>(),
info.item_type_path_table().path()
);
} else {
panic!("Expected `TypeInfo::List`");
}
let info = MyCowSlice::type_info().as_list().unwrap();
assert!(info.is::<MyCowSlice>());
assert!(info.item_is::<u8>());
assert_eq!(std::any::type_name::<MyCowSlice>(), info.type_path());
assert_eq!(
std::any::type_name::<u8>(),
info.item_type_path_table().path()
);
let value: &dyn Reflect = &Cow::<'static, [u8]>::Owned(vec![0, 1, 2, 3]);
let info = value.get_represented_type_info().unwrap();
@ -1716,17 +1686,14 @@ mod tests {
// Map
type MyMap = HashMap<usize, f32>;
let info = MyMap::type_info();
if let TypeInfo::Map(info) = info {
assert!(info.is::<MyMap>());
assert!(info.key_is::<usize>());
assert!(info.value_is::<f32>());
assert_eq!(MyMap::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.key_type_path_table().path());
assert_eq!(f32::type_path(), info.value_type_path_table().path());
} else {
panic!("Expected `TypeInfo::Map`");
}
let info = MyMap::type_info().as_map().unwrap();
assert!(info.is::<MyMap>());
assert!(info.key_is::<usize>());
assert!(info.value_is::<f32>());
assert_eq!(MyMap::type_path(), info.type_path());
assert_eq!(usize::type_path(), info.key_type_path_table().path());
assert_eq!(f32::type_path(), info.value_type_path_table().path());
let value: &dyn Reflect = &MyMap::new();
let info = value.get_represented_type_info().unwrap();
@ -1735,13 +1702,10 @@ mod tests {
// Value
type MyValue = String;
let info = MyValue::type_info();
if let TypeInfo::Value(info) = info {
assert!(info.is::<MyValue>());
assert_eq!(MyValue::type_path(), info.type_path());
} else {
panic!("Expected `TypeInfo::Value`");
}
let info = MyValue::type_info().as_value().unwrap();
assert!(info.is::<MyValue>());
assert_eq!(MyValue::type_path(), info.type_path());
let value: &dyn Reflect = &String::from("Hello!");
let info = value.get_represented_type_info().unwrap();
@ -1881,15 +1845,12 @@ mod tests {
data: Vec<i32>,
}
let info = <SomeStruct as Typed>::type_info();
if let TypeInfo::Struct(info) = info {
let mut fields = info.iter();
assert_eq!(Some(" The name"), fields.next().unwrap().docs());
assert_eq!(Some(" The index"), fields.next().unwrap().docs());
assert_eq!(None, fields.next().unwrap().docs());
} else {
panic!("expected struct info");
}
let info = <SomeStruct as Typed>::type_info().as_struct().unwrap();
let mut fields = info.iter();
assert_eq!(Some(" The name"), fields.next().unwrap().docs());
assert_eq!(Some(" The index"), fields.next().unwrap().docs());
assert_eq!(None, fields.next().unwrap().docs());
}
#[test]
@ -1910,31 +1871,20 @@ mod tests {
},
}
let info = <SomeEnum as Typed>::type_info();
if let TypeInfo::Enum(info) = info {
let mut variants = info.iter();
assert_eq!(None, variants.next().unwrap().docs());
let info = <SomeEnum as Typed>::type_info().as_enum().unwrap();
let variant = variants.next().unwrap();
assert_eq!(Some(" Option A"), variant.docs());
if let VariantInfo::Tuple(variant) = variant {
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Index"), field.docs());
} else {
panic!("expected tuple variant")
}
let mut variants = info.iter();
assert_eq!(None, variants.next().unwrap().docs());
let variant = variants.next().unwrap();
assert_eq!(Some(" Option B"), variant.docs());
if let VariantInfo::Struct(variant) = variant {
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Name"), field.docs());
} else {
panic!("expected struct variant")
}
} else {
panic!("expected enum info");
}
let variant = variants.next().unwrap().as_tuple_variant().unwrap();
assert_eq!(Some(" Option A"), variant.docs());
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Index"), field.docs());
let variant = variants.next().unwrap().as_struct_variant().unwrap();
assert_eq!(Some(" Option B"), variant.docs());
let field = variant.field_at(0).unwrap();
assert_eq!(Some(" Name"), field.docs());
}
}

View file

@ -1,9 +1,10 @@
use crate::{
ArrayInfo, EnumInfo, ListInfo, MapInfo, Reflect, StructInfo, TupleInfo, TupleStructInfo,
TypePath, TypePathTable,
ArrayInfo, EnumInfo, ListInfo, MapInfo, Reflect, ReflectKind, StructInfo, TupleInfo,
TupleStructInfo, TypePath, TypePathTable,
};
use std::any::{Any, TypeId};
use std::fmt::Debug;
use thiserror::Error;
/// A static accessor to compile-time type information.
///
@ -81,6 +82,19 @@ pub trait Typed: Reflect + TypePath {
fn type_info() -> &'static TypeInfo;
}
/// A [`TypeInfo`]-specific error.
#[derive(Debug, Error)]
pub enum TypeInfoError {
/// Caused when a type was expected to be of a certain [kind], but was not.
///
/// [kind]: ReflectKind
#[error("kind mismatch: expected {expected:?}, received {received:?}")]
KindMismatch {
expected: ReflectKind,
received: ReflectKind,
},
}
/// Compile-time type information for various reflected types.
///
/// Generally, for any given type, this value can be retrieved one of three ways:
@ -174,6 +188,50 @@ impl TypeInfo {
Self::Value(info) => info.docs(),
}
}
/// Returns the [kind] of this `TypeInfo`.
///
/// [kind]: ReflectKind
pub fn kind(&self) -> ReflectKind {
match self {
Self::Struct(_) => ReflectKind::Struct,
Self::TupleStruct(_) => ReflectKind::TupleStruct,
Self::Tuple(_) => ReflectKind::Tuple,
Self::List(_) => ReflectKind::List,
Self::Array(_) => ReflectKind::Array,
Self::Map(_) => ReflectKind::Map,
Self::Enum(_) => ReflectKind::Enum,
Self::Value(_) => ReflectKind::Value,
}
}
}
macro_rules! impl_cast_method {
($name:ident : $kind:ident => $info:ident) => {
#[doc = concat!("Attempts a cast to [`", stringify!($info), "`].")]
#[doc = concat!("\n\nReturns an error if `self` is not [`TypeInfo::", stringify!($kind), "`].")]
pub fn $name(&self) -> Result<&$info, TypeInfoError> {
match self {
Self::$kind(info) => Ok(info),
_ => Err(TypeInfoError::KindMismatch {
expected: ReflectKind::$kind,
received: self.kind(),
}),
}
}
};
}
/// Conversion convenience methods for [`TypeInfo`].
impl TypeInfo {
impl_cast_method!(as_struct: Struct => StructInfo);
impl_cast_method!(as_tuple_struct: TupleStruct => TupleStructInfo);
impl_cast_method!(as_tuple: Tuple => TupleInfo);
impl_cast_method!(as_list: List => ListInfo);
impl_cast_method!(as_array: Array => ArrayInfo);
impl_cast_method!(as_map: Map => MapInfo);
impl_cast_method!(as_enum: Enum => EnumInfo);
impl_cast_method!(as_value: Value => ValueInfo);
}
/// A container for compile-time info related to general value types, including primitives.
@ -241,3 +299,20 @@ impl ValueInfo {
self.docs
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_return_error_on_invalid_cast() {
let info = <Vec<i32> as Typed>::type_info();
assert!(matches!(
info.as_struct(),
Err(TypeInfoError::KindMismatch {
expected: ReflectKind::Struct,
received: ReflectKind::List
})
));
}
}