From 07b0edaf67fb64325119323b7aa6c01b0ba0e1b8 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Mon, 13 Jan 2025 04:07:06 +0000 Subject: [PATCH 1/5] experimental using derive for auto ops impl Signed-off-by: Teo Koon Peng --- macros/src/diagram_message.rs | 15 +++++ macros/src/lib.rs | 11 +++- src/diagram.rs | 2 + src/diagram/message.rs | 63 +++++++++++++++++++++ src/diagram/node_registry.rs | 101 +++++++++++++++++++++++++++++++++- src/diagram/testing.rs | 9 +++ 6 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 macros/src/diagram_message.rs create mode 100644 src/diagram/message.rs diff --git a/macros/src/diagram_message.rs b/macros/src/diagram_message.rs new file mode 100644 index 00000000..d2c0a69f --- /dev/null +++ b/macros/src/diagram_message.rs @@ -0,0 +1,15 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::DeriveInput; + +pub fn impl_diagram_message(ast: DeriveInput) -> TokenStream { + let name = ast.ident; + + let gen = quote! { + impl DiagramMessage for #name { + type DynUnzipImpl = NotSupported; + } + }; + + gen.into() +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d40c9309..a92510f0 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -15,9 +15,12 @@ * */ +mod diagram_message; +use diagram_message::impl_diagram_message; + use proc_macro::TokenStream; use quote::quote; -use syn::DeriveInput; +use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(Stream)] pub fn simple_stream_macro(item: TokenStream) -> TokenStream { @@ -58,3 +61,9 @@ pub fn delivery_label_macro(item: TokenStream) -> TokenStream { } .into() } + +#[proc_macro_derive(DiagramMessage)] +pub fn derive_diagram_message(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + impl_diagram_message(input) +} diff --git a/src/diagram.rs b/src/diagram.rs index 7efea31b..eb0b4c84 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -2,6 +2,7 @@ mod fork_clone; mod fork_result; mod impls; mod join; +mod message; mod node_registry; mod serialization; mod split_serialized; @@ -14,6 +15,7 @@ use fork_clone::ForkCloneOp; use fork_result::ForkResultOp; use join::JoinOp; pub use join::JoinOutput; +pub use message::DiagramMessage; pub use node_registry::*; pub use serialization::*; pub use split_serialized::*; diff --git a/src/diagram/message.rs b/src/diagram/message.rs new file mode 100644 index 00000000..b3c21877 --- /dev/null +++ b/src/diagram/message.rs @@ -0,0 +1,63 @@ +use bevy_utils::all_tuples_with_size; + +use super::{impls::DefaultImpl, impls::NotSupported, unzip::DynUnzip, SerializeMessage}; + +pub trait DiagramMessage +where + Self: Sized, +{ + type DynUnzipImpl: DynUnzip; +} + +impl DiagramMessage for () { + type DynUnzipImpl = NotSupported; +} + +macro_rules! dyn_unzip_impl { + ($len:literal, $(($P:ident, $o:ident)),*) => { + impl<$($P),*, Serializer> DiagramMessage for ($($P,)*) + where + $($P: Send + Sync + 'static + DiagramMessage),*, + Serializer: $(SerializeMessage<$P> +)* $(SerializeMessage> +)*, + { + type DynUnzipImpl = DefaultImpl; + } + }; +} + +all_tuples_with_size!(dyn_unzip_impl, 2, 12, R, o); + +#[cfg(test)] +mod tests { + use bevy_impulse_derive::DiagramMessage; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + + use crate::{diagram::testing::DiagramTestFixture, NodeBuilderOptions}; + + use super::*; + + #[derive(DiagramMessage, Clone, JsonSchema, Deserialize, Serialize)] + struct Foo {} + + #[test] + fn test_unzip() { + let mut fixture = DiagramTestFixture::new_empty(); + + fixture.registry.register_with_diagram_message( + NodeBuilderOptions::new("derive_foo_unzip"), + |builder, _config: ()| builder.create_map_block(|_: ()| (Foo {}, Foo {})), + ); + + assert_eq!( + fixture + .registry + .get_registration("derive_foo_unzip") + .unwrap() + .metadata + .response + .unzip_slots, + 2 + ); + } +} diff --git a/src/diagram/node_registry.rs b/src/diagram/node_registry.rs index 09a832d1..90f7b58f 100644 --- a/src/diagram/node_registry.rs +++ b/src/diagram/node_registry.rs @@ -25,9 +25,9 @@ use super::{ impls::{DefaultImpl, NotSupported}, register_deserialize, register_serialize, unzip::DynUnzip, - BuilderId, DefaultDeserializer, DefaultSerializer, DeserializeMessage, DiagramError, DynSplit, - DynSplitOutputs, DynType, OpaqueMessageDeserializer, OpaqueMessageSerializer, ResponseMetadata, - SplitOp, + BuilderId, DefaultDeserializer, DefaultSerializer, DeserializeMessage, DiagramError, + DiagramMessage, DynSplit, DynSplitOutputs, DynType, OpaqueMessageDeserializer, + OpaqueMessageSerializer, ResponseMetadata, SplitOp, }; /// A type erased [`crate::InputSlot`] @@ -335,6 +335,81 @@ impl<'a, DeserializeImpl, SerializeImpl, Cloneable> } } + /// Register a node builder with the specified common operations. + /// + /// # Arguments + /// + /// * `id` - Id of the builder, this must be unique. + /// * `name` - Friendly name for the builder, this is only used for display purposes. + /// * `f` - The node builder to register. + pub fn register_with_diagram_message( + self, + options: NodeBuilderOptions, + mut f: impl FnMut(&mut Builder, Config) -> Node + 'static, + ) -> RegistrationBuilder<'a, Request, Response, Streams> + where + Config: JsonSchema + DeserializeOwned, + Request: Send + Sync + 'static + DiagramMessage, + Response: Send + Sync + 'static + DiagramMessage, + Streams: StreamPack, + DeserializeImpl: DeserializeMessage, + SerializeImpl: SerializeMessage, + Cloneable: DynForkClone, + { + register_deserialize::(&mut self.registry.data); + register_serialize::(&mut self.registry.data); + + let mut response = ResponseMetadata::new( + SerializeImpl::json_schema(&mut self.registry.data.schema_generator) + .unwrap_or_else(|| self.registry.data.schema_generator.subschema_for::<()>()), + SerializeImpl::serializable(), + Cloneable::CLONEABLE, + ); + response.unzip_slots = ::UNZIP_SLOTS; + + let registration = NodeRegistration::new( + NodeMetadata { + id: options.id.clone(), + name: options.name.unwrap_or(options.id.clone()), + request: RequestMetadata { + schema: DeserializeImpl::json_schema(&mut self.registry.data.schema_generator) + .unwrap_or_else(|| { + self.registry.data.schema_generator.subschema_for::<()>() + }), + deserializable: DeserializeImpl::deserializable(), + }, + response, + config_schema: self + .registry + .data + .schema_generator + .subschema_for::(), + }, + RefCell::new(Box::new(move |builder, config| { + let config = serde_json::from_value(config)?; + let n = f(builder, config); + Ok(DynNode::new(n.output, n.input)) + })), + if Cloneable::CLONEABLE { + Some(Box::new(|builder, output, amount| { + Cloneable::dyn_fork_clone(builder, output, amount) + })) + } else { + None + }, + ); + self.registry.nodes.insert(options.id.clone(), registration); + + // SAFETY: We inserted an entry at this ID a moment ago + let node = self.registry.nodes.get_mut(&options.id).unwrap(); + + RegistrationBuilder:: { + node, + data: &mut self.registry.data, + _ignore: Default::default(), + } + } + /// Opt out of deserializing the request of the node. Use this to build a /// node whose request type is not deserializable. pub fn no_request_deserializing( @@ -551,6 +626,26 @@ impl NodeRegistry { self.opt_out().register_node_builder(options, builder) } + pub fn register_with_diagram_message( + &mut self, + options: NodeBuilderOptions, + builder: impl FnMut(&mut Builder, Config) -> Node + 'static, + ) -> RegistrationBuilder + where + Config: JsonSchema + DeserializeOwned, + Request: Send + + Sync + + 'static + + DynType + + DeserializeOwned + + DiagramMessage, + Response: + Send + Sync + 'static + DynType + Serialize + Clone + DiagramMessage, + { + self.opt_out() + .register_with_diagram_message(options, builder) + } + /// In some cases the common operations of deserialization, serialization, /// and cloning cannot be performed for the request or response of a node. /// When that happens you can still register your node builder by calling diff --git a/src/diagram/testing.rs b/src/diagram/testing.rs index fc0a553a..42884cfb 100644 --- a/src/diagram/testing.rs +++ b/src/diagram/testing.rs @@ -14,6 +14,7 @@ pub(super) struct DiagramTestFixture { } impl DiagramTestFixture { + /// Create a new text fixure with basic nodes registered. pub(super) fn new() -> Self { Self { context: TestingContext::minimal_plugins(), @@ -21,6 +22,14 @@ impl DiagramTestFixture { } } + /// Create a new text fixure with no nodes registered. + pub(super) fn new_empty() -> Self { + Self { + context: TestingContext::minimal_plugins(), + registry: NodeRegistry::new(), + } + } + pub(super) fn spawn_workflow( &mut self, diagram: &Diagram, From 6b935bdabe9efb9206670794b3f927f52bf7e4e6 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Mon, 13 Jan 2025 04:14:45 +0000 Subject: [PATCH 2/5] fixes Signed-off-by: Teo Koon Peng --- src/diagram/message.rs | 54 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/diagram/message.rs b/src/diagram/message.rs index b3c21877..be58f088 100644 --- a/src/diagram/message.rs +++ b/src/diagram/message.rs @@ -25,7 +25,7 @@ macro_rules! dyn_unzip_impl { }; } -all_tuples_with_size!(dyn_unzip_impl, 2, 12, R, o); +all_tuples_with_size!(dyn_unzip_impl, 1, 12, R, o); #[cfg(test)] mod tests { @@ -45,19 +45,67 @@ mod tests { let mut fixture = DiagramTestFixture::new_empty(); fixture.registry.register_with_diagram_message( - NodeBuilderOptions::new("derive_foo_unzip"), + NodeBuilderOptions::new("foo_unzippable"), + |builder, _config: ()| builder.create_map_block(|_: ()| Foo {}), + ); + + assert_eq!( + fixture + .registry + .get_registration("foo_unzippable") + .unwrap() + .metadata + .response + .unzip_slots, + 0 + ); + + fixture.registry.register_with_diagram_message( + NodeBuilderOptions::new("foo_1_tuple"), + |builder, _config: ()| builder.create_map_block(|_: ()| (Foo {},)), + ); + + assert_eq!( + fixture + .registry + .get_registration("foo_1_tuple") + .unwrap() + .metadata + .response + .unzip_slots, + 1 + ); + + fixture.registry.register_with_diagram_message( + NodeBuilderOptions::new("foo_unzip_2_tuple"), |builder, _config: ()| builder.create_map_block(|_: ()| (Foo {}, Foo {})), ); assert_eq!( fixture .registry - .get_registration("derive_foo_unzip") + .get_registration("foo_unzip_2_tuple") .unwrap() .metadata .response .unzip_slots, 2 ); + + fixture.registry.register_with_diagram_message( + NodeBuilderOptions::new("foo_unzip_3_tuple"), + |builder, _config: ()| builder.create_map_block(|_: ()| (Foo {}, Foo {}, Foo {})), + ); + + assert_eq!( + fixture + .registry + .get_registration("foo_unzip_3_tuple") + .unwrap() + .metadata + .response + .unzip_slots, + 3 + ); } } From eae8189a8b9e6f4a01308807cdfcf2df894927b0 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Mon, 13 Jan 2025 04:36:16 +0000 Subject: [PATCH 3/5] actually set the unzip impl Signed-off-by: Teo Koon Peng --- src/diagram/node_registry.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/diagram/node_registry.rs b/src/diagram/node_registry.rs index 90f7b58f..eeeb0461 100644 --- a/src/diagram/node_registry.rs +++ b/src/diagram/node_registry.rs @@ -402,6 +402,14 @@ impl<'a, DeserializeImpl, SerializeImpl, Cloneable> // SAFETY: We inserted an entry at this ID a moment ago let node = self.registry.nodes.get_mut(&options.id).unwrap(); + node.unzip_impl = if Response::DynUnzipImpl::UNZIP_SLOTS > 0 { + Response::DynUnzipImpl::on_register(&mut self.registry.data); + Some(Box::new(|builder, output| { + Response::DynUnzipImpl::dyn_unzip(builder, output) + })) + } else { + None + }; RegistrationBuilder:: { node, From f732c075818cef5811db4f186ec0a203055f1ef8 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Mon, 13 Jan 2025 04:40:55 +0000 Subject: [PATCH 4/5] docs Signed-off-by: Teo Koon Peng --- src/diagram/node_registry.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/diagram/node_registry.rs b/src/diagram/node_registry.rs index eeeb0461..6ff08393 100644 --- a/src/diagram/node_registry.rs +++ b/src/diagram/node_registry.rs @@ -335,13 +335,16 @@ impl<'a, DeserializeImpl, SerializeImpl, Cloneable> } } - /// Register a node builder with the specified common operations. + /// Register a node builder with the specified common operations, using [`DiagramMessage`] + /// to automatically opt in to the following operations: + /// + /// * unzip /// /// # Arguments /// /// * `id` - Id of the builder, this must be unique. /// * `name` - Friendly name for the builder, this is only used for display purposes. - /// * `f` - The node builder to register. + /// * `f` - The node builder to register. The request and response must impl [`DiagramMessage`]. pub fn register_with_diagram_message( self, options: NodeBuilderOptions, @@ -634,6 +637,16 @@ impl NodeRegistry { self.opt_out().register_node_builder(options, builder) } + /// Similar to `register_node_builder`, but uses [`DiagramMessage`] to automatically + /// opt in to the following operations: + /// + /// * unzip + /// + /// # Arguments + /// + /// * `id` - Id of the builder, this must be unique. + /// * `name` - Friendly name for the builder, this is only used for display purposes. + /// * `f` - The node builder to register. The request and response must impl [`DiagramMessage`]. pub fn register_with_diagram_message( &mut self, options: NodeBuilderOptions, From e424cefa5583f0284236ddef7a7e417fcc22c6d7 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Mon, 13 Jan 2025 04:49:29 +0000 Subject: [PATCH 5/5] naming Signed-off-by: Teo Koon Peng --- src/diagram/message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/diagram/message.rs b/src/diagram/message.rs index be58f088..d4172425 100644 --- a/src/diagram/message.rs +++ b/src/diagram/message.rs @@ -13,7 +13,7 @@ impl DiagramMessage for () { type DynUnzipImpl = NotSupported; } -macro_rules! dyn_unzip_impl { +macro_rules! diagram_message_impl { ($len:literal, $(($P:ident, $o:ident)),*) => { impl<$($P),*, Serializer> DiagramMessage for ($($P,)*) where @@ -25,7 +25,7 @@ macro_rules! dyn_unzip_impl { }; } -all_tuples_with_size!(dyn_unzip_impl, 1, 12, R, o); +all_tuples_with_size!(diagram_message_impl, 1, 12, R, o); #[cfg(test)] mod tests {