arcane-rs / arcane

1 stars 1 forks source link

Check that every combination of event::Name and event::Version correspond to single Rust type (#1) #3

Closed ilslv closed 3 years ago

ilslv commented 3 years ago

Part of #1, #2

Synopsis

As discussed we should check that every combination of event::Name and event::Version correspond to single Rust type.

Solution

As TypeId::of() is const fn yet (tracking issue), we use codegen location to uniquely identify Rust type.

Checklist

ilslv commented 3 years ago

FCM

Check that every combination of event::Name and event::Version correspond to single Rust type (#3, #1)

Additionally:
- extend support for event::Initial in derive macros
ilslv commented 3 years ago

please, add also some examples with Initial<_> in #[derive(Event)]. It seems that we haven't covered this situation, which may result to be quite tricky one.

Quite tricky one, indeed. As we discussed, we want to express, that some state can be Initialized pretty strictly.

// Compiles just fine
#[derive(Event)]
enum ChatEvent {
    Created(Init<ChatCreated>),
    MessagePosted(ChatMessagePosted),
}

// Failed to compile, as we forgot the `Init<...>` wrapper
#[derive(Event)]
enum ChatEvent {
    Created(ChatCreated),
    MessagePosted(ChatMessagePosted),
}

Proposal

  1. Add Initial trait, which will be the same as Versioned, but with special blanket impl.
pub trait Initial {
    fn name() -> Name;

    fn version() -> Version;
}

impl<Ev: Initial + ?Sized> Event for Init<Ev> {
    fn name(&self) -> Name {
        Ev::name()
    }

    fn version(&self) -> Version {
        Ev::version()
    }
}
  1. Change existing blanket impls
//         ⌄ wrong
impl<Ev: Event + ?Sized, S: Sourced<Ev>> Sourced<Ev> for Option<S> {
    fn apply(&mut self, event: &Ev) {
        if let Some(state) = self {
            state.apply(event);
        }
    }
}

If we want to store Init<Event> inside enum variant, this blanket impl needs small change:

//            ⌄ was `Event`, became `Versioned`
impl<Ev: Versioned + ?Sized, S: Sourced<Ev>> Sourced<Ev> for Option<S> {
    fn apply(&mut self, event: &Ev) {
        if let Some(state) = self {
            state.apply(event);
        }
    }
}

And add new blanket

impl<Ev: Initial + ?Sized, S: Initialized<Ev>> Sourced<Init<Ev>> for Option<S> {
    fn apply(&mut self, event: &Initial<Ev>) {
        *self = Some(S::init(&event.0));
    }
}
  1. Add Initial derive macro, that works similar to Versioned

Main idea of those changes is to separate Initial events from Versioned (maybe rename it to reflect that separation?).

ack @tyranron

tyranron commented 3 years ago

@ilslv Events are primary thing in event sourcing, while the state is secondary. It should be OK for the same Event to be initial for one state and not initial for another. Initiality is responsibility of event::Sourced/event::Initialized traits which express relation between an Event and the state. It's not the reposnibility of an Event itself.

I don't see why se should introduce an additional trait/macro to handle the case. It should be possible with current setup by either specializing over Initial wrapper (giving it some additonal type machinery) or introducing #[event(initial)] attribute for the variant which will consider that sitouation in code generation.

ilslv commented 3 years ago

Discussed: we don't try to derive Event on Initial, but rather write our own copy of Borrow trait (but sealed) with blanket impl for Self and use it in codegen