-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Introduce ReflectConvert, a generic reflection mechanism for type conversions, intended for asset BSN.
#23742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3bf7545
Introduce `ReflectConvert`, a generic reflection mechanism for type
pcwalton 865fa70
Doc check police
pcwalton 5534917
Doc check police
pcwalton d66e05f
Add `register_into_type_conversion` and fix tests
pcwalton 5b89b14
Make sure `HandleTemplate` is registered
pcwalton d6659c0
Merge remote-tracking branch 'origin/main' into reflect-convert
pcwalton 3395263
Merge remote-tracking branch 'origin/main' into reflect-convert
pcwalton 3a5871e
Add a test for multiple conversions to the same type
pcwalton 5951964
Use a `Fn` instead of a `fn`
pcwalton 1e17659
Doc check police
pcwalton 008aefe
Address review comments
pcwalton 0cf1e95
Add TypeRegistration::get_or_insert_data_with and use it in the
cart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| //! The [`ReflectConvert`] type, which allows types to register conversions to | ||
| //! and from one another. | ||
|
|
||
| use alloc::boxed::Box; | ||
| use core::{any::TypeId, marker::PhantomData}; | ||
|
|
||
| use bevy_utils::TypeIdMap; | ||
|
|
||
| use crate::{Reflect, TypePath}; | ||
|
|
||
| /// Provides a mechanism for converting values of one type to another. | ||
| /// | ||
| /// This [`crate::type_registry::TypeData`] is associated with the type to be | ||
| /// converted *into*, not the type to be converted *from*. To convert a value, | ||
| /// use code like the following: | ||
| /// | ||
| /// ```rust | ||
| /// # use bevy_reflect::{convert::ReflectConvert, GetTypeRegistration, TypeRegistry}; | ||
| /// # use std::any::TypeId; | ||
| /// # | ||
| /// # let mut registry = TypeRegistry::default(); | ||
| /// # registry.add_registration(i32::get_type_registration()); | ||
| /// # registry.add_registration(String::get_type_registration()); | ||
| /// # registry.register_type_conversion(|x: i32| Ok(x.to_string())); | ||
| /// | ||
| /// let reflect_convert = registry | ||
| /// .get_type_data::<ReflectConvert>(TypeId::of::<String>()) | ||
| /// .unwrap(); | ||
| /// let converted: String = *reflect_convert | ||
| /// .try_convert_from(Box::new(12345i32)) | ||
| /// .unwrap() | ||
| /// .downcast::<String>() | ||
| /// .unwrap(); | ||
| /// ``` | ||
| #[derive(Default)] | ||
| pub struct ReflectConvert { | ||
| /// A mapping from the type to be converted *from* to its associated | ||
| /// [`Converter`]. | ||
| conversions: TypeIdMap<Box<dyn Converter>>, | ||
| } | ||
|
|
||
| /// An internal trait that wraps a conversion function in an untyped interface. | ||
| trait Converter: Send + Sync { | ||
| /// Converts the value to the appropriate type. | ||
| /// | ||
| /// This returns the converted value if the conversion succeeds or the | ||
| /// original value if the conversion fails. | ||
| fn convert(&self, input: Box<dyn Reflect>) -> Result<Box<dyn Reflect>, Box<dyn Reflect>>; | ||
|
|
||
| /// Returns a new boxed instance wrapping the same [`Converter`]. | ||
| fn clone_converter(&self) -> Box<dyn Converter>; | ||
| } | ||
|
|
||
| /// A wrapper that contains a conversion function and implements [`Converter`]. | ||
| struct TypedConverter<T, U, F> | ||
| where | ||
| T: Reflect + TypePath, | ||
| U: Reflect + TypePath, | ||
| F: Fn(T) -> Result<U, T> + Clone + Send + Sync + 'static, | ||
| { | ||
| function: F, | ||
| phantom: PhantomData<(T, U)>, | ||
| } | ||
|
|
||
| impl ReflectConvert { | ||
| /// Attempts to construct an instance of this type from the provided | ||
| /// `input`. | ||
| /// | ||
| /// If the conversion fails, either because no conversion has been | ||
| /// registered from the type of `input` or because the conversion function | ||
| /// itself returned `Err`, the `input` value is returned as an error. | ||
| pub fn try_convert_from( | ||
| &self, | ||
| input: Box<dyn Reflect>, | ||
| ) -> Result<Box<dyn Reflect>, Box<dyn Reflect>> { | ||
| let type_id = (*input.as_any()).type_id(); | ||
| match self.conversions.get(&type_id) { | ||
| Some(converter) => converter.convert(input), | ||
| None => Err(input), | ||
| } | ||
| } | ||
|
|
||
| /// Adds a conversion function from the type `T` to this type. | ||
| /// | ||
| /// If the conversion succeeds, the function should return the converted | ||
| /// value. If the conversion fails, the function should return the original | ||
| /// input value. | ||
| pub fn register_type_conversion<T, U, F>(&mut self, function: F) | ||
| where | ||
| T: Reflect + TypePath, | ||
| U: Reflect + TypePath, | ||
| F: Fn(T) -> Result<U, T> + Clone + Send + Sync + 'static, | ||
| { | ||
| self.conversions.insert( | ||
| TypeId::of::<T>(), | ||
| Box::new(TypedConverter { | ||
| function, | ||
| phantom: PhantomData, | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| impl Clone for ReflectConvert { | ||
| fn clone(&self) -> Self { | ||
| ReflectConvert { | ||
| conversions: self | ||
| .conversions | ||
| .iter() | ||
| .map(|(type_id, converter)| (*type_id, converter.clone_converter())) | ||
| .collect(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<T, U, F> Clone for TypedConverter<T, U, F> | ||
| where | ||
| T: Reflect + TypePath, | ||
| U: Reflect + TypePath, | ||
| F: Fn(T) -> Result<U, T> + Clone + Send + Sync + 'static, | ||
| { | ||
| fn clone(&self) -> Self { | ||
| TypedConverter { | ||
| function: self.function.clone(), | ||
| phantom: PhantomData, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<T, U, F> Converter for TypedConverter<T, U, F> | ||
| where | ||
| T: Reflect + TypePath, | ||
| U: Reflect + TypePath, | ||
| F: Fn(T) -> Result<U, T> + Clone + Send + Sync + 'static, | ||
| { | ||
| fn convert(&self, input: Box<dyn Reflect>) -> Result<Box<dyn Reflect>, Box<dyn Reflect>> { | ||
| let mut input = input.downcast::<T>()?; | ||
| match (self.function)(*input) { | ||
| Ok(value) => Ok(Box::new(value)), | ||
| Err(value) => { | ||
| *input = value; | ||
| Err(input) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn clone_converter(&self) -> Box<dyn Converter> { | ||
| Box::new(self.clone()) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use alloc::{ | ||
| borrow::ToOwned as _, | ||
| boxed::Box, | ||
| string::{String, ToString}, | ||
| }; | ||
| use core::any::TypeId; | ||
|
|
||
| use crate::{convert::ReflectConvert, type_registry::GetTypeRegistration, TypeRegistry}; | ||
|
|
||
| /// Tests that `i32` can be converted to `String` if the appropriate | ||
| /// conversion is registered. | ||
| #[test] | ||
| fn convert_from_i32_to_string() { | ||
| // Register the types and the conversion. | ||
| let mut registry = TypeRegistry::default(); | ||
| registry.add_registration(i32::get_type_registration()); | ||
| registry.add_registration(String::get_type_registration()); | ||
| registry.register_type_conversion(|x: i32| Ok(x.to_string())); | ||
|
|
||
| let reflect_convert = registry | ||
| .get_type_data::<ReflectConvert>(TypeId::of::<String>()) | ||
| .unwrap(); | ||
|
|
||
| // Test that a successful conversion works. | ||
| let converted = reflect_convert | ||
| .try_convert_from(Box::new(12345i32)) | ||
| .unwrap() | ||
| .downcast::<String>() | ||
| .unwrap(); | ||
| assert_eq!(&**converted, "12345"); | ||
| } | ||
|
|
||
| /// Tests that `String` can be fallibly converted to `i32` if the | ||
| /// appropriate conversion is registered. | ||
| /// | ||
| /// This also tests that the behavior of returning the original string on | ||
| /// error is correct. | ||
| #[test] | ||
| fn convert_from_string_to_i32() { | ||
| // Register the types and the conversion. | ||
| let mut registry = TypeRegistry::default(); | ||
| registry.add_registration(i32::get_type_registration()); | ||
| registry.add_registration(String::get_type_registration()); | ||
| registry.register_type_conversion(|x: String| match x.parse::<i32>() { | ||
| Ok(value) => Ok(value), | ||
| Err(_) => Err(x), | ||
| }); | ||
|
|
||
| let reflect_convert = registry | ||
| .get_type_data::<ReflectConvert>(TypeId::of::<i32>()) | ||
| .unwrap(); | ||
|
|
||
| // Test a successful conversion from string to integer. | ||
| let converted = reflect_convert | ||
| .try_convert_from(Box::new("12345".to_owned())) | ||
| .unwrap() | ||
| .downcast::<i32>() | ||
| .unwrap(); | ||
| assert_eq!(*converted, 12345); | ||
|
|
||
| // Test an unsuccessful conversion from string to integer. | ||
| let error = reflect_convert | ||
| .try_convert_from(Box::new("qqqqq".to_owned())) | ||
| .unwrap_err() | ||
| .downcast::<String>() | ||
| .unwrap(); | ||
| assert_eq!(&**error, "qqqqq"); | ||
| } | ||
|
|
||
| /// Tests that we can register multiple conversions into the same type and | ||
| /// that they all work. | ||
| #[test] | ||
| fn convert_from_f32_and_u32_to_i32() { | ||
| let mut registry = TypeRegistry::default(); | ||
| registry.add_registration(i32::get_type_registration()); | ||
| registry.add_registration(f32::get_type_registration()); | ||
| registry.add_registration(u32::get_type_registration()); | ||
| registry.register_type_conversion::<u32, i32, _>(|n: u32| n.try_into().map_err(|_| n)); | ||
| registry.register_type_conversion::<f32, i32, _>(|n: f32| Ok(n as i32)); | ||
|
|
||
| let reflect_convert = registry | ||
| .get_type_data::<ReflectConvert>(TypeId::of::<i32>()) | ||
| .unwrap(); | ||
|
|
||
| // Test that we can convert `u32` and `f32` into `i32`. | ||
| let a = reflect_convert | ||
| .try_convert_from(Box::new(99u32)) | ||
| .unwrap() | ||
| .downcast::<i32>() | ||
| .unwrap(); | ||
| assert_eq!(*a, 99i32); | ||
| let b = reflect_convert | ||
| .try_convert_from(Box::new(99.0f32)) | ||
| .unwrap() | ||
| .downcast::<i32>() | ||
| .unwrap(); | ||
| assert_eq!(*b, 99i32); | ||
| } | ||
|
|
||
| /// Tests that the error-handling behavior is correct when attempting a | ||
| /// conversion that hasn't been registered. | ||
| #[test] | ||
| fn no_such_conversion() { | ||
| let mut registry = TypeRegistry::default(); | ||
| registry.add_registration(i32::get_type_registration()); | ||
| registry.add_registration(String::get_type_registration()); | ||
| registry | ||
| .get_mut(TypeId::of::<i32>()) | ||
| .unwrap() | ||
| .insert(ReflectConvert::default()); | ||
|
|
||
| let reflect_convert = registry | ||
| .get_type_data::<ReflectConvert>(TypeId::of::<i32>()) | ||
| .unwrap(); | ||
|
|
||
| // Test that we get the original value back on error. | ||
| let error = reflect_convert | ||
| .try_convert_from(Box::new("12345".to_owned())) | ||
| .unwrap_err() | ||
| .downcast::<String>() | ||
| .unwrap(); | ||
| assert_eq!(&**error, "12345"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -645,6 +645,7 @@ mod impls { | |
| } | ||
|
|
||
| pub mod attributes; | ||
| pub mod convert; | ||
| pub mod enums; | ||
| mod generics; | ||
| pub mod serde; | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.