# System config genericization # Genericizing the current design Basically if we look at `NodeConfig` as it is today: ```rust pub struct NodeConfig<T> { pub(crate) node: T, pub(crate) graph_info: GraphInfo, pub(crate) conditions: Vec<BoxedCondition>, } ``` We want to not allow/include `GraphInfo` for observer configs, and just want to support run conditions. Maybe we'll support observer ordering in the future but for now it is controversial, and its not hard to change it to support it in the future if we follow the design below. So, we have a `T` there, which means we know what kind of node we're handling. We can bound `T` with a new trait that lets us also specify the kind of metadata to store: ```rust pub trait NodeType { type Metadata; // <-- Used in NodeConfig type GroupMetadata; // <-- Used in NodeConfigs (plural) } pub struct NodeConfig<T: NodeType> { pub(crate) node: T, pub(crate) metadata: T::Metadata, pub(crate) conditions: Vec<BoxedCondition>, } ``` And to maintain compatibility, we implement `NodeType` for our two pre-existing node types: ```rust impl NodeType for ScheduleSystem { type Metadata = GraphInfo; type GroupMetadata = Chain; } impl NodeType for InternedSystemSet { type Metadata = GraphInfo; type GroupMetadata = Chain; } ``` So they still get to have `NodeConfig` hold their `GraphInfo` stuff. `NodeConfigs` (plural) is a simple change as well: ```rust pub enum NodeConfigs<T: NodeType> { // <-- add the bound here NodeConfig(NodeConfig<T>), Configs { configs: Vec<NodeConfigs<T>>, collective_conditions: Vec<BoxedCondition>, metadata: T::GroupMetadata, // <-- use the associated type here }, } ``` Now anytime we have functions that need to operate on the `GraphInfo`, like adding system sets, ordering, etc. we can just use a bound `T: NodeType<Metadata = GraphInfo>`. The `IntoSystemConfigs` and `IntoSystemSetConfigs` traits are basically the same, except they operate on different node types. Since we have a `NodeType` trait now, we can combine them into a single trait with a `T: NodeType` bound: ```rust pub trait IntoNodeConfigs<T: NodeType, Marker>: Sized { fn into_configs(self) -> NodeConfigs<T>; // For the helper functions like in_set, before, after, etc. // we just add a bound `T: NodeType<Metadata = GraphInfo>` to // the function. fn in_set(self, set: impl SystemSet) -> NodeConfigs<T> where T: NodeType<Metadata = NodeInfo> { // ... } // Except distributive_run_if and run_if, since conditions are always // around regardless of node type. So they can be left unchanged. fn distributive_run_if<M>(self, condition: impl Condition<M> + Clone) -> NodeConfigs<T> { // ... } fn run_if<M>(self, condition: impl Condition<M>) -> NodeConfigs<T> { // ... } // For chain and chain_ignore_deferred, we instead bound with // `T: NodeType<GroupMetadata = Chain>` fn chain(self) -> NodeConfigs<T> where T: NodeType<GroupMetadata = Chain> { // ... } fn chain_ignore_deferred(self) -> NodeConfigs<T> where T: NodeType<GroupMetadata = Chain> { // ... } } ``` Now for `add_systems` and `configure_sets`, they use the `IntoNodeConfigs` trait but specify their node type: ```rust impl App { pub fn add_systems<M>( &mut self, schedule: impl ScheduleLabel, systems: impl IntoNodeConfigs<ScheduleSystem, M>, ) -> &mut Self; pub fn configure_sets<M>( // <-- configure sets does get a marker generic now, NBD though &mut self, schedule: impl ScheduleLabel, sets: impl IntoNodeConfigs<InternedSystemSet, M>, ) -> &mut Self; } ``` # Supporting observer configs Now with all that done, adding observer configuration support is incredibly easy. First add a new node type: ```rust pub type ObserveSystem<E, B> = Box<dyn ObserverSystem<E, B>>; impl<E: 'static, B: Bundle> Nodetype for ObserveSystem<E, B> { type Metadata = (); // <------- No metadata for now, maybe we add some later. type GroupMetadata = (); // <--/ } ``` And also implement `IntoNodeConfigs` for observer systems. We can mostly copy the impl for `IntoNodeConfigs<ScheduleSystem>` and change a few things: ```rust impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Infallible, Marker)> for S where E: 'static, B: Bundle, S: IntoObserverSystem<E, B, Marker, ()>, { fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> { // ... } } impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Fallible, Marker)> for S where E: 'static, B: Bundle, S: IntoObserverSystem<E, B, Marker, Result>, { fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> { // ... } } ``` Then, change the function signature of `add_observer` to accept configs instead. Maybe we should also pluralize the function name? ```rust impl App { pub fn add_observers<E: Event, B: Bundle, M>( &mut self, observers: impl IntoNodeConfigs<ObserveSystem<E, B>, M>, ) -> &mut Self; } ``` ## :warning: This design leaves out something that needs to be handled however! Collective conditions can't be easily cloned and pushed alongside every observer system, because conditons aren't required to be `Clone`able! So I'll leave someone else to figure that out. It should be possible to just push them into a hash map and reference them in each observer container, probably.