Qualify types behind abstraction

Hi here !

I'm looking for a solution for an abstraction mechanism that uses Boxes.

This is the situation: I have a struct (System) which own a vector of other structs which are known by their trait (Component). I quickly find myself having to identify, in concrete terms, the types produced by these components. So, I use an enum which contain produced Components types.

There is a working code of that:

////// System which contains abstracted components
struct System {
    components: Vec<Box<dyn Component>>,
}

impl System {
    fn work(&self) -> Vec<Item>{
        self.components.iter().map(|c| c.work()).collect()
    }
}

enum Item {
    Component(ComponentItem)
}

trait Component {
    fn work(&self) -> Item;
}

/////// A Component, which I desire permit been declared by other crates
struct MyComponent;

impl Component for MyComponent {
    fn work(&self) -> Item {
        Item::Component(ComponentItem::MyComponent(MyComponentItem::AKindOfWork))
    }
}

enum MyComponentItem {
    AKindOfWork,
}


/////// The glue is here
enum ComponentItem {
    MyComponent(MyComponentItem)
}

fn main() {
    let my_component = MyComponent;
    let system = System {components: vec![Box::new(my_component)] };
    let _worked = system.work();
}

To identify types produced by these components, the Item::Component variant is used. It contains the ComponentItem enum which contain a variant of each Component. As it, I can know the type of each system.work() items.

To make my code writing more easy, I use a macro like that:

/////// The glue is here
macro_rules! items {
    ( $( [$name:ident, $event:ident] ),* ) => {
        enum ComponentItem {
            $( $name($event) ),*
        }
    };
}

items!([MyComponent, MyComponentItem]);

That works fine, as long as my Component implementations are declared in the same crate ... So, the questions are : Does exist a way to declare this enum according to other crate Component implementations ? Seems a little bit magic ... Or, when faced with this kind of problem, how can be resolved ?

Edit: What I'm trying to reach is a system which look like Resource in bevy.

Thanks !

Enums are not extensible.

For arbitrary types you will need something similar to the Any trait or hack something using type_id().

You can also use Bevy's ECS without the rest of the engine.

I will take a look on Any (nevers used yet) and think about using an ECS. Thanks !

For what it's worth, I've found a way to access it. But need some encapsulation. The trick is to use generics and homemade From/Into traits. There is how it can work (workspace styled example):

crates/common/src/lib.rs

// Use generic `T` to delegate to owner the "glue" type
pub struct System<T> {
    pub components: Vec<Box<dyn Component<T>>>,
}

impl<T> System<T> {
    // Example of usage of these components and how can they interact with `T`
    pub fn work(&self) {
        let work: Vec<Item<T>> = self.components.iter().map(|c| c.work()).collect();
        for component in &self.components {
            for item in &work {
                component.react(item)
            }
        }
    }
}

// Use generic `T` to delegate to owner the "glue" type
pub enum Item<T> {
    Component(T),
}

pub trait Component<T> {
    fn work(&self) -> Item<T>;
    fn react(&self, item: &Item<T>);
}

// Declare a trait will be used to obtain the specific component type
pub trait FromComponentItem<T> {
    fn from_component_item(value: T) -> Self;
}

// Declare a trait will be used to obtain, from "other components", a type knew by the component type
pub trait AsOtherComponentItem<T> {
    fn as_other_component_item(&self) -> Option<T>;
}

crates/component-archives/src/lib.rs (a first component)

use common::{Component, FromComponentItem, AsOtherComponentItem, Item};

pub struct ArchivesComponent;

// This component indicate T must implement `FromComponentItem` and `AsOtherComponentItem` to permir conversions.
impl<T: FromComponentItem<ArchiverItem> + AsOtherComponentItem<SystemEvent>> Component<T>
    for ArchivesComponent
{
    fn work(&self) -> Item<T> {
        Item::Component(T::from_component_item(ArchiverItem::ReadyToArchive))
    }

    // Example of how use Generic `Item` by in reality, use specific known type `SystemEvent`
    fn react(&self, item: &Item<T>) {
        match item {
            Item::Component(item) => {
                if let Some(item) = item.as_other_component_item() {
                    match item {
                        SystemEvent::Users(UsersSystemEvent::Created(user_name)) => {
                            println!("Archive user: {}", user_name)
                        },
                        SystemEvent::Users(UsersSystemEvent::Deleted(user_name)) => {
                            println!("Archive deleted user: {}", user_name)
                        }
                    }
                }
            }
        }
    }
}

pub enum ArchiverItem {
    ReadyToArchive,
}

pub enum SystemEvent {
    Users(UsersSystemEvent),
}

pub enum UsersSystemEvent {
    Created(String),
    Deleted(String),
}

crates/component-users/src.lib (a second component)

use common::{Component, FromComponentItem, Item};

pub struct UsersComponent;

impl<T: FromComponentItem<UsersItem>> Component<T> for UsersComponent {
    fn work(&self) -> Item<T> {
        Item::Component(T::from_component_item(UsersItem::CreatedUser("Frnck".into())))
    }

    fn react(&self, _item: &Item<T>) {}
}

pub enum UsersItem {
    CreatedUser(String),
    DeletedUser(String),
}

crates/runner/src/main.rs (all the glue done here, in the system owner)

use archives::{ArchiverItem, ArchivesComponent};
use common::{Component, FromComponentItem, AsOtherComponentItem, System};
use users::{UsersComponent, UsersItem};

// Convenient macro to declare the type which tale place of `T` and `FromComponentItem` implementations
macro_rules! items {
    ( $( [$name:ident, $event:ident] ),* ) => {
        enum ComponentItem {
            $( $name($event) ),*
        }

        $(
            impl FromComponentItem<$event> for ComponentItem {
                fn from_component_item(value: $event) -> Self {
                    ComponentItem::$name(value)
                }
            }
        )*
    };
}

// Shortcut to declare the conversion between components types
macro_rules! convert {
    ($target_type:ty, $outer_variant:path, { $($inner_pattern:pat => $result:expr),* $(,)? }) => {
        impl AsOtherComponentItem<$target_type> for ComponentItem {
            fn as_other_component_item(&self) -> Option<$target_type> {
                #[allow(unreachable_patterns)]
                match self {
                    ComponentItem::ArchivesComponent(_) => None,
                    $outer_variant(inner) => match inner {
                        $(
                            $inner_pattern => Some($result),
                        )*
                        _ => None,
                    },
                    _ => None,
                }
            }
        }
    };
}

// Building of the `T` type and `FromComponentItem` usages
items!(
    [UsersComponents, UsersItem],
    [ArchivesComponent, ArchiverItem]
);

// Building the conversion between components types
convert!(
    archives::SystemEvent,
    ComponentItem::UsersComponent,
    {
        UsersItem::CreatedUser(user_name) => archives::SystemEvent::Users(
            archives::UsersSystemEvent::Created(user_name.clone())
        ),
        UsersItem::DeletedUser(user_name) => archives::SystemEvent::Users(
            archives::UsersSystemEvent::Deleted(user_name.clone())
        )
    }
);

fn main() {
    let users = UsersComponent;
    let archives = ArchivesComponent;
    let c: Vec<Box<dyn Component<ComponentItem>>> = vec![Box::new(users), Box::new(archives)];
    let system: System<ComponentItem> = System { components: c };
    system.work();
}

A little bit sophisticated/complicated, but permit to keep component independent.