Skip to content

Commit 3bf7545

Browse files
committed
Introduce ReflectConvert, a generic reflection mechanism for type
conversions, intended for asset BSN. One of the important features of BSN is that supplying a value of type T where a value of type U is expected is allowed if a `From` conversion exists from T to U. This is what allows `HandleTemplate`s to be automatically created from string literals. We need this feature for BSN like this to be valid: ```rust SceneRoot("models/FlightHelmet/FlightHelmet.gltf#Scene0") ``` And for this to be valid: ```rust EnvironmentMapLight { diffuse_map: "environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2", specular_map: "environment_maps/pisa_specular_rgb9e5_zstd.ktx2", intensity: 250.0, } ``` Unfortunately, the `From` trait isn't reflectable, and this is a problem as reflection drives asset BSN. It's not immediately clear how to make `From` reflectable either, as a syntax like `#[reflect(From)]` wouldn't work as it's a generic trait. Instead of making `From` directly reflectable, this patch introduces `ReflectConvert`, a new `TypeData` that encodes a generic way to convert a value of type T to type U. You register a type conversion like this: ```rust App::new() .register_type::<i32>() .register_type::<String>() .register_type_conversion::<i32, String>(|n| Ok(n.into())); ``` And you invoke the conversion like this: ```rust let reflect_convert = registry .get_type_data::<ReflectConvert>(TypeId::of::<String>()) .unwrap(); let converted = reflect_convert .try_convert_from(Box::new(12345i32)) .unwrap() .downcast::<String>() .unwrap(); ``` I tested `ReflectConvert` with asset BSN (PR bevyengine#23576). The `ReflectConvert` trait as implemented in this patch successfully enables `HandleTemplate`s to be created from strings, without hard-coding `HandleTemplate` anywhere in the patch generation logic.
1 parent 9e7355d commit 3bf7545

7 files changed

Lines changed: 302 additions & 1 deletion

File tree

crates/bevy_app/src/app.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,32 @@ impl App {
694694
self
695695
}
696696

697+
/// Registers a fallible conversion from type T to U with the reflection
698+
/// system.
699+
///
700+
/// The supplied closure is expected to produce a value of type U, given an
701+
/// instance of type T. If the conversion fails, the closure should return
702+
/// the input value, wrapped in an `Err` variant.
703+
///
704+
/// # Example
705+
/// ```
706+
/// App::new()
707+
/// .register_type::<i32>()
708+
/// .register_type::<String>()
709+
/// .register_type_conversion::<i32, String>(|n| Ok(n.into()));
710+
/// ```
711+
///
712+
/// See [`bevy_reflect::TypeRegistry::register_type_conversion`].
713+
#[cfg(feature = "bevy_reflect")]
714+
pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>) -> &mut Self
715+
where
716+
T: Reflect + TypePath,
717+
U: Reflect + TypePath,
718+
{
719+
self.main_mut().register_type_conversion(function);
720+
self
721+
}
722+
697723
/// Registers the given function into the [`AppFunctionRegistry`] resource.
698724
///
699725
/// The given function will internally be stored as a [`DynamicFunction`]

crates/bevy_app/src/sub_app.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,18 @@ impl SubApp {
485485
self
486486
}
487487

488+
/// See [`App::register_type_conversion`].
489+
#[cfg(feature = "bevy_reflect")]
490+
pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>) -> &mut Self
491+
where
492+
T: bevy_reflect::Reflect + bevy_reflect::TypePath,
493+
U: bevy_reflect::Reflect + bevy_reflect::TypePath,
494+
{
495+
let registry = self.world.resource_mut::<AppTypeRegistry>();
496+
registry.write().register_type_conversion::<T, U>(function);
497+
self
498+
}
499+
488500
/// See [`App::register_function`].
489501
#[cfg(feature = "reflect_functions")]
490502
pub fn register_function<F, Marker>(&mut self, function: F) -> &mut Self

crates/bevy_asset/src/handle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ impl<T: Asset> FromTemplate for Handle<T> {
208208
type Template = HandleTemplate<T>;
209209
}
210210

211+
#[derive(Reflect)]
211212
pub enum HandleTemplate<T: Asset> {
212213
Path(AssetPath<'static>),
213214
Handle(Handle<T>),

crates/bevy_asset/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@ impl AssetApp for App {
684684
type_registry.register::<Handle<A>>();
685685
type_registry.register_type_data::<A, ReflectAsset>();
686686
type_registry.register_type_data::<Handle<A>, ReflectHandle>();
687+
type_registry.register_type_conversion::<String, HandleTemplate<A>>(|s| Ok(s.into()));
687688
}
688689

689690
self

crates/bevy_reflect/src/convert.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
//! The [`ReflectConvert`] type, which allows types to register conversions to
2+
//! and from one another.
3+
4+
use alloc::boxed::Box;
5+
use core::any::TypeId;
6+
7+
use bevy_utils::TypeIdMap;
8+
9+
use crate::{Reflect, TypePath};
10+
11+
/// Provides a mechanism for converting values of one type to another.
12+
///
13+
/// This [`crate::type_registry::TypeData`] is associated with the type to be
14+
/// converted *into*, not the type to be converted *from*. To convert a value,
15+
/// use code like the following:
16+
///
17+
/// ```rust
18+
/// # let mut registry = TypeRegistry::default();
19+
/// # registry.add_registration(i32::get_type_registration());
20+
/// # registry.add_registration(String::get_type_registration());
21+
/// # registry.register_type_conversion(|x: i32| Ok(x.to_string()));
22+
///
23+
/// let reflect_convert = registry
24+
/// .get_type_data::<ReflectConvert>(TypeId::of::<String>())
25+
/// .unwrap();
26+
/// let converted: String = reflect_convert
27+
/// .try_convert_from(Box::new(12345i32))
28+
/// .unwrap()
29+
/// .downcast::<String>()
30+
/// .unwrap();
31+
/// ```
32+
#[derive(Default)]
33+
pub struct ReflectConvert {
34+
/// A mapping from the type to be converted *from* to its associated
35+
/// [`Converter`].
36+
conversions: TypeIdMap<Box<dyn Converter>>,
37+
}
38+
39+
/// An internal trait that wraps a conversion function in an untyped interface.
40+
trait Converter: Send + Sync {
41+
/// Converts the value to the appropriate type.
42+
///
43+
/// This returns the converted value if the conversion succeeds or the
44+
/// original value if the conversion fails.
45+
fn convert(&self, input: Box<dyn Reflect>) -> Result<Box<dyn Reflect>, Box<dyn Reflect>>;
46+
47+
/// Returns a new boxed instance wrapping the same [`Converter`].
48+
fn clone_converter(&self) -> Box<dyn Converter>;
49+
}
50+
51+
/// A wrapper that contains a conversion function and implements [`Converter`].
52+
struct TypedConverter<T, U>(fn(T) -> Result<U, T>)
53+
where
54+
T: Reflect + TypePath,
55+
U: Reflect + TypePath;
56+
57+
impl ReflectConvert {
58+
/// Creates a new, empty [`ReflectConvert`] with no conversions registered.
59+
pub fn new() -> ReflectConvert {
60+
Self::default()
61+
}
62+
63+
/// Attempts to construct an instance of this type from the provided
64+
/// `input`.
65+
///
66+
/// If the conversion fails, either because no conversion has been
67+
/// registered from the type of `input` or because the conversion function
68+
/// itself returned `Err`, the `input` value is returned as an error.
69+
pub fn try_convert_from(
70+
&self,
71+
input: Box<dyn Reflect>,
72+
) -> Result<Box<dyn Reflect>, Box<dyn Reflect>> {
73+
let type_id = (*input.as_any()).type_id();
74+
match self.conversions.get(&type_id) {
75+
Some(converter) => converter.convert(input),
76+
None => Err(input),
77+
}
78+
}
79+
80+
/// Adds a conversion function from the type `T` to this type.
81+
///
82+
/// If the conversion succeeds, the function should return the converted
83+
/// value. If the conversion fails, the function should return the original
84+
/// input value.
85+
pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>)
86+
where
87+
T: Reflect + TypePath,
88+
U: Reflect + TypePath,
89+
{
90+
self.conversions
91+
.insert(TypeId::of::<T>(), Box::new(TypedConverter(function)));
92+
}
93+
}
94+
95+
impl Clone for ReflectConvert {
96+
fn clone(&self) -> Self {
97+
ReflectConvert {
98+
conversions: self
99+
.conversions
100+
.iter()
101+
.map(|(type_id, converter)| (*type_id, converter.clone_converter()))
102+
.collect(),
103+
}
104+
}
105+
}
106+
107+
impl<T, U> Clone for TypedConverter<T, U>
108+
where
109+
T: Reflect + TypePath,
110+
U: Reflect + TypePath,
111+
{
112+
fn clone(&self) -> Self {
113+
TypedConverter(self.0)
114+
}
115+
}
116+
117+
impl<T, U> Converter for TypedConverter<T, U>
118+
where
119+
T: Reflect + TypePath,
120+
U: Reflect + TypePath,
121+
{
122+
fn convert(&self, input: Box<dyn Reflect>) -> Result<Box<dyn Reflect>, Box<dyn Reflect>> {
123+
match (self.0)(input.take()?) {
124+
Ok(value) => Ok(Box::new(value)),
125+
Err(value) => Err(Box::new(value)),
126+
}
127+
}
128+
129+
fn clone_converter(&self) -> Box<dyn Converter> {
130+
Box::new(self.clone())
131+
}
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use alloc::{
137+
borrow::ToOwned as _,
138+
boxed::Box,
139+
string::{String, ToString},
140+
};
141+
use core::any::TypeId;
142+
143+
use crate::{convert::ReflectConvert, type_registry::GetTypeRegistration, TypeRegistry};
144+
145+
/// Tests that `i32` can be converted to `String` if the appropriate
146+
/// conversion is registered.
147+
#[test]
148+
fn convert_from_i32_to_string() {
149+
// Register the types and the conversion.
150+
let mut registry = TypeRegistry::default();
151+
registry.add_registration(i32::get_type_registration());
152+
registry.add_registration(String::get_type_registration());
153+
registry.register_type_conversion(|x: i32| Ok(x.to_string()));
154+
155+
let reflect_convert = registry
156+
.get_type_data::<ReflectConvert>(TypeId::of::<String>())
157+
.unwrap();
158+
159+
// Test that a successful conversion works.
160+
let converted = reflect_convert
161+
.try_convert_from(Box::new(12345i32))
162+
.unwrap()
163+
.downcast::<String>()
164+
.unwrap();
165+
assert_eq!(&**converted, "12345");
166+
}
167+
168+
/// Tests that `String` can be fallibly converted to `i32` if the
169+
/// appropriate conversion is registered.
170+
///
171+
/// This also tests that the behavior of returning the original string on
172+
/// error is correct.
173+
#[test]
174+
fn convert_from_string_to_i32() {
175+
// Register the types and the conversion.
176+
let mut registry = TypeRegistry::default();
177+
registry.add_registration(i32::get_type_registration());
178+
registry.add_registration(String::get_type_registration());
179+
registry.register_type_conversion(|x: String| match x.parse::<i32>() {
180+
Ok(value) => Ok(value),
181+
Err(_) => Err(x),
182+
});
183+
184+
let reflect_convert = registry
185+
.get_type_data::<ReflectConvert>(TypeId::of::<i32>())
186+
.unwrap();
187+
188+
// Test a successful conversion from string to integer.
189+
let converted = reflect_convert
190+
.try_convert_from(Box::new("12345".to_owned()))
191+
.unwrap()
192+
.downcast::<i32>()
193+
.unwrap();
194+
assert_eq!(*converted, 12345);
195+
196+
// Test an unsuccessful conversion from string to integer.
197+
let error = reflect_convert
198+
.try_convert_from(Box::new("qqqqq".to_owned()))
199+
.unwrap_err()
200+
.downcast::<String>()
201+
.unwrap();
202+
assert_eq!(&**error, "qqqqq");
203+
}
204+
205+
/// Tests that the error-handling behavior is correct when attempting a
206+
/// conversion that hasn't been registered.
207+
#[test]
208+
fn no_such_conversion() {
209+
let mut registry = TypeRegistry::default();
210+
registry.add_registration(i32::get_type_registration());
211+
registry.add_registration(String::get_type_registration());
212+
213+
let reflect_convert = registry
214+
.get_type_data::<ReflectConvert>(TypeId::of::<i32>())
215+
.unwrap();
216+
217+
// Test that we get the original value back on error.
218+
let error = reflect_convert
219+
.try_convert_from(Box::new("12345".to_owned()))
220+
.unwrap_err()
221+
.downcast::<String>()
222+
.unwrap();
223+
assert_eq!(&**error, "12345");
224+
}
225+
}

crates/bevy_reflect/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ mod impls {
645645
}
646646

647647
pub mod attributes;
648+
pub mod convert;
648649
pub mod enums;
649650
mod generics;
650651
pub mod serde;

crates/bevy_reflect/src/type_registry.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::{serde::Serializable, FromReflect, Reflect, TypeInfo, TypePath, Typed};
1+
use crate::{
2+
convert::ReflectConvert, serde::Serializable, FromReflect, Reflect, TypeInfo, TypePath, Typed,
3+
};
24
use alloc::{boxed::Box, string::String};
35
use bevy_platform::{
46
collections::{HashMap, HashSet},
@@ -353,6 +355,39 @@ impl TypeRegistry {
353355
data.insert(D::from_type());
354356
}
355357

358+
/// Registers a fallible conversion from type T to U with the reflection
359+
/// system.
360+
///
361+
/// The supplied closure is expected to produce a value of type U, given an
362+
/// instance of type T. If the conversion fails, the closure should return
363+
/// the input value, wrapped in an `Err` variant.
364+
///
365+
/// # Example
366+
/// ```
367+
/// let mut type_registry = TypeRegistry::default();
368+
/// type_registry.register::<i32>();
369+
/// type_registry.register::<String>();
370+
/// type_registry.register_type_conversion::<i32, String>(|n| Ok(n.into()));
371+
/// ```
372+
pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>)
373+
where
374+
T: Reflect + TypePath,
375+
U: Reflect + TypePath,
376+
{
377+
let data = self.get_mut(TypeId::of::<U>()).unwrap_or_else(|| {
378+
panic!(
379+
"attempted to call `TypeRegistry::register_type_conversion` for type `{U}` without registering `{U}` first",
380+
U = U::type_path(),
381+
)
382+
});
383+
if !data.contains::<ReflectConvert>() {
384+
data.insert(ReflectConvert::new());
385+
}
386+
data.data_mut::<ReflectConvert>()
387+
.unwrap()
388+
.register_type_conversion(function);
389+
}
390+
356391
/// Whether the type with given [`TypeId`] has been registered in this registry.
357392
pub fn contains(&self, type_id: TypeId) -> bool {
358393
self.registrations.contains_key(&type_id)

0 commit comments

Comments
 (0)