diff --git a/macros/src/buffer.rs b/macros/src/buffer.rs index 188c4b0b..d10d8711 100644 --- a/macros/src/buffer.rs +++ b/macros/src/buffer.rs @@ -1,61 +1,128 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_quote, Field, Generics, Ident, ItemStruct, Type, TypePath}; +use syn::{ + parse_quote, Field, Generics, Ident, ImplGenerics, ItemStruct, Type, TypeGenerics, TypePath, + Visibility, WhereClause, +}; use crate::Result; +const JOINED_ATTR_TAG: &'static str = "joined"; +const KEY_ATTR_TAG: &'static str = "key"; + pub(crate) fn impl_joined_value(input_struct: &ItemStruct) -> Result { let struct_ident = &input_struct.ident; let (impl_generics, ty_generics, where_clause) = input_struct.generics.split_for_impl(); let StructConfig { buffer_struct_name: buffer_struct_ident, - } = StructConfig::from_data_struct(&input_struct); + } = StructConfig::from_data_struct(&input_struct, &JOINED_ATTR_TAG); let buffer_struct_vis = &input_struct.vis; - let (field_ident, _, field_config) = get_fields_map(&input_struct.fields)?; + let (field_ident, _, field_config) = + get_fields_map(&input_struct.fields, FieldSettings::for_joined())?; let buffer: Vec<&Type> = field_config.iter().map(|config| &config.buffer).collect(); let noncopy = field_config.iter().any(|config| config.noncopy); - let buffer_struct: ItemStruct = parse_quote! { - #[allow(non_camel_case_types, unused)] - #buffer_struct_vis struct #buffer_struct_ident #impl_generics #where_clause { - #( - #buffer_struct_vis #field_ident: #buffer, - )* - } - }; + let buffer_struct: ItemStruct = generate_buffer_struct( + &buffer_struct_ident, + buffer_struct_vis, + &impl_generics, + &where_clause, + &field_ident, + &buffer, + ); + + let impl_buffer_clone = impl_buffer_clone( + &buffer_struct_ident, + &impl_generics, + &ty_generics, + &where_clause, + &field_ident, + noncopy, + ); + + let impl_select_buffers = impl_select_buffers( + struct_ident, + &buffer_struct_ident, + buffer_struct_vis, + &impl_generics, + &ty_generics, + &where_clause, + &field_ident, + &buffer, + ); + + let impl_buffer_map_layout = + impl_buffer_map_layout(&buffer_struct, &field_ident, &field_config)?; + let impl_joined = impl_joined(&buffer_struct, &input_struct, &field_ident)?; - let impl_buffer_clone = if noncopy { - // Clone impl for structs with a buffer that is not copyable - quote! { - impl #impl_generics ::std::clone::Clone for #buffer_struct_ident #ty_generics #where_clause { - fn clone(&self) -> Self { - Self { - #( - #field_ident: self.#field_ident.clone(), - )* - } - } - } + let gen = quote! { + impl #impl_generics ::bevy_impulse::JoinedValue for #struct_ident #ty_generics #where_clause { + type Buffers = #buffer_struct_ident #ty_generics; } - } else { - // Clone and copy impl for structs with buffers that are all copyable - quote! { - impl #impl_generics ::std::clone::Clone for #buffer_struct_ident #ty_generics #where_clause { - fn clone(&self) -> Self { - *self - } - } - impl #impl_generics ::std::marker::Copy for #buffer_struct_ident #ty_generics #where_clause {} - } + #buffer_struct + + #impl_buffer_clone + + #impl_select_buffers + + #impl_buffer_map_layout + + #impl_joined }; - let impl_buffer_map_layout = impl_buffer_map_layout(&buffer_struct, &input_struct)?; - let impl_joined = impl_joined(&buffer_struct, &input_struct)?; + Ok(gen.into()) +} + +pub(crate) fn impl_buffer_key_map(input_struct: &ItemStruct) -> Result { + let struct_ident = &input_struct.ident; + let (impl_generics, ty_generics, where_clause) = input_struct.generics.split_for_impl(); + let StructConfig { + buffer_struct_name: buffer_struct_ident, + } = StructConfig::from_data_struct(&input_struct, &KEY_ATTR_TAG); + let buffer_struct_vis = &input_struct.vis; + + let (field_ident, field_type, field_config) = + get_fields_map(&input_struct.fields, FieldSettings::for_key())?; + let buffer: Vec<&Type> = field_config.iter().map(|config| &config.buffer).collect(); + let noncopy = field_config.iter().any(|config| config.noncopy); + + let buffer_struct: ItemStruct = generate_buffer_struct( + &buffer_struct_ident, + buffer_struct_vis, + &impl_generics, + &where_clause, + &field_ident, + &buffer, + ); + + let impl_buffer_clone = impl_buffer_clone( + &buffer_struct_ident, + &impl_generics, + &ty_generics, + &where_clause, + &field_ident, + noncopy, + ); + + let impl_select_buffers = impl_select_buffers( + struct_ident, + &buffer_struct_ident, + buffer_struct_vis, + &impl_generics, + &ty_generics, + &where_clause, + &field_ident, + &buffer, + ); + + let impl_buffer_map_layout = + impl_buffer_map_layout(&buffer_struct, &field_ident, &field_config)?; + let impl_accessed = impl_accessed(&buffer_struct, &input_struct, &field_ident, &field_type)?; let gen = quote! { - impl #impl_generics ::bevy_impulse::JoinedValue for #struct_ident #ty_generics #where_clause { + impl #impl_generics ::bevy_impulse::BufferKeyMap for #struct_ident #ty_generics #where_clause { type Buffers = #buffer_struct_ident #ty_generics; } @@ -63,23 +130,11 @@ pub(crate) fn impl_joined_value(input_struct: &ItemStruct) -> Result #buffer_struct_ident #ty_generics { - #buffer_struct_ident { - #( - #field_ident, - )* - } - } - } + #impl_select_buffers #impl_buffer_map_layout - #impl_joined + #impl_accessed }; Ok(gen.into()) @@ -112,7 +167,7 @@ struct StructConfig { } impl StructConfig { - fn from_data_struct(data_struct: &ItemStruct) -> Self { + fn from_data_struct(data_struct: &ItemStruct, attr_tag: &str) -> Self { let mut config = Self { buffer_struct_name: format_ident!("__bevy_impulse_{}_Buffers", data_struct.ident), }; @@ -120,7 +175,7 @@ impl StructConfig { let attr = data_struct .attrs .iter() - .find(|attr| attr.path().is_ident("joined")); + .find(|attr| attr.path().is_ident(attr_tag)); if let Some(attr) = attr { attr.parse_nested_meta(|meta| { @@ -137,23 +192,52 @@ impl StructConfig { } } +struct FieldSettings { + default_buffer: fn(&Type) -> Type, + attr_tag: &'static str, +} + +impl FieldSettings { + fn for_joined() -> Self { + Self { + default_buffer: Self::default_field_for_joined, + attr_tag: JOINED_ATTR_TAG, + } + } + + fn for_key() -> Self { + Self { + default_buffer: Self::default_field_for_key, + attr_tag: KEY_ATTR_TAG, + } + } + + fn default_field_for_joined(ty: &Type) -> Type { + parse_quote! { ::bevy_impulse::Buffer<#ty> } + } + + fn default_field_for_key(ty: &Type) -> Type { + parse_quote! { <#ty as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer } + } +} + struct FieldConfig { buffer: Type, noncopy: bool, } impl FieldConfig { - fn from_field(field: &Field) -> Self { + fn from_field(field: &Field, settings: &FieldSettings) -> Self { let ty = &field.ty; let mut config = Self { - buffer: parse_quote! { ::bevy_impulse::Buffer<#ty> }, + buffer: (settings.default_buffer)(ty), noncopy: false, }; for attr in field .attrs .iter() - .filter(|attr| attr.path().is_ident("joined")) + .filter(|attr| attr.path().is_ident(settings.attr_tag)) { attr.parse_nested_meta(|meta| { if meta.path.is_ident("buffer") { @@ -172,7 +256,10 @@ impl FieldConfig { } } -fn get_fields_map(fields: &syn::Fields) -> Result<(Vec<&Ident>, Vec<&Type>, Vec)> { +fn get_fields_map( + fields: &syn::Fields, + settings: FieldSettings, +) -> Result<(Vec<&Ident>, Vec<&Type>, Vec)> { match fields { syn::Fields::Named(data) => { let mut idents = Vec::new(); @@ -185,7 +272,7 @@ fn get_fields_map(fields: &syn::Fields) -> Result<(Vec<&Ident>, Vec<&Type>, Vec< .ok_or("expected named fields".to_string())?; idents.push(ident); types.push(&field.ty); - configs.push(FieldConfig::from_field(field)); + configs.push(FieldConfig::from_field(field, &settings)); } Ok((idents, types, configs)) } @@ -193,16 +280,98 @@ fn get_fields_map(fields: &syn::Fields) -> Result<(Vec<&Ident>, Vec<&Type>, Vec< } } +fn generate_buffer_struct( + buffer_struct_ident: &Ident, + buffer_struct_vis: &Visibility, + impl_generics: &ImplGenerics, + where_clause: &Option<&WhereClause>, + field_ident: &Vec<&Ident>, + buffer: &Vec<&Type>, +) -> ItemStruct { + parse_quote! { + #[allow(non_camel_case_types, unused)] + #buffer_struct_vis struct #buffer_struct_ident #impl_generics #where_clause { + #( + #buffer_struct_vis #field_ident: #buffer, + )* + } + } +} + +fn impl_select_buffers( + struct_ident: &Ident, + buffer_struct_ident: &Ident, + buffer_struct_vis: &Visibility, + impl_generics: &ImplGenerics, + ty_generics: &TypeGenerics, + where_clause: &Option<&WhereClause>, + field_ident: &Vec<&Ident>, + buffer: &Vec<&Type>, +) -> TokenStream { + quote! { + impl #impl_generics #struct_ident #ty_generics #where_clause { + #buffer_struct_vis fn select_buffers( + #( + #field_ident: #buffer, + )* + ) -> #buffer_struct_ident #ty_generics { + #buffer_struct_ident { + #( + #field_ident, + )* + } + } + } + } + .into() +} + +fn impl_buffer_clone( + buffer_struct_ident: &Ident, + impl_generics: &ImplGenerics, + ty_generics: &TypeGenerics, + where_clause: &Option<&WhereClause>, + field_ident: &Vec<&Ident>, + noncopy: bool, +) -> TokenStream { + if noncopy { + // Clone impl for structs with a buffer that is not copyable + quote! { + impl #impl_generics ::std::clone::Clone for #buffer_struct_ident #ty_generics #where_clause { + fn clone(&self) -> Self { + Self { + #( + #field_ident: self.#field_ident.clone(), + )* + } + } + } + } + } else { + // Clone and copy impl for structs with buffers that are all copyable + quote! { + impl #impl_generics ::std::clone::Clone for #buffer_struct_ident #ty_generics #where_clause { + fn clone(&self) -> Self { + *self + } + } + + impl #impl_generics ::std::marker::Copy for #buffer_struct_ident #ty_generics #where_clause {} + } + } +} + /// Params: /// buffer_struct: The struct to implement `BufferMapLayout`. /// item_struct: The struct which `buffer_struct` is derived from. +/// settings: [`FieldSettings`] to use when parsing the field attributes fn impl_buffer_map_layout( buffer_struct: &ItemStruct, - item_struct: &ItemStruct, + field_ident: &Vec<&Ident>, + field_config: &Vec, ) -> Result { let struct_ident = &buffer_struct.ident; let (impl_generics, ty_generics, where_clause) = buffer_struct.generics.split_for_impl(); - let (field_ident, _, field_config) = get_fields_map(&item_struct.fields)?; let buffer: Vec<&Type> = field_config.iter().map(|config| &config.buffer).collect(); let map_key: Vec = field_ident.iter().map(|v| v.to_string()).collect(); @@ -249,17 +418,17 @@ fn impl_buffer_map_layout( fn impl_joined( joined_struct: &ItemStruct, item_struct: &ItemStruct, + field_ident: &Vec<&Ident>, ) -> Result { let struct_ident = &joined_struct.ident; let item_struct_ident = &item_struct.ident; let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl(); - let (field_ident, _, _) = get_fields_map(&item_struct.fields)?; Ok(quote! { impl #impl_generics ::bevy_impulse::Joined for #struct_ident #ty_generics #where_clause { type Item = #item_struct_ident #ty_generics; - fn pull(&self, session: ::bevy_ecs::prelude::Entity, world: &mut ::bevy_ecs::prelude::World) -> Result { + fn pull(&self, session: ::bevy_impulse::re_exports::Entity, world: &mut ::bevy_impulse::re_exports::World) -> Result { #( let #field_ident = self.#field_ident.pull(session, world)?; )* @@ -271,3 +440,54 @@ fn impl_joined( } }.into()) } + +fn impl_accessed( + accessed_struct: &ItemStruct, + key_struct: &ItemStruct, + field_ident: &Vec<&Ident>, + field_type: &Vec<&Type>, +) -> Result { + let struct_ident = &accessed_struct.ident; + let key_struct_ident = &key_struct.ident; + let (impl_generics, ty_generics, where_clause) = key_struct.generics.split_for_impl(); + + Ok(quote! { + impl #impl_generics ::bevy_impulse::Accessed for #struct_ident #ty_generics #where_clause { + type Key = #key_struct_ident #ty_generics; + + fn add_accessor( + &self, + accessor: ::bevy_impulse::re_exports::Entity, + world: &mut ::bevy_impulse::re_exports::World, + ) -> ::bevy_impulse::OperationResult { + #( + ::bevy_impulse::Accessed::add_accessor(&self.#field_ident, accessor, world)?; + )* + Ok(()) + } + + fn create_key(&self, builder: &::bevy_impulse::BufferKeyBuilder) -> Self::Key { + Self::Key {#( + // TODO(@mxgrey): This currently does not have good support for the user + // substituting in a different key type than what the BufferKeyLifecycle expects. + // We could consider adding a .clone().into() to help support that use case, but + // this would be such a niche use case that I think we can ignore it for now. + #field_ident: <#field_type as ::bevy_impulse::BufferKeyLifecycle>::create_key(&self.#field_ident, builder), + )*} + } + + fn deep_clone_key(key: &Self::Key) -> Self::Key { + Self::Key {#( + #field_ident: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.#field_ident), + )*} + } + + fn is_key_in_use(key: &Self::Key) -> bool { + false + #( + || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.#field_ident) + )* + } + } + }.into()) +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index df58fdc6..d63b8913 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -16,7 +16,7 @@ */ mod buffer; -use buffer::impl_joined_value; +use buffer::{impl_buffer_key_map, impl_joined_value}; use proc_macro::TokenStream; use quote::quote; @@ -76,3 +76,15 @@ pub fn derive_joined_value(input: TokenStream) -> TokenStream { .into(), } } + +#[proc_macro_derive(BufferKeyMap, attributes(key))] +pub fn derive_buffer_key_map(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + match impl_buffer_key_map(&input) { + Ok(tokens) => tokens.into(), + Err(msg) => quote! { + compile_error!(#msg); + } + .into(), + } +} diff --git a/src/buffer.rs b/src/buffer.rs index 7418107b..77baf5f1 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -17,9 +17,9 @@ use bevy_ecs::{ change_detection::Mut, - prelude::{Commands, Entity, Query}, + prelude::{Commands, Entity, Query, World}, query::QueryEntityError, - system::SystemParam, + system::{SystemParam, SystemState}, }; use std::{ops::RangeBounds, sync::Arc}; @@ -38,7 +38,7 @@ pub use buffer_access_lifecycle::BufferKeyLifecycle; pub(crate) use buffer_access_lifecycle::*; mod buffer_key_builder; -pub(crate) use buffer_key_builder::*; +pub use buffer_key_builder::*; mod buffer_map; pub use buffer_map::*; @@ -402,6 +402,67 @@ where } } +/// This trait allows [`World`] to give you access to any buffer using a [`BufferKey`] +pub trait BufferWorldAccess { + /// Call this to get read-only access to a buffer from a [`World`]. + /// + /// Alternatively you can use [`BufferAccess`] as a regular bevy system parameter, + /// which does not need direct world access. + fn buffer_view(&self, key: &BufferKey) -> Result, BufferError> + where + T: 'static + Send + Sync; + + /// Call this to get mutable access to a buffer. + /// + /// Pass in a callback that will receive [`BufferMut`], allowing it to view + /// and modify the contents of the buffer. + fn buffer_mut( + &mut self, + key: &BufferKey, + f: impl FnOnce(BufferMut) -> U, + ) -> Result + where + T: 'static + Send + Sync; +} + +impl BufferWorldAccess for World { + fn buffer_view(&self, key: &BufferKey) -> Result, BufferError> + where + T: 'static + Send + Sync, + { + let buffer_ref = self + .get_entity(key.tag.buffer) + .ok_or(BufferError::BufferMissing)?; + let storage = buffer_ref + .get::>() + .ok_or(BufferError::BufferMissing)?; + let gate = buffer_ref + .get::() + .ok_or(BufferError::BufferMissing)?; + Ok(BufferView { + storage, + gate, + session: key.tag.session, + }) + } + + fn buffer_mut( + &mut self, + key: &BufferKey, + f: impl FnOnce(BufferMut) -> U, + ) -> Result + where + T: 'static + Send + Sync, + { + let mut state = SystemState::>::new(self); + let mut buffer_access_mut = state.get_mut(self); + let buffer_mut = buffer_access_mut + .get_mut(key) + .map_err(|_| BufferError::BufferMissing)?; + Ok(f(buffer_mut)) + } +} + /// Access to view a buffer that exists inside a workflow. pub struct BufferView<'a, T> where diff --git a/src/buffer/any_buffer.rs b/src/buffer/any_buffer.rs index feb04e0f..a7a4f2d2 100644 --- a/src/buffer/any_buffer.rs +++ b/src/buffer/any_buffer.rs @@ -517,7 +517,7 @@ pub trait AnyBufferWorldAccess { /// For technical reasons this requires direct [`World`] access, but you can /// do other read-only queries on the world while holding onto the /// [`AnyBufferView`]. - fn any_buffer_view<'a>(&self, key: &AnyBufferKey) -> Result, BufferError>; + fn any_buffer_view(&self, key: &AnyBufferKey) -> Result, BufferError>; /// Call this to get mutable access to any buffer. /// @@ -531,7 +531,7 @@ pub trait AnyBufferWorldAccess { } impl AnyBufferWorldAccess for World { - fn any_buffer_view<'a>(&self, key: &AnyBufferKey) -> Result, BufferError> { + fn any_buffer_view(&self, key: &AnyBufferKey) -> Result, BufferError> { key.interface.create_any_buffer_view(key, self) } diff --git a/src/buffer/buffer_map.rs b/src/buffer/buffer_map.rs index 5eef2cb3..173ec1af 100644 --- a/src/buffer/buffer_map.rs +++ b/src/buffer/buffer_map.rs @@ -29,7 +29,7 @@ use crate::{ Joined, Node, OperationError, OperationResult, OperationRoster, }; -pub use bevy_impulse_derive::JoinedValue; +pub use bevy_impulse_derive::{BufferKeyMap, JoinedValue}; /// Uniquely identify a buffer within a buffer map, either by name or by an /// index value. @@ -282,8 +282,66 @@ impl Buffered for T { } /// This trait can be implemented for structs that are created by joining together -/// values from a collection of buffers. Usually you do not need to implement this -/// yourself. Instead you can use `#[derive(JoinedValue)]`. +/// values from a collection of buffers. This allows [`join`][1] to produce arbitrary +/// structs. Structs with this trait can be produced by [`try_join`][2]. +/// +/// Each field in this struct needs to have the trait bounds `'static + Send + Sync`. +/// +/// This does not generally need to be implemented explicitly. Instead you should +/// use `#[derive(JoinedValue)]`: +/// +/// ``` +/// use bevy_impulse::prelude::*; +/// +/// #[derive(JoinedValue)] +/// struct SomeValues { +/// integer: i64, +/// string: String, +/// } +/// ``` +/// +/// The above example would allow you to join a value from an `i64` buffer with +/// a value from a `String` buffer. You can have as many fields in the struct +/// as you'd like. +/// +/// This macro will generate a struct of buffers to match the fields of the +/// struct that it's applied to. The name of that struct is anonymous by default +/// since you don't generally need to use it directly, but if you want to give +/// it a name you can use #[joined(buffers_struct_name = ...)]`: +/// +/// ``` +/// # use bevy_impulse::prelude::*; +/// +/// #[derive(JoinedValue)] +/// #[joined(buffers_struct_name = SomeBuffers)] +/// struct SomeValues { +/// integer: i64, +/// string: String, +/// } +/// ``` +/// +/// By default each field of the generated buffers struct will have a type of +/// [`Buffer`], but you can override this using `#[joined(buffer = ...)]` +/// to specify a special buffer type. For example if your `JoinedValue` struct +/// contains an [`AnyMessageBox`] then by default the macro will use `Buffer`, +/// but you probably really want it to have an [`AnyBuffer`]: +/// +/// ``` +/// # use bevy_impulse::prelude::*; +/// +/// #[derive(JoinedValue)] +/// struct SomeValues { +/// integer: i64, +/// string: String, +/// #[joined(buffer = AnyBuffer)] +/// any: AnyMessageBox, +/// } +/// ``` +/// +/// The above method also works for joining a `JsonMessage` field from a `JsonBuffer`. +/// +/// [1]: crate::Builder::join +/// [2]: crate::Builder::try_join pub trait JoinedValue: 'static + Send + Sync + Sized { /// This associated type must represent a buffer map layout that implements /// the [`Joined`] trait. The message type yielded by [`Joined`] for this @@ -300,7 +358,47 @@ pub trait JoinedValue: 'static + Send + Sync + Sized { } } -/// Trait to describe a set of buffer keys. +/// Trait to describe a set of buffer keys. This allows [listen][1] and [access][2] +/// to work for arbitrary structs of buffer keys. Structs with this trait can be +/// produced by [`try_listen`][3] and [`try_create_buffer_access`][4]. +/// +/// Each field in the struct must be some kind of buffer key. +/// +/// This does not generally need to be implemented explicitly. Instead you should +/// define a struct where all fields are buffer keys and then apply +/// `#[derive(BufferKeyMap)]` to it, e.g.: +/// +/// ``` +/// use bevy_impulse::prelude::*; +/// +/// #[derive(Clone, BufferKeyMap)] +/// struct SomeKeys { +/// integer: BufferKey, +/// string: BufferKey, +/// any: AnyBufferKey, +/// } +/// ``` +/// +/// The macro will generate a struct of buffers to match the keys. The name of +/// that struct is anonymous by default since you don't generally need to use it +/// directly, but if you want to give it a name you can use `#[key(buffers_struct_name = ...)]`: +/// +/// ``` +/// # use bevy_impulse::prelude::*; +/// +/// #[derive(Clone, BufferKeyMap)] +/// #[key(buffers_struct_name = SomeBuffers)] +/// struct SomeKeys { +/// integer: BufferKey, +/// string: BufferKey, +/// any: AnyBufferKey, +/// } +/// ``` +/// +/// [1]: crate::Builder::listen +/// [2]: crate::Builder::create_buffer_access +/// [3]: crate::Builder::try_listen +/// [4]: crate::Builder::try_create_buffer_access pub trait BufferKeyMap: 'static + Send + Sync + Sized + Clone { type Buffers: 'static + BufferMapLayout + Accessed + Send + Sync; @@ -433,7 +531,7 @@ impl BufferMapLa #[cfg(test)] mod tests { - use crate::{prelude::*, testing::*, Accessed, AddBufferToMap, BufferMap}; + use crate::{prelude::*, testing::*, AddBufferToMap, BufferMap}; #[derive(JoinedValue)] struct TestJoinedValue { @@ -624,7 +722,8 @@ mod tests { _b: u32, } - #[derive(Clone)] + #[derive(Clone, BufferKeyMap)] + #[key(buffers_struct_name = TestKeysBuffers)] struct TestKeys { integer: BufferKey, float: BufferKey, @@ -632,106 +731,159 @@ mod tests { generic: BufferKey, any: AnyBufferKey, } + #[test] + fn test_listen() { + let mut context = TestingContext::minimal_plugins(); - impl BufferKeyMap for TestKeys { - type Buffers = TestKeysBuffers; - } + let workflow = context.spawn_io_workflow(|scope, builder| { + let buffer_any = builder.create_buffer::(BufferSettings::default()); - #[derive(Clone)] - struct TestKeysBuffers { - integer: as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer, - float: as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer, - string: as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer, - generic: as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer, - any: ::TargetBuffer, - } + let buffers = TestKeys::select_buffers( + builder.create_buffer(BufferSettings::default()), + builder.create_buffer(BufferSettings::default()), + builder.create_buffer(BufferSettings::default()), + builder.create_buffer(BufferSettings::default()), + buffer_any.as_any_buffer(), + ); - impl BufferMapLayout for TestKeysBuffers { - fn try_from_buffer_map(buffers: &BufferMap) -> Result { - let mut compatibility = ::bevy_impulse::IncompatibleLayout::default(); - let integer = compatibility.require_buffer_by_literal::< as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer>("integer", buffers); - let float = compatibility.require_buffer_by_literal::< as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer>("float", buffers); - let string = compatibility.require_buffer_by_literal::< as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer>("string", buffers); - let generic = compatibility.require_buffer_by_literal::< as ::bevy_impulse::BufferKeyLifecycle>::TargetBuffer>("generic", buffers); - let any = compatibility.require_buffer_by_literal::<::TargetBuffer>("any", buffers); + scope.input.chain(builder).fork_unzip(( + |chain: Chain<_>| chain.connect(buffers.integer.input_slot()), + |chain: Chain<_>| chain.connect(buffers.float.input_slot()), + |chain: Chain<_>| chain.connect(buffers.string.input_slot()), + |chain: Chain<_>| chain.connect(buffers.generic.input_slot()), + |chain: Chain<_>| chain.connect(buffer_any.input_slot()), + )); - let Ok(integer) = integer else { - return Err(compatibility); - }; - let Ok(float) = float else { - return Err(compatibility); - }; - let Ok(string) = string else { - return Err(compatibility); - }; - let Ok(generic) = generic else { - return Err(compatibility); - }; - let Ok(any) = any else { - return Err(compatibility); - }; + builder + .listen(buffers) + .then(join_via_listen.into_blocking_callback()) + .dispose_on_none() + .connect(scope.terminate); + }); - Ok(Self { - integer, - float, - string, - generic, - any, - }) - } - } + let mut promise = context.command(|commands| { + commands + .request( + (5_i64, 3.14_f64, "hello".to_string(), "world", 42_i64), + workflow, + ) + .take_response() + }); - impl ::bevy_impulse::BufferMapStruct for TestKeysBuffers { - fn buffer_list(&self) -> smallvec::SmallVec<[AnyBuffer; 8]> { - smallvec::smallvec![ - ::bevy_impulse::AsAnyBuffer::as_any_buffer(&self.integer), - ::bevy_impulse::AsAnyBuffer::as_any_buffer(&self.float), - ::bevy_impulse::AsAnyBuffer::as_any_buffer(&self.string), - ::bevy_impulse::AsAnyBuffer::as_any_buffer(&self.generic), - ::bevy_impulse::AsAnyBuffer::as_any_buffer(&self.any), - ] - } + context.run_with_conditions(&mut promise, Duration::from_secs(2)); + let value: TestJoinedValue<&'static str> = promise.take().available().unwrap(); + assert_eq!(value.integer, 5); + assert_eq!(value.float, 3.14); + assert_eq!(value.string, "hello"); + assert_eq!(value.generic, "world"); + assert_eq!(*value.any.downcast::().unwrap(), 42); + assert!(context.no_unhandled_errors()); } - impl Accessed for TestKeysBuffers { - type Key = TestKeys; + #[test] + fn test_try_listen() { + let mut context = TestingContext::minimal_plugins(); - fn add_accessor(&self, accessor: Entity, world: &mut World) -> crate::OperationResult { - ::bevy_impulse::Accessed::add_accessor(&self.integer, accessor, world)?; - ::bevy_impulse::Accessed::add_accessor(&self.float, accessor, world)?; - ::bevy_impulse::Accessed::add_accessor(&self.string, accessor, world)?; - ::bevy_impulse::Accessed::add_accessor(&self.generic, accessor, world)?; - ::bevy_impulse::Accessed::add_accessor(&self.any, accessor, world)?; - Ok(()) - } + let workflow = context.spawn_io_workflow(|scope, builder| { + let buffer_i64 = builder.create_buffer::(BufferSettings::default()); + let buffer_f64 = builder.create_buffer::(BufferSettings::default()); + let buffer_string = builder.create_buffer::(BufferSettings::default()); + let buffer_generic = builder.create_buffer::<&'static str>(BufferSettings::default()); + let buffer_any = builder.create_buffer::(BufferSettings::default()); - fn create_key(&self, builder: &crate::BufferKeyBuilder) -> Self::Key { - TestKeys { - integer: ::bevy_impulse::BufferKeyLifecycle::create_key(&self.integer, builder), - float: ::bevy_impulse::BufferKeyLifecycle::create_key(&self.float, builder), - string: ::bevy_impulse::BufferKeyLifecycle::create_key(&self.string, builder), - generic: ::bevy_impulse::BufferKeyLifecycle::create_key(&self.generic, builder), - any: ::bevy_impulse::BufferKeyLifecycle::create_key(&self.any, builder), - } - } + scope.input.chain(builder).fork_unzip(( + |chain: Chain<_>| chain.connect(buffer_i64.input_slot()), + |chain: Chain<_>| chain.connect(buffer_f64.input_slot()), + |chain: Chain<_>| chain.connect(buffer_string.input_slot()), + |chain: Chain<_>| chain.connect(buffer_generic.input_slot()), + |chain: Chain<_>| chain.connect(buffer_any.input_slot()), + )); - fn deep_clone_key(key: &Self::Key) -> Self::Key { - TestKeys { - integer: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.integer), - float: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.float), - string: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.string), - generic: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.generic), - any: ::bevy_impulse::BufferKeyLifecycle::deep_clone(&key.any), - } - } + let mut buffer_map = BufferMap::new(); + buffer_map.insert_buffer("integer", buffer_i64); + buffer_map.insert_buffer("float", buffer_f64); + buffer_map.insert_buffer("string", buffer_string); + buffer_map.insert_buffer("generic", buffer_generic); + buffer_map.insert_buffer("any", buffer_any); + + builder + .try_listen(&buffer_map) + .unwrap() + .then(join_via_listen.into_blocking_callback()) + .dispose_on_none() + .connect(scope.terminate); + }); + + let mut promise = context.command(|commands| { + commands + .request( + (5_i64, 3.14_f64, "hello".to_string(), "world", 42_i64), + workflow, + ) + .take_response() + }); - fn is_key_in_use(key: &Self::Key) -> bool { - false - || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.integer) - || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.float) - || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.string) - || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.generic) - || ::bevy_impulse::BufferKeyLifecycle::is_in_use(&key.any) + context.run_with_conditions(&mut promise, Duration::from_secs(2)); + let value: TestJoinedValue<&'static str> = promise.take().available().unwrap(); + assert_eq!(value.integer, 5); + assert_eq!(value.float, 3.14); + assert_eq!(value.string, "hello"); + assert_eq!(value.generic, "world"); + assert_eq!(*value.any.downcast::().unwrap(), 42); + assert!(context.no_unhandled_errors()); + } + + /// This macro is a manual implementation of the join operation that uses + /// the buffer listening mechanism. There isn't any reason to reimplement + /// join here except so we can test that listening is working correctly for + /// BufferKeyMap. + fn join_via_listen( + In(keys): In>, + world: &mut World, + ) -> Option> { + if world.buffer_view(&keys.integer).ok()?.is_empty() { + return None; + } + if world.buffer_view(&keys.float).ok()?.is_empty() { + return None; } + if world.buffer_view(&keys.string).ok()?.is_empty() { + return None; + } + if world.buffer_view(&keys.generic).ok()?.is_empty() { + return None; + } + if world.any_buffer_view(&keys.any).ok()?.is_empty() { + return None; + } + + let integer = world + .buffer_mut(&keys.integer, |mut buffer| buffer.pull()) + .unwrap() + .unwrap(); + let float = world + .buffer_mut(&keys.float, |mut buffer| buffer.pull()) + .unwrap() + .unwrap(); + let string = world + .buffer_mut(&keys.string, |mut buffer| buffer.pull()) + .unwrap() + .unwrap(); + let generic = world + .buffer_mut(&keys.generic, |mut buffer| buffer.pull()) + .unwrap() + .unwrap(); + let any = world + .any_buffer_mut(&keys.any, |mut buffer| buffer.pull()) + .unwrap() + .unwrap(); + + Some(TestJoinedValue { + integer, + float, + string, + generic, + any, + }) } } diff --git a/src/buffer/json_buffer.rs b/src/buffer/json_buffer.rs index 08d2fb62..80d768c4 100644 --- a/src/buffer/json_buffer.rs +++ b/src/buffer/json_buffer.rs @@ -1581,4 +1581,15 @@ mod tests { assert_eq!(values[2], serde_json::Value::String("hello".to_string())); assert_eq!(values[3], serde_json::to_value(TestMessage::new()).unwrap()); } + + // We define this struct just to make sure the BufferKeyMap macro successfully + // compiles with JsonBufferKey. + #[derive(Clone, BufferKeyMap)] + #[allow(unused)] + struct TestJsonKeyMap { + integer: BufferKey, + string: BufferKey, + json: JsonBufferKey, + any: AnyBufferKey, + } } diff --git a/src/lib.rs b/src/lib.rs index 4c4fb168..47213e8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,8 @@ pub use async_execution::Sendish; pub mod buffer; pub use buffer::*; +pub mod re_exports; + pub mod builder; pub use builder::*; @@ -340,8 +342,8 @@ pub mod prelude { buffer::{ Accessible, AnyBuffer, AnyBufferKey, AnyBufferMut, AnyBufferWorldAccess, AnyMessageBox, AsAnyBuffer, Buffer, BufferAccess, BufferAccessMut, BufferKey, BufferKeyMap, BufferMap, - BufferMapLayout, BufferSettings, Bufferable, Buffered, IncompatibleLayout, - IterBufferable, Joinable, JoinedValue, RetentionPolicy, + BufferMapLayout, BufferSettings, BufferWorldAccess, Bufferable, Buffered, + IncompatibleLayout, IterBufferable, Joinable, JoinedValue, RetentionPolicy, }, builder::Builder, callback::{AsCallback, Callback, IntoAsyncCallback, IntoBlockingCallback}, diff --git a/src/re_exports.rs b/src/re_exports.rs new file mode 100644 index 00000000..84f22076 --- /dev/null +++ b/src/re_exports.rs @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +//! This module contains symbols that are being re-exported so they can be used +//! by bevy_impulse_derive. + +pub use bevy_ecs::prelude::{Entity, World};