From 4c69321c6804ddfad2d195c8329aac6e072c5d73 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 21 Dec 2022 00:03:10 -0700 Subject: [PATCH 01/70] decider traits and in memory impl --- src/decider.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 73 insertions(+) create mode 100644 src/decider.rs diff --git a/src/decider.rs b/src/decider.rs new file mode 100644 index 0000000..7dde10e --- /dev/null +++ b/src/decider.rs @@ -0,0 +1,72 @@ +use std::{marker::PhantomData, collections::HashMap}; + +use async_trait::async_trait; + +trait Command { + type State: Default + Clone; +} +trait Event {} + +#[async_trait] +trait Decider { + fn decide(cmd: Cmd, state: S) -> Result, Err>; + fn evolve(state: S, event: E) -> S; + fn init() -> S; +} + +trait EventRepository { + fn load(&self) -> Result, Err>; + fn append(&mut self, events: Vec) -> Result, Err>; +} + +trait LockingEventRepository {} + +trait StateRepository { + fn reify(&self) -> ::State; + fn save(&mut self, state: ::State) -> Result<::State, Err>; +} + +trait LockingStateRepository {} + +#[derive(Default)] +struct InMemoryEventRepository { + events: Vec, + position: usize, + pd: PhantomData, +} + +impl EventRepository + for InMemoryEventRepository +{ + fn load(&self) -> Result, InMemoryEventRepositoryError> { + Ok(self.events.clone()) + } + + fn append(&mut self, events: Vec) -> Result, InMemoryEventRepositoryError> { + self.events.extend(events.clone()); + self.position += 1; + + Ok(events) + } +} + +enum InMemoryEventRepositoryError {} + +#[derive(Default)] +struct InMemoryStateRepository { + state: ::State, +} + +impl StateRepository for InMemoryStateRepository { + fn reify(&self) -> ::State { + self.state.clone() + } + + fn save(&mut self, state: ::State) -> Result<::State, InMemoryEventRepositoryError> { + self.state = state.clone(); + Ok(state) + } +} + + +enum InMemoryStateRepositoryError {} diff --git a/src/lib.rs b/src/lib.rs index 9ea9c6f..4ddc8ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use thiserror::Error; use uuid::Uuid; pub mod event_store; +pub mod decider; #[derive(Debug, Clone)] pub struct EventEnvelope From d01c291ee036615acca70a4d832a897461a23a37 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 21 Dec 2022 11:45:46 -0700 Subject: [PATCH 02/70] test stubs for in decider --- src/decider.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 7dde10e..4468dc4 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, collections::HashMap}; +use std::marker::PhantomData; use async_trait::async_trait; @@ -57,16 +57,119 @@ struct InMemoryStateRepository { state: ::State, } -impl StateRepository for InMemoryStateRepository { +impl StateRepository for InMemoryStateRepository { fn reify(&self) -> ::State { self.state.clone() } - fn save(&mut self, state: ::State) -> Result<::State, InMemoryEventRepositoryError> { + fn save( + &mut self, + state: ::State, + ) -> Result<::State, InMemoryEventRepositoryError> { self.state = state.clone(); Ok(state) } } - enum InMemoryStateRepositoryError {} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + trait ValueType { + fn value(&self) -> T; + } + + mod user { + use super::ValueType; + + #[derive(Clone)] + pub struct User { + pub id: UserId, + pub name: String, + } + + pub type UserId = usize; + pub struct UserName(String); + + impl TryFrom<&str> for UserName { + type Error = UserFieldError; + + fn try_from(value: &str) -> Result { + let len = value.len(); + if len < 1 { + Err(UserFieldError::EmptyName) + } else if len > 10 { + Err(UserFieldError::NameToLong(value.to_owned())) + } else { + Ok(Self(value.to_owned())) + } + } + } + + impl ValueType for UserName { + fn value(&self) -> String { + self.0.to_owned() + } + } + + pub enum UserFieldError { + EmptyName, + NameToLong(String), + } + } + + #[derive(Default, Clone)] + struct UserDeciderState { + users: HashMap, + } + + enum UserCommand { + AddUser(user::UserName), + UpdateUserName(user::UserId, user::UserName), + } + + impl Command for UserCommand { + type State = UserDeciderState; + } + + enum UserEvent { + UserAdded(user::User), + UserNameUpdated(user::UserId, user::UserName), + } + + impl Event for UserEvent {} + + struct UserDecider {} + + impl Decider for UserDecider { + fn decide( + cmd: UserCommand, + state: UserDeciderState, + ) -> Result, UserDeciderError> { + match cmd { + UserCommand::AddUser(_) => todo!(), + UserCommand::UpdateUserName(_, _) => todo!(), + } + } + + fn evolve(state: UserDeciderState, event: UserEvent) -> UserDeciderState { + match event { + UserEvent::UserAdded(_) => todo!(), + UserEvent::UserNameUpdated(_, _) => todo!(), + } + } + + fn init() -> UserDeciderState { + Default::default() + } + } + + enum UserDeciderError {} + + #[test] + fn test_in_memory() {} +} From c666be00cb41b0529b4c4a65ed20efc145cf8f49 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 21 Dec 2022 15:55:31 -0700 Subject: [PATCH 03/70] WIP: tests for in-memory decider example --- src/decider.rs | 93 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 4468dc4..269585b 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -3,9 +3,9 @@ use std::marker::PhantomData; use async_trait::async_trait; trait Command { - type State: Default + Clone; + type State: Clone; } -trait Event {} +pub trait Event {} #[async_trait] trait Decider { @@ -50,13 +50,37 @@ impl EventRepository InMemoryEventRepository { + fn new() -> Self { + let events: Vec = vec![]; + + Self { + events, + position: Default::default(), + pd: PhantomData::::default(), + } + } +} + +#[derive(Debug)] enum InMemoryEventRepositoryError {} -#[derive(Default)] struct InMemoryStateRepository { state: ::State, } +impl InMemoryStateRepository +where + C: Command, + ::State: Default, +{ + fn new() -> Self { + Self { + state: ::State::default(), + } + } +} + impl StateRepository for InMemoryStateRepository { fn reify(&self) -> ::State { self.state.clone() @@ -77,6 +101,10 @@ enum InMemoryStateRepositoryError {} mod tests { use std::collections::HashMap; + use thiserror::Error; + + use self::user::UserFieldError; + use super::*; trait ValueType { @@ -84,21 +112,27 @@ mod tests { } mod user { + use thiserror::Error; + use super::ValueType; #[derive(Clone)] pub struct User { pub id: UserId, - pub name: String, + pub name: UserName, } pub type UserId = usize; + + pub type UnvalidatedUserName = String; + + #[derive(Clone)] pub struct UserName(String); - impl TryFrom<&str> for UserName { + impl TryFrom for UserName { type Error = UserFieldError; - fn try_from(value: &str) -> Result { + fn try_from(value: String) -> Result { let len = value.len(); if len < 1 { Err(UserFieldError::EmptyName) @@ -116,19 +150,22 @@ mod tests { } } + #[derive(Debug, Error)] pub enum UserFieldError { + #[error("Username cannot be empty")] EmptyName, + #[error("Username {0} is to long")] NameToLong(String), } } - #[derive(Default, Clone)] + #[derive(Clone, Default)] struct UserDeciderState { users: HashMap, } enum UserCommand { - AddUser(user::UserName), + AddUser(user::UnvalidatedUserName), UpdateUserName(user::UserId, user::UserName), } @@ -136,6 +173,7 @@ mod tests { type State = UserDeciderState; } + #[derive(Clone)] enum UserEvent { UserAdded(user::User), UserNameUpdated(user::UserId, user::UserName), @@ -150,8 +188,16 @@ mod tests { cmd: UserCommand, state: UserDeciderState, ) -> Result, UserDeciderError> { - match cmd { - UserCommand::AddUser(_) => todo!(), + match cmd { + UserCommand::AddUser(user_name) => { + let name = user::UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserAdded(user::User { + id: 1, + name + })]) + } UserCommand::UpdateUserName(_, _) => todo!(), } } @@ -168,8 +214,31 @@ mod tests { } } - enum UserDeciderError {} + #[derive(Debug, Error)] + enum UserDeciderError { + #[error("Invalid user field {0:?}")] + UserField(UserFieldError), + } #[test] - fn test_in_memory() {} + fn test_in_memory() { + let event_repository: InMemoryEventRepository = + InMemoryEventRepository::new(); + let state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); + + let state = event_repository + .load() + .expect("Empty Events Vector") + .into_iter() + .fold(UserDecider::init(), UserDecider::evolve); + + let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); + let events = UserDecider::decide(cmd, state).expect("Decider Success"); + + if let Some(UserEvent::UserAdded(user::User{ name, .. })) = events.first() { + assert_eq!(name.value(), "Mike".to_string()) + } else { + panic!("Events not produced") + } + } } From 6b504722855955c8c64ee4090913cbb20a0ed0a8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 21 Dec 2022 16:38:43 -0700 Subject: [PATCH 04/70] WIP: more tests --- Cargo.toml | 3 +++ src/decider.rs | 32 +++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05e95ea..5273144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ thiserror = "1.0" # Use 0.8.2 only here because of a weird deps bug with eventstore # 1.x uuids are not compatible with eventstore internal uuids uuid = { version = "0.8.2", features = ["v4", "serde"] } + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/src/decider.rs b/src/decider.rs index 269585b..f77ded6 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -101,6 +101,7 @@ enum InMemoryStateRepositoryError {} mod tests { use std::collections::HashMap; + use assert_matches::assert_matches; use thiserror::Error; use self::user::UserFieldError; @@ -116,7 +117,7 @@ mod tests { use super::ValueType; - #[derive(Clone)] + #[derive(Debug, Clone)] pub struct User { pub id: UserId, pub name: UserName, @@ -126,7 +127,7 @@ mod tests { pub type UnvalidatedUserName = String; - #[derive(Clone)] + #[derive(Debug, Clone, PartialEq, Eq)] pub struct UserName(String); impl TryFrom for UserName { @@ -202,9 +203,12 @@ mod tests { } } - fn evolve(state: UserDeciderState, event: UserEvent) -> UserDeciderState { + fn evolve(mut state: UserDeciderState, event: UserEvent) -> UserDeciderState { match event { - UserEvent::UserAdded(_) => todo!(), + UserEvent::UserAdded(user) => { + state.users.insert(user.id.to_owned(), user.to_owned()); + state + }, UserEvent::UserNameUpdated(_, _) => todo!(), } } @@ -221,7 +225,7 @@ mod tests { } #[test] - fn test_in_memory() { + fn test_raw_decider() { let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); let state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); @@ -233,12 +237,22 @@ mod tests { .fold(UserDecider::init(), UserDecider::evolve); let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); - let events = UserDecider::decide(cmd, state).expect("Decider Success"); + let events = UserDecider::decide(cmd, state.clone()).expect("Decider Success"); - if let Some(UserEvent::UserAdded(user::User{ name, .. })) = events.first() { - assert_eq!(name.value(), "Mike".to_string()) + if let Some(UserEvent::UserAdded(user::User{ name, id })) = events.clone().first() { + let user_id = id.clone(); + let user_name = name.clone(); + + assert_eq!(name.value(), "Mike".to_string()); + + let state = events.into_iter().fold(state.clone(), UserDecider::evolve); + + assert_matches!(state.users.get(&id).expect("User exists"), user::User { + id, + name + } if (id == &user_id && name == &user_name)); } else { panic!("Events not produced") - } + } } } From 64cf2cfd48e86656117c82bc00c0c93fdc16ff31 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 22 Dec 2022 17:08:35 -0700 Subject: [PATCH 05/70] WIP: another test case before splitting modules --- src/decider.rs | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index f77ded6..32d39b6 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -23,7 +23,7 @@ trait LockingEventRepository {} trait StateRepository { fn reify(&self) -> ::State; - fn save(&mut self, state: ::State) -> Result<::State, Err>; + fn save(&mut self, state: &::State) -> Result<::State, Err>; } trait LockingStateRepository {} @@ -88,10 +88,10 @@ impl StateRepository for InMemorySt fn save( &mut self, - state: ::State, + state: &::State, ) -> Result<::State, InMemoryEventRepositoryError> { self.state = state.clone(); - Ok(state) + Ok(state.to_owned()) } } @@ -117,7 +117,7 @@ mod tests { use super::ValueType; - #[derive(Debug, Clone)] + #[derive(Debug, Clone, PartialEq, Eq)] pub struct User { pub id: UserId, pub name: UserName, @@ -160,14 +160,14 @@ mod tests { } } - #[derive(Clone, Default)] + #[derive(Debug, Clone, Default, PartialEq, Eq)] struct UserDeciderState { users: HashMap, } enum UserCommand { AddUser(user::UnvalidatedUserName), - UpdateUserName(user::UserId, user::UserName), + UpdateUserName(user::UserId, user::UnvalidatedUserName), } impl Command for UserCommand { @@ -194,12 +194,14 @@ mod tests { let name = user::UserName::try_from(user_name) .map_err(|e| UserDeciderError::UserField(e))?; - Ok(vec![UserEvent::UserAdded(user::User { - id: 1, - name - })]) + Ok(vec![UserEvent::UserAdded(user::User { id: 1, name })]) } - UserCommand::UpdateUserName(_, _) => todo!(), + UserCommand::UpdateUserName(user_id, user_name) => { + let name = user::UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserNameUpdated(user_id, name)]) + }, } } @@ -208,8 +210,11 @@ mod tests { UserEvent::UserAdded(user) => { state.users.insert(user.id.to_owned(), user.to_owned()); state - }, - UserEvent::UserNameUpdated(_, _) => todo!(), + } + UserEvent::UserNameUpdated(user_id, user_name) => { + state.users.get_mut(&user_id).unwrap().name = user_name.to_owned(); + state + } } } @@ -228,7 +233,7 @@ mod tests { fn test_raw_decider() { let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); - let state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); + let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); let state = event_repository .load() @@ -239,7 +244,7 @@ mod tests { let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); let events = UserDecider::decide(cmd, state.clone()).expect("Decider Success"); - if let Some(UserEvent::UserAdded(user::User{ name, id })) = events.clone().first() { + if let Some(UserEvent::UserAdded(user::User { name, id })) = events.clone().first() { let user_id = id.clone(); let user_name = name.clone(); @@ -247,12 +252,15 @@ mod tests { let state = events.into_iter().fold(state.clone(), UserDecider::evolve); + let _ = state_repository.save(&state); + assert_eq!(state_repository.reify(), state.clone()); + assert_matches!(state.users.get(&id).expect("User exists"), user::User { id, name } if (id == &user_id && name == &user_name)); } else { panic!("Events not produced") - } + } } } From dba14b8cbcfeb153cf1cc7319d6d329bc093e629 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 26 Dec 2022 16:54:49 -0700 Subject: [PATCH 06/70] ESDB event repository --- Cargo.toml | 6 + src/{decider.rs => decider/mod.rs} | 130 ++++++-------------- src/decider/repository/esdb/mod.rs | 177 ++++++++++++++++++++++++++++ src/decider/repository/in_memory.rs | 83 +++++++++++++ src/decider/repository/mod.rs | 76 ++++++++++++ 5 files changed, 375 insertions(+), 97 deletions(-) rename src/{decider.rs => decider/mod.rs} (67%) create mode 100644 src/decider/repository/esdb/mod.rs create mode 100644 src/decider/repository/in_memory.rs create mode 100644 src/decider/repository/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 5273144..ad4608e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["in_memory", "esdb"] +in_memory = [] +esdb = [] + [dependencies] async-trait = "0.1.53" chrono = "0.4.19" @@ -17,4 +22,5 @@ thiserror = "1.0" uuid = { version = "0.8.2", features = ["v4", "serde"] } [dev-dependencies] +actix-rt = "2.7.0" assert_matches = "1.5.0" diff --git a/src/decider.rs b/src/decider/mod.rs similarity index 67% rename from src/decider.rs rename to src/decider/mod.rs index 32d39b6..97d1d85 100644 --- a/src/decider.rs +++ b/src/decider/mod.rs @@ -1,102 +1,21 @@ -use std::marker::PhantomData; - use async_trait::async_trait; -trait Command { - type State: Clone; +pub mod repository; + +pub trait Command { + type State: Clone + Send + Sync; +} +pub trait Event { + fn event_type(&self) -> String; } -pub trait Event {} #[async_trait] -trait Decider { +pub trait Decider { fn decide(cmd: Cmd, state: S) -> Result, Err>; fn evolve(state: S, event: E) -> S; fn init() -> S; } -trait EventRepository { - fn load(&self) -> Result, Err>; - fn append(&mut self, events: Vec) -> Result, Err>; -} - -trait LockingEventRepository {} - -trait StateRepository { - fn reify(&self) -> ::State; - fn save(&mut self, state: &::State) -> Result<::State, Err>; -} - -trait LockingStateRepository {} - -#[derive(Default)] -struct InMemoryEventRepository { - events: Vec, - position: usize, - pd: PhantomData, -} - -impl EventRepository - for InMemoryEventRepository -{ - fn load(&self) -> Result, InMemoryEventRepositoryError> { - Ok(self.events.clone()) - } - - fn append(&mut self, events: Vec) -> Result, InMemoryEventRepositoryError> { - self.events.extend(events.clone()); - self.position += 1; - - Ok(events) - } -} - -impl InMemoryEventRepository { - fn new() -> Self { - let events: Vec = vec![]; - - Self { - events, - position: Default::default(), - pd: PhantomData::::default(), - } - } -} - -#[derive(Debug)] -enum InMemoryEventRepositoryError {} - -struct InMemoryStateRepository { - state: ::State, -} - -impl InMemoryStateRepository -where - C: Command, - ::State: Default, -{ - fn new() -> Self { - Self { - state: ::State::default(), - } - } -} - -impl StateRepository for InMemoryStateRepository { - fn reify(&self) -> ::State { - self.state.clone() - } - - fn save( - &mut self, - state: &::State, - ) -> Result<::State, InMemoryEventRepositoryError> { - self.state = state.clone(); - Ok(state.to_owned()) - } -} - -enum InMemoryStateRepositoryError {} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -104,9 +23,17 @@ mod tests { use assert_matches::assert_matches; use thiserror::Error; + use crate::decider::repository::StateRepository; + use self::user::UserFieldError; - use super::*; + use super::{ + repository::{ + in_memory::{InMemoryEventRepository, InMemoryStateRepository}, + EventRepository, + }, + *, + }; trait ValueType { fn value(&self) -> T; @@ -180,7 +107,14 @@ mod tests { UserNameUpdated(user::UserId, user::UserName), } - impl Event for UserEvent {} + impl Event for UserEvent { + fn event_type(&self) -> String { + match self { + UserEvent::UserAdded(_) => "UserAdded".to_string(), + UserEvent::UserNameUpdated(_, _) => "UserNameUpdated".to_string(), + } + } + } struct UserDecider {} @@ -201,7 +135,7 @@ mod tests { .map_err(|e| UserDeciderError::UserField(e))?; Ok(vec![UserEvent::UserNameUpdated(user_id, name)]) - }, + } } } @@ -229,14 +163,16 @@ mod tests { UserField(UserFieldError), } - #[test] - fn test_raw_decider() { + #[actix_rt::test] + async fn test_raw_decider() { let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); - let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); + let mut state_repository: InMemoryStateRepository = + InMemoryStateRepository::new(); let state = event_repository .load() + .await .expect("Empty Events Vector") .into_iter() .fold(UserDecider::init(), UserDecider::evolve); @@ -252,8 +188,8 @@ mod tests { let state = events.into_iter().fold(state.clone(), UserDecider::evolve); - let _ = state_repository.save(&state); - assert_eq!(state_repository.reify(), state.clone()); + let _ = state_repository.save(&state).await; + assert_eq!(state_repository.reify().await, state.clone()); assert_matches!(state.users.get(&id).expect("User exists"), user::User { id, diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs new file mode 100644 index 0000000..d1798da --- /dev/null +++ b/src/decider/repository/esdb/mod.rs @@ -0,0 +1,177 @@ +use async_trait::async_trait; +use eventstore::{ + AppendToStreamOptions, Client, EventData, ExpectedRevision, ReadStreamOptions, ResolvedEvent, + StreamPosition, +}; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; +use uuid::Uuid; + +use crate::decider::{Command, Event}; + +use super::LockingEventStoreWithStreams; + +struct ESDBEventRepository { + client: Client, + stream_name: String, +} + +impl<'a> ESDBEventRepository { + fn get_stream(&self, stream_id: Option) -> String { + if let Some(id) = stream_id { + format!("{}/{}", self.stream_name, id) + } else { + self.stream_name.clone() + } + } + + fn expected_revision_to_position(er: &ExpectedRevision) -> StreamPosition { + if let ExpectedRevision::Exact(u) = er { + StreamPosition::Position(u.to_owned()) + } else { + StreamPosition::Start + } + } +} + +#[async_trait] +impl<'a, C, E> LockingEventStoreWithStreams<'a, C, E, ESEBEventRepositoryError> + for ESDBEventRepository +where + C: Command, + E: Event + Sync + Send + Serialize + DeserializeOwned + Clone, +{ + type StreamId = String; + type Version = ExpectedRevision; + + async fn load( + &self, + id: Option, + ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> { + let mut stream = self + .client + .read_stream(self.get_stream(id), &ReadStreamOptions::default()) + .await + .map_err(ESEBEventRepositoryError::ESDBGeneral)?; + + let mut evts: Vec = vec![]; + + loop { + match stream.next().await { + Ok(Some(event)) => evts.push(event), + Ok(None) => break, + Err(eventstore::Error::ResourceNotFound) => { + return Ok((vec![], ExpectedRevision::NoStream)) + } + Err(e) => return Err(ESEBEventRepositoryError::ReadStream(e)), + } + } + + let mut rv = vec![]; + let mut pos = ExpectedRevision::StreamExists; + + for ev in evts { + let event_data = ev.get_original_event(); + let event = event_data + .as_json::() + .map_err(ESEBEventRepositoryError::DeserializeEvent)?; + + pos = ExpectedRevision::Exact(event_data.revision); + rv.push(event) + } + + Ok((rv, pos)) + } + + async fn load_from_version( + &self, + version: Self::Version, + id: Option, + ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> { + let options = + ReadStreamOptions::default().position(Self::expected_revision_to_position(&version)); + + let mut stream = self + .client + .read_stream(self.get_stream(id), &options) + .await + .map_err(ESEBEventRepositoryError::ESDBGeneral)?; + + let mut evts: Vec = vec![]; + + loop { + match stream.next().await { + Ok(Some(event)) => evts.push(event), + Ok(None) => break, + Err(eventstore::Error::ResourceNotFound) => { + return Ok((vec![], ExpectedRevision::NoStream)) + } + Err(e) => return Err(ESEBEventRepositoryError::ReadStream(e)), + } + } + + let mut rv = vec![]; + let mut pos = ExpectedRevision::StreamExists; + + for ev in evts { + let event_data = ev.get_original_event(); + let event = event_data + .as_json::() + .map_err(ESEBEventRepositoryError::DeserializeEvent)?; + + pos = ExpectedRevision::Exact(event_data.revision); + rv.push(event) + } + + Ok((rv, pos)) + } + + async fn append( + &mut self, + version: Self::Version, + stream: Self::StreamId, + events: Vec, + ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> + where + 'a: 'async_trait, + E: 'async_trait, + { + let mut perpared_events = vec![]; + + for e in &events { + let ed = EventData::json(e.event_type(), e.clone()) + .map(|ed| ed.id(Uuid::new_v4())) + .map_err(ESEBEventRepositoryError::SerializeEventDataPayload)?; + + perpared_events.push(ed); + } + + let res = self + .client + .append_to_stream( + self.get_stream(Some(stream.to_owned())), + &AppendToStreamOptions::default().expected_revision(version), + perpared_events, + ) + .await + .map_err(|e| ESEBEventRepositoryError::WriteStream(stream, e))?; + + Ok((events, ExpectedRevision::Exact(res.next_expected_version))) + } +} + +#[derive(Debug, Error)] +enum ESEBEventRepositoryError { + #[error("ESDB Error {0}")] + ESDBGeneral(eventstore::Error), + #[error("Error reading stream: {0}")] + ReadStream(eventstore::Error), + #[error("Could not deserialize event {0}")] + DeserializeEvent(serde_json::Error), + #[error("Could not parse event meta {0}")] + ParseMetadata(serde_json::Error), + #[error("Could not serialize event {0}")] + SerializeEventDataPayload(serde_json::Error), + #[error("Could not write to stream {0}: {1}")] + WriteStream(String, eventstore::Error), +} diff --git a/src/decider/repository/in_memory.rs b/src/decider/repository/in_memory.rs new file mode 100644 index 0000000..59323c0 --- /dev/null +++ b/src/decider/repository/in_memory.rs @@ -0,0 +1,83 @@ +use std::{marker::PhantomData, fmt::Debug}; + +use async_trait::async_trait; + +use crate::decider::{Command, Event}; + +use super::{EventRepository, StateRepository}; + +#[derive(Default)] +pub struct InMemoryEventRepository { + events: Vec, + position: usize, + pd: PhantomData, +} + +#[async_trait] +impl + EventRepository for InMemoryEventRepository +{ + async fn load(&self) -> Result, InMemoryEventRepositoryError> { + Ok(self.events.clone()) + } + + async fn append(&mut self, events: Vec) -> Result, InMemoryEventRepositoryError> { + self.events.extend(events.clone()); + self.position += 1; + + Ok(events) + } +} + +impl InMemoryEventRepository { + pub fn new() -> Self { + let events: Vec = vec![]; + + Self { + events, + position: Default::default(), + pd: PhantomData::::default(), + } + } +} + +#[derive(Debug)] +pub enum InMemoryEventRepositoryError {} + +pub struct InMemoryStateRepository { + state: ::State, +} + +impl InMemoryStateRepository +where + C: Command + Send + Sync, + ::State: Default + Send + Sync + Debug, +{ + pub fn new() -> Self { + Self { + state: ::State::default(), + } + } +} + +#[async_trait] +impl StateRepository + for InMemoryStateRepository + where + C: Command + Send + Sync, + ::State: Default + Send + Sync + Debug +{ + async fn reify(&self) -> ::State { + self.state.clone() + } + + async fn save( + &mut self, + state: &::State, + ) -> Result<::State, InMemoryEventRepositoryError> { + self.state = state.clone(); + Ok(self.state.to_owned()) + } +} + +pub enum InMemoryStateRepositoryError {} diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs new file mode 100644 index 0000000..2c6ce08 --- /dev/null +++ b/src/decider/repository/mod.rs @@ -0,0 +1,76 @@ +use async_trait::async_trait; + +use super::{Command, Event}; + +#[cfg(feature = "in_memory")] +pub mod in_memory; +#[cfg(feature = "esdb")] +pub mod esdb; + +// Event Repositories +#[async_trait] +pub trait EventRepository +where + C: Command, + E: Event + Sync + Send, +{ + async fn load(&self) -> Result, Err>; + async fn append(&mut self, events: Vec) -> Result, Err>; +} + +#[async_trait] +pub trait LockingEventRepository +where + C: Command, + E: Event + Sync + Send, +{ + type Version: Eq; + + async fn load(&self) -> Result<(Vec, Self::Version), Err>; + async fn append( + &mut self, + version: Self::Version, + events: Vec, + ) -> Result<(Vec, Self::Version), Err>; +} + +#[async_trait] +pub trait LockingEventStoreWithStreams<'a, C, E, Err> +where + C: Command, + E: Event + Sync + Send, +{ + type StreamId; + type Version: Eq; + + async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Err>; + async fn load_from_version(&self, version: Self::Version, id: Option) -> Result<(Vec, Self::Version), Err>; + async fn append( + &mut self, + version: Self::Version, + stream: Self::StreamId, + events: Vec, + ) -> Result<(Vec, Self::Version), Err> + where + 'a: 'async_trait, + E: 'async_trait; +} + +// State Repositories +#[async_trait] +pub trait StateRepository { + async fn reify(&self) -> ::State; + async fn save(&mut self, state: &::State) -> Result<::State, Err>; +} + +#[async_trait] +pub trait LockingStateRepository { + type Version: Eq; + + async fn reify(&self) -> Result<(::State, Self::Version), Err>; + async fn save( + &mut self, + version: Self::Version, + state: &::State, + ) -> Result<::State, Err>; +} From 1a71eda25a048cbeb934b99b74e75b7be37e572b Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 26 Dec 2022 17:17:10 -0700 Subject: [PATCH 07/70] out with old cold - only new decider --- epoch/Cargo.toml | 17 -- epoch/src/event_store.rs | 230 --------------------------- epoch/src/lib.rs | 153 ------------------ src/decider/repository/esdb/error.rs | 15 ++ src/decider/repository/esdb/mod.rs | 44 ++--- src/decider/repository/mod.rs | 22 ++- src/event_store.rs | 230 --------------------------- src/lib.rs | 155 +----------------- 8 files changed, 47 insertions(+), 819 deletions(-) delete mode 100644 epoch/Cargo.toml delete mode 100644 epoch/src/event_store.rs delete mode 100644 epoch/src/lib.rs create mode 100644 src/decider/repository/esdb/error.rs delete mode 100644 src/event_store.rs diff --git a/epoch/Cargo.toml b/epoch/Cargo.toml deleted file mode 100644 index 05e95ea..0000000 --- a/epoch/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "epoch" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-trait = "0.1.53" -chrono = "0.4.19" -eventstore = "2.1.1" -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.81" -thiserror = "1.0" -# Use 0.8.2 only here because of a weird deps bug with eventstore -# 1.x uuids are not compatible with eventstore internal uuids -uuid = { version = "0.8.2", features = ["v4", "serde"] } diff --git a/epoch/src/event_store.rs b/epoch/src/event_store.rs deleted file mode 100644 index c6e7765..0000000 --- a/epoch/src/event_store.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use chrono::{DateTime, Utc}; -use eventstore::{ - AppendToStreamOptions, EventData, ExpectedRevision, ReadStreamOptions, ResolvedEvent, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use async_trait::async_trait; -use uuid::Uuid; - -use crate::Event; - -use super::{ - CommmandInvariantError, EventContext, EventEnvelope, EventStore, FromCommandInvariant, - PreparedEvent, ToCommandInvariantError, -}; - -#[derive(Clone)] -pub struct ESDBEventStore { - is_test: bool, - client: eventstore::Client, -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("ESDB ERROR {0}")] - ESDBGeneral(eventstore::Error), - #[error("Error reading stream: {0}")] - ReadStream(eventstore::Error), - #[error("Could not deserialize event {0}")] - DeserializeEvent(serde_json::Error), - #[error("Could not parse event meta {0}")] - ParseMetadata(serde_json::Error), - #[error("Could not serialize event {0}")] - SerializeEventDataPayload(serde_json::Error), - #[error("Could not write to stream {0}: {1}")] - WriteStream(String, eventstore::Error), - #[error("Could not convert EventContext::Id to String {0}")] - ContextIdFromStr(String), - #[error("Command invariant bounds prevented a committed event {0}")] - CommandInvariant(String), -} - -impl FromCommandInvariant for Error { - fn from_command_invariant(cmd_err: Ctx::Err) -> Self - where - Ctx: EventContext, - ::Err: ToCommandInvariantError, - { - // Self::CommandInvariant(cmd_err.to_command_invariant_error()) - Self::CommandInvariant(cmd_err.to_string()) - } -} - -impl ESDBEventStore { - pub fn new(client: eventstore::Client) -> ESDBEventStore { - Self { - client, - is_test: false, - } - } - - // #[cfg(test)] - pub fn new_for_test(client: eventstore::Client) -> ESDBEventStore { - Self { - client, - is_test: true, - } - } - - pub fn stream_id(&self, id: Option) -> String - where - ::Id: Display, - { - let prefix = if self.is_test { "test_" } else { "" }; - - let ctx = Ctx::event_context(); - - match id { - Some(id_str) => format!("{}/{}", ctx, id_str), - None => format!("{}{}", prefix, ctx), - } - } - - fn resolved_to_event_envelope( - resolved_event: &ResolvedEvent, - ) -> Result<(EventEnvelope, ExpectedRevision), Error> - where - Ctx: EventContext, - ::Id: FromStr, - { - let event_data = resolved_event.get_original_event(); - let event = event_data - .as_json::<::Event>() - .map_err(Error::DeserializeEvent)?; - - let EventMetadata { - time, - event_context, - event_context_id, - }: EventMetadata = - serde_json::from_slice(&event_data.custom_metadata).map_err(Error::ParseMetadata)?; - - let event_context_id = match event_context_id { - Some(str) => { - Some(Ctx::Id::from_str(&str.clone()).map_err(|_| Error::ContextIdFromStr(str))?) - } - None => None, - }; - - Ok(( - EventEnvelope { - id: event_data.id, - time, - event_context, - event_context_id, - data: event, - }, - ExpectedRevision::Exact(event_data.revision), - )) - } -} - -#[async_trait] -impl EventStore for ESDBEventStore { - type Error = Error; - type Position = ExpectedRevision; - - async fn load( - &self, - id: Option, - ) -> Result<(Vec>, Self::Position), Self::Error> - where - ::Id: Display + FromStr, - { - let mut stream = self - .client - .read_stream(self.stream_id::(id), &ReadStreamOptions::default()) - .await - .map_err(Self::Error::ESDBGeneral)?; - - let mut evts: Vec = vec![]; - loop { - match stream.next().await { - Ok(Some(event)) => evts.push(event), - Ok(None) => break, - Err(eventstore::Error::ResourceNotFound) => { - return Ok((vec![], ExpectedRevision::NoStream)) - } - Err(e) => return Err(Self::Error::ReadStream(e)), - } - } - - let mut rv = vec![]; - let mut pos = ExpectedRevision::StreamExists; - - for ev in evts { - let (ee, revision) = ESDBEventStore::resolved_to_event_envelope(&ev)?; - - rv.push(ee); - pos = revision; - } - - Ok((rv, pos)) - } - - async fn append( - &self, - position: Self::Position, - event: PreparedEvent, - id: Option<::Id>, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Id: Clone + Display + ToString, - ::Event: Serialize + Event, - { - let event_meta = EventMetadata { - time: Utc::now(), - event_context: event.event_context, - event_context_id: id.clone().map(|x| x.to_string()), - }; - - let event_id = Uuid::new_v4(); - let event_data = EventData::json(event.data.event_type(), &event.data.clone()) - .map_err(Error::SerializeEventDataPayload)? - .id(event_id.clone()) - .metadata_as_json(event_meta.clone()) - .map_err(Error::SerializeEventDataPayload)?; - - let options = AppendToStreamOptions::default().expected_revision(position); - - let stream_id = self.stream_id::(id.clone()); - - let res = self - .client - .append_to_stream(stream_id.clone(), &options, event_data.clone()) - .await - .map_err(|e| Error::WriteStream(stream_id, e))?; - - let EventMetadata { - time, - event_context, - .. - } = event_meta; - - Ok(( - EventEnvelope { - id: event_id, - time, - event_context, - event_context_id: id, - data: event.data, - }, - ExpectedRevision::Exact(res.next_expected_version), - )) - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct EventMetadata { - time: DateTime, - event_context: String, - event_context_id: Option, -} diff --git a/epoch/src/lib.rs b/epoch/src/lib.rs deleted file mode 100644 index 9ea9c6f..0000000 --- a/epoch/src/lib.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; -use uuid::Uuid; - -pub mod event_store; - -#[derive(Debug, Clone)] -pub struct EventEnvelope -where - Ctx: EventContext + ?Sized, - ::Id: FromStr + Clone, - ::Event: Event + Clone, -{ - pub id: Uuid, - pub event_context: String, - pub event_context_id: Option, - pub time: DateTime, - pub data: Ctx::Event, -} - -#[derive(Debug, Clone)] -pub struct PreparedEvent -where - Ctx: EventContext + ?Sized, -{ - pub event_context: String, - pub event_context_id: Option, - pub data: Ctx::Event, -} - -pub trait Event { - fn event_type(&self) -> String; -} - -pub trait FromCommandInvariant { - fn from_command_invariant(cmd_err: Ctx::Err) -> Self - where - Ctx: EventContext, - ::Err: ToCommandInvariantError; -} - -#[derive(Error, Debug)] -pub enum CommmandInvariantError { - #[error("Command Invariant: {0}")] - CommandInvariant(String), - // #[error("User Command Invariant Command {0}")] - // Users(UserError) -} - -pub trait ToCommandInvariantError: Display { - fn to_command_invariant_error(&self) -> CommmandInvariantError; -} - -#[async_trait] -pub trait EventContext { - type Id: ToString + FromStr + Send + Sync + Eq + PartialEq + Clone; - type Command; - type Event: Send + Sync + DeserializeOwned + Event + Clone; - type Err: Debug; - type Services; - type State: Default; - - fn event_context() -> String; - - fn to_prepared_event(id: Option, event: Self::Event) -> PreparedEvent { - PreparedEvent { - event_context: Self::event_context(), - event_context_id: id, - data: event, - } - } - - async fn handle( - state: Self::State, - cmd: Self::Command, - services: Self::Services, - ) -> Result, Self::Err>; - - fn apply(state: Self::State, event: &EventEnvelope) -> Self::State; -} - -#[async_trait] -pub trait EventStore { - type Error; - type Position: Eq + PartialEq + Clone + Send; - - async fn load( - &self, - id: Option, - ) -> Result<(Vec>, Self::Position), Self::Error> - where - ::Id: Display + FromStr; - - async fn append( - &self, - position: Self::Position, - event: PreparedEvent, - id: Option, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Id: Clone + Display, - ::Event: Serialize + Event; - - async fn execute( - &self, - cmd: Ctx::Command, - services: Ctx::Services, - id: Option<::Id>, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync + Debug, - ::Id: Display + Clone, - ::Event: Send + Clone + Serialize + Debug, - ::Services: Send, - ::Command: Send, - ::State: Send, - ::Err: Send + Sync + Clone + ToCommandInvariantError, - Self::Error: FromCommandInvariant, - { - let (value, position) = self.get_current_state::(id.clone()).await?; - let res = Ctx::handle(value, cmd, services) - .await - .map_err(Self::Error::from_command_invariant::)?; - - Ok(self.append::(position, res, id).await?) - } - - async fn get_current_state( - &self, - id: Option, - ) -> Result<(Ctx::State, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Event: Send + Clone + Debug, - ::Id: Display + Clone, - { - let (evts, position) = self.load::(id).await?; - - Ok(( - evts.iter() - .fold(Ctx::State::default(), |state, evt| Ctx::apply(state, evt)), - position, - )) - } -} diff --git a/src/decider/repository/esdb/error.rs b/src/decider/repository/esdb/error.rs new file mode 100644 index 0000000..c7fdb6d --- /dev/null +++ b/src/decider/repository/esdb/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error{ + #[error("ESDB Error {0}")] + ESDBGeneral(eventstore::Error), + #[error("Error reading stream: {0}")] + ReadStream(eventstore::Error), + #[error("Could not deserialize event {0}")] + DeserializeEvent(serde_json::Error), + #[error("Could not serialize event {0}")] + SerializeEventDataPayload(serde_json::Error), + #[error("Could not write to stream {0}: {1}")] + WriteStream(String, eventstore::Error), +} \ No newline at end of file diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index d1798da..4d08462 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -4,13 +4,16 @@ use eventstore::{ StreamPosition, }; use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; use uuid::Uuid; use crate::decider::{Command, Event}; +use self::error::Error; + use super::LockingEventStoreWithStreams; +pub mod error; + struct ESDBEventRepository { client: Client, stream_name: String, @@ -35,7 +38,7 @@ impl<'a> ESDBEventRepository { } #[async_trait] -impl<'a, C, E> LockingEventStoreWithStreams<'a, C, E, ESEBEventRepositoryError> +impl<'a, C, E> LockingEventStoreWithStreams<'a, C, E, Error> for ESDBEventRepository where C: Command, @@ -47,12 +50,12 @@ where async fn load( &self, id: Option, - ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> { + ) -> Result<(Vec, Self::Version), Error> { let mut stream = self .client .read_stream(self.get_stream(id), &ReadStreamOptions::default()) .await - .map_err(ESEBEventRepositoryError::ESDBGeneral)?; + .map_err(Error::ESDBGeneral)?; let mut evts: Vec = vec![]; @@ -63,7 +66,7 @@ where Err(eventstore::Error::ResourceNotFound) => { return Ok((vec![], ExpectedRevision::NoStream)) } - Err(e) => return Err(ESEBEventRepositoryError::ReadStream(e)), + Err(e) => return Err(Error::ReadStream(e)), } } @@ -74,7 +77,7 @@ where let event_data = ev.get_original_event(); let event = event_data .as_json::() - .map_err(ESEBEventRepositoryError::DeserializeEvent)?; + .map_err(Error::DeserializeEvent)?; pos = ExpectedRevision::Exact(event_data.revision); rv.push(event) @@ -87,7 +90,7 @@ where &self, version: Self::Version, id: Option, - ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> { + ) -> Result<(Vec, Self::Version), Error> { let options = ReadStreamOptions::default().position(Self::expected_revision_to_position(&version)); @@ -95,7 +98,7 @@ where .client .read_stream(self.get_stream(id), &options) .await - .map_err(ESEBEventRepositoryError::ESDBGeneral)?; + .map_err(Error::ESDBGeneral)?; let mut evts: Vec = vec![]; @@ -106,7 +109,7 @@ where Err(eventstore::Error::ResourceNotFound) => { return Ok((vec![], ExpectedRevision::NoStream)) } - Err(e) => return Err(ESEBEventRepositoryError::ReadStream(e)), + Err(e) => return Err(Error::ReadStream(e)), } } @@ -117,7 +120,7 @@ where let event_data = ev.get_original_event(); let event = event_data .as_json::() - .map_err(ESEBEventRepositoryError::DeserializeEvent)?; + .map_err(Error::DeserializeEvent)?; pos = ExpectedRevision::Exact(event_data.revision); rv.push(event) @@ -131,7 +134,7 @@ where version: Self::Version, stream: Self::StreamId, events: Vec, - ) -> Result<(Vec, Self::Version), ESEBEventRepositoryError> + ) -> Result<(Vec, Self::Version), Error> where 'a: 'async_trait, E: 'async_trait, @@ -141,7 +144,7 @@ where for e in &events { let ed = EventData::json(e.event_type(), e.clone()) .map(|ed| ed.id(Uuid::new_v4())) - .map_err(ESEBEventRepositoryError::SerializeEventDataPayload)?; + .map_err(Error::SerializeEventDataPayload)?; perpared_events.push(ed); } @@ -154,24 +157,9 @@ where perpared_events, ) .await - .map_err(|e| ESEBEventRepositoryError::WriteStream(stream, e))?; + .map_err(|e| Error::WriteStream(stream, e))?; Ok((events, ExpectedRevision::Exact(res.next_expected_version))) } } -#[derive(Debug, Error)] -enum ESEBEventRepositoryError { - #[error("ESDB Error {0}")] - ESDBGeneral(eventstore::Error), - #[error("Error reading stream: {0}")] - ReadStream(eventstore::Error), - #[error("Could not deserialize event {0}")] - DeserializeEvent(serde_json::Error), - #[error("Could not parse event meta {0}")] - ParseMetadata(serde_json::Error), - #[error("Could not serialize event {0}")] - SerializeEventDataPayload(serde_json::Error), - #[error("Could not write to stream {0}: {1}")] - WriteStream(String, eventstore::Error), -} diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index 2c6ce08..dcdafff 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -2,10 +2,10 @@ use async_trait::async_trait; use super::{Command, Event}; -#[cfg(feature = "in_memory")] -pub mod in_memory; #[cfg(feature = "esdb")] pub mod esdb; +#[cfg(feature = "in_memory")] +pub mod in_memory; // Event Repositories #[async_trait] @@ -33,7 +33,11 @@ where events: Vec, ) -> Result<(Vec, Self::Version), Err>; } - +// Lifetimes added here to fix codegen issue with macro generated lifetimes - adding 'a and 'async_trait prevents +// compile errors about E not living long enough for fn append +// https://stackoverflow.com/questions/69560112/how-to-use-rust-async-trait-generic-to-a-lifetime-parameter +// https://github.com/dtolnay/async-trait/issues/8 +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f #[async_trait] pub trait LockingEventStoreWithStreams<'a, C, E, Err> where @@ -44,16 +48,20 @@ where type Version: Eq; async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Err>; - async fn load_from_version(&self, version: Self::Version, id: Option) -> Result<(Vec, Self::Version), Err>; + async fn load_from_version( + &self, + version: Self::Version, + id: Option, + ) -> Result<(Vec, Self::Version), Err>; async fn append( &mut self, version: Self::Version, stream: Self::StreamId, events: Vec, ) -> Result<(Vec, Self::Version), Err> - where - 'a: 'async_trait, - E: 'async_trait; + where + 'a: 'async_trait, + E: 'async_trait; } // State Repositories diff --git a/src/event_store.rs b/src/event_store.rs deleted file mode 100644 index c6e7765..0000000 --- a/src/event_store.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use chrono::{DateTime, Utc}; -use eventstore::{ - AppendToStreamOptions, EventData, ExpectedRevision, ReadStreamOptions, ResolvedEvent, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use async_trait::async_trait; -use uuid::Uuid; - -use crate::Event; - -use super::{ - CommmandInvariantError, EventContext, EventEnvelope, EventStore, FromCommandInvariant, - PreparedEvent, ToCommandInvariantError, -}; - -#[derive(Clone)] -pub struct ESDBEventStore { - is_test: bool, - client: eventstore::Client, -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("ESDB ERROR {0}")] - ESDBGeneral(eventstore::Error), - #[error("Error reading stream: {0}")] - ReadStream(eventstore::Error), - #[error("Could not deserialize event {0}")] - DeserializeEvent(serde_json::Error), - #[error("Could not parse event meta {0}")] - ParseMetadata(serde_json::Error), - #[error("Could not serialize event {0}")] - SerializeEventDataPayload(serde_json::Error), - #[error("Could not write to stream {0}: {1}")] - WriteStream(String, eventstore::Error), - #[error("Could not convert EventContext::Id to String {0}")] - ContextIdFromStr(String), - #[error("Command invariant bounds prevented a committed event {0}")] - CommandInvariant(String), -} - -impl FromCommandInvariant for Error { - fn from_command_invariant(cmd_err: Ctx::Err) -> Self - where - Ctx: EventContext, - ::Err: ToCommandInvariantError, - { - // Self::CommandInvariant(cmd_err.to_command_invariant_error()) - Self::CommandInvariant(cmd_err.to_string()) - } -} - -impl ESDBEventStore { - pub fn new(client: eventstore::Client) -> ESDBEventStore { - Self { - client, - is_test: false, - } - } - - // #[cfg(test)] - pub fn new_for_test(client: eventstore::Client) -> ESDBEventStore { - Self { - client, - is_test: true, - } - } - - pub fn stream_id(&self, id: Option) -> String - where - ::Id: Display, - { - let prefix = if self.is_test { "test_" } else { "" }; - - let ctx = Ctx::event_context(); - - match id { - Some(id_str) => format!("{}/{}", ctx, id_str), - None => format!("{}{}", prefix, ctx), - } - } - - fn resolved_to_event_envelope( - resolved_event: &ResolvedEvent, - ) -> Result<(EventEnvelope, ExpectedRevision), Error> - where - Ctx: EventContext, - ::Id: FromStr, - { - let event_data = resolved_event.get_original_event(); - let event = event_data - .as_json::<::Event>() - .map_err(Error::DeserializeEvent)?; - - let EventMetadata { - time, - event_context, - event_context_id, - }: EventMetadata = - serde_json::from_slice(&event_data.custom_metadata).map_err(Error::ParseMetadata)?; - - let event_context_id = match event_context_id { - Some(str) => { - Some(Ctx::Id::from_str(&str.clone()).map_err(|_| Error::ContextIdFromStr(str))?) - } - None => None, - }; - - Ok(( - EventEnvelope { - id: event_data.id, - time, - event_context, - event_context_id, - data: event, - }, - ExpectedRevision::Exact(event_data.revision), - )) - } -} - -#[async_trait] -impl EventStore for ESDBEventStore { - type Error = Error; - type Position = ExpectedRevision; - - async fn load( - &self, - id: Option, - ) -> Result<(Vec>, Self::Position), Self::Error> - where - ::Id: Display + FromStr, - { - let mut stream = self - .client - .read_stream(self.stream_id::(id), &ReadStreamOptions::default()) - .await - .map_err(Self::Error::ESDBGeneral)?; - - let mut evts: Vec = vec![]; - loop { - match stream.next().await { - Ok(Some(event)) => evts.push(event), - Ok(None) => break, - Err(eventstore::Error::ResourceNotFound) => { - return Ok((vec![], ExpectedRevision::NoStream)) - } - Err(e) => return Err(Self::Error::ReadStream(e)), - } - } - - let mut rv = vec![]; - let mut pos = ExpectedRevision::StreamExists; - - for ev in evts { - let (ee, revision) = ESDBEventStore::resolved_to_event_envelope(&ev)?; - - rv.push(ee); - pos = revision; - } - - Ok((rv, pos)) - } - - async fn append( - &self, - position: Self::Position, - event: PreparedEvent, - id: Option<::Id>, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Id: Clone + Display + ToString, - ::Event: Serialize + Event, - { - let event_meta = EventMetadata { - time: Utc::now(), - event_context: event.event_context, - event_context_id: id.clone().map(|x| x.to_string()), - }; - - let event_id = Uuid::new_v4(); - let event_data = EventData::json(event.data.event_type(), &event.data.clone()) - .map_err(Error::SerializeEventDataPayload)? - .id(event_id.clone()) - .metadata_as_json(event_meta.clone()) - .map_err(Error::SerializeEventDataPayload)?; - - let options = AppendToStreamOptions::default().expected_revision(position); - - let stream_id = self.stream_id::(id.clone()); - - let res = self - .client - .append_to_stream(stream_id.clone(), &options, event_data.clone()) - .await - .map_err(|e| Error::WriteStream(stream_id, e))?; - - let EventMetadata { - time, - event_context, - .. - } = event_meta; - - Ok(( - EventEnvelope { - id: event_id, - time, - event_context, - event_context_id: id, - data: event.data, - }, - ExpectedRevision::Exact(res.next_expected_version), - )) - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct EventMetadata { - time: DateTime, - event_context: String, - event_context_id: Option, -} diff --git a/src/lib.rs b/src/lib.rs index 4ddc8ec..c5e1197 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,154 +1 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; -use uuid::Uuid; - -pub mod event_store; -pub mod decider; - -#[derive(Debug, Clone)] -pub struct EventEnvelope -where - Ctx: EventContext + ?Sized, - ::Id: FromStr + Clone, - ::Event: Event + Clone, -{ - pub id: Uuid, - pub event_context: String, - pub event_context_id: Option, - pub time: DateTime, - pub data: Ctx::Event, -} - -#[derive(Debug, Clone)] -pub struct PreparedEvent -where - Ctx: EventContext + ?Sized, -{ - pub event_context: String, - pub event_context_id: Option, - pub data: Ctx::Event, -} - -pub trait Event { - fn event_type(&self) -> String; -} - -pub trait FromCommandInvariant { - fn from_command_invariant(cmd_err: Ctx::Err) -> Self - where - Ctx: EventContext, - ::Err: ToCommandInvariantError; -} - -#[derive(Error, Debug)] -pub enum CommmandInvariantError { - #[error("Command Invariant: {0}")] - CommandInvariant(String), - // #[error("User Command Invariant Command {0}")] - // Users(UserError) -} - -pub trait ToCommandInvariantError: Display { - fn to_command_invariant_error(&self) -> CommmandInvariantError; -} - -#[async_trait] -pub trait EventContext { - type Id: ToString + FromStr + Send + Sync + Eq + PartialEq + Clone; - type Command; - type Event: Send + Sync + DeserializeOwned + Event + Clone; - type Err: Debug; - type Services; - type State: Default; - - fn event_context() -> String; - - fn to_prepared_event(id: Option, event: Self::Event) -> PreparedEvent { - PreparedEvent { - event_context: Self::event_context(), - event_context_id: id, - data: event, - } - } - - async fn handle( - state: Self::State, - cmd: Self::Command, - services: Self::Services, - ) -> Result, Self::Err>; - - fn apply(state: Self::State, event: &EventEnvelope) -> Self::State; -} - -#[async_trait] -pub trait EventStore { - type Error; - type Position: Eq + PartialEq + Clone + Send; - - async fn load( - &self, - id: Option, - ) -> Result<(Vec>, Self::Position), Self::Error> - where - ::Id: Display + FromStr; - - async fn append( - &self, - position: Self::Position, - event: PreparedEvent, - id: Option, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Id: Clone + Display, - ::Event: Serialize + Event; - - async fn execute( - &self, - cmd: Ctx::Command, - services: Ctx::Services, - id: Option<::Id>, - ) -> Result<(EventEnvelope, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync + Debug, - ::Id: Display + Clone, - ::Event: Send + Clone + Serialize + Debug, - ::Services: Send, - ::Command: Send, - ::State: Send, - ::Err: Send + Sync + Clone + ToCommandInvariantError, - Self::Error: FromCommandInvariant, - { - let (value, position) = self.get_current_state::(id.clone()).await?; - let res = Ctx::handle(value, cmd, services) - .await - .map_err(Self::Error::from_command_invariant::)?; - - Ok(self.append::(position, res, id).await?) - } - - async fn get_current_state( - &self, - id: Option, - ) -> Result<(Ctx::State, Self::Position), Self::Error> - where - Ctx: EventContext + Send + Sync, - ::Event: Send + Clone + Debug, - ::Id: Display + Clone, - { - let (evts, position) = self.load::(id).await?; - - Ok(( - evts.iter() - .fold(Ctx::State::default(), |state, evt| Ctx::apply(state, evt)), - position, - )) - } -} +pub mod decider; \ No newline at end of file From bc490725eda26d8c2433f29e98f9460c8e65e1af Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 26 Dec 2022 18:46:35 -0700 Subject: [PATCH 08/70] move tests data to its own module, remove from Event Repository generics --- .gitignore | 3 + Cargo.toml | 2 +- src/decider/mod.rs | 143 ++-------------------------- src/decider/repository/esdb/mod.rs | 73 +++++++++++--- src/decider/repository/in_memory.rs | 28 +++--- src/decider/repository/mod.rs | 9 +- src/lib.rs | 5 +- src/test/deciders.rs | 141 +++++++++++++++++++++++++++ src/test/mod.rs | 5 + 9 files changed, 239 insertions(+), 170 deletions(-) create mode 100644 src/test/deciders.rs create mode 100644 src/test/mod.rs diff --git a/.gitignore b/.gitignore index 088ba6b..4135d20 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.env + diff --git a/Cargo.toml b/Cargo.toml index ad4608e..be28ea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ esdb = [] [dependencies] async-trait = "0.1.53" -chrono = "0.4.19" eventstore = "2.1.1" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.81" @@ -24,3 +23,4 @@ uuid = { version = "0.8.2", features = ["v4", "serde"] } [dev-dependencies] actix-rt = "2.7.0" assert_matches = "1.5.0" +dotenv = "0.15.0" \ No newline at end of file diff --git a/src/decider/mod.rs b/src/decider/mod.rs index 97d1d85..977765e 100644 --- a/src/decider/mod.rs +++ b/src/decider/mod.rs @@ -18,14 +18,16 @@ pub trait Decider { #[cfg(test)] mod tests { - use std::collections::HashMap; use assert_matches::assert_matches; - use thiserror::Error; - use crate::decider::repository::StateRepository; - - use self::user::UserFieldError; + use crate::{ + decider::repository::StateRepository, + test::{ + deciders::user::{self, UserCommand, UserDecider, UserEvent}, + ValueType, + }, + }; use super::{ repository::{ @@ -35,138 +37,9 @@ mod tests { *, }; - trait ValueType { - fn value(&self) -> T; - } - - mod user { - use thiserror::Error; - - use super::ValueType; - - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct User { - pub id: UserId, - pub name: UserName, - } - - pub type UserId = usize; - - pub type UnvalidatedUserName = String; - - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct UserName(String); - - impl TryFrom for UserName { - type Error = UserFieldError; - - fn try_from(value: String) -> Result { - let len = value.len(); - if len < 1 { - Err(UserFieldError::EmptyName) - } else if len > 10 { - Err(UserFieldError::NameToLong(value.to_owned())) - } else { - Ok(Self(value.to_owned())) - } - } - } - - impl ValueType for UserName { - fn value(&self) -> String { - self.0.to_owned() - } - } - - #[derive(Debug, Error)] - pub enum UserFieldError { - #[error("Username cannot be empty")] - EmptyName, - #[error("Username {0} is to long")] - NameToLong(String), - } - } - - #[derive(Debug, Clone, Default, PartialEq, Eq)] - struct UserDeciderState { - users: HashMap, - } - - enum UserCommand { - AddUser(user::UnvalidatedUserName), - UpdateUserName(user::UserId, user::UnvalidatedUserName), - } - - impl Command for UserCommand { - type State = UserDeciderState; - } - - #[derive(Clone)] - enum UserEvent { - UserAdded(user::User), - UserNameUpdated(user::UserId, user::UserName), - } - - impl Event for UserEvent { - fn event_type(&self) -> String { - match self { - UserEvent::UserAdded(_) => "UserAdded".to_string(), - UserEvent::UserNameUpdated(_, _) => "UserNameUpdated".to_string(), - } - } - } - - struct UserDecider {} - - impl Decider for UserDecider { - fn decide( - cmd: UserCommand, - state: UserDeciderState, - ) -> Result, UserDeciderError> { - match cmd { - UserCommand::AddUser(user_name) => { - let name = user::UserName::try_from(user_name) - .map_err(|e| UserDeciderError::UserField(e))?; - - Ok(vec![UserEvent::UserAdded(user::User { id: 1, name })]) - } - UserCommand::UpdateUserName(user_id, user_name) => { - let name = user::UserName::try_from(user_name) - .map_err(|e| UserDeciderError::UserField(e))?; - - Ok(vec![UserEvent::UserNameUpdated(user_id, name)]) - } - } - } - - fn evolve(mut state: UserDeciderState, event: UserEvent) -> UserDeciderState { - match event { - UserEvent::UserAdded(user) => { - state.users.insert(user.id.to_owned(), user.to_owned()); - state - } - UserEvent::UserNameUpdated(user_id, user_name) => { - state.users.get_mut(&user_id).unwrap().name = user_name.to_owned(); - state - } - } - } - - fn init() -> UserDeciderState { - Default::default() - } - } - - #[derive(Debug, Error)] - enum UserDeciderError { - #[error("Invalid user field {0:?}")] - UserField(UserFieldError), - } - #[actix_rt::test] async fn test_raw_decider() { - let event_repository: InMemoryEventRepository = - InMemoryEventRepository::new(); + let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index 4d08462..49bcf75 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use async_trait::async_trait; use eventstore::{ AppendToStreamOptions, Client, EventData, ExpectedRevision, ReadStreamOptions, ResolvedEvent, @@ -14,12 +16,21 @@ use super::LockingEventStoreWithStreams; pub mod error; -struct ESDBEventRepository { +struct ESDBEventRepository { client: Client, stream_name: String, + _hidden: PhantomData, } -impl<'a> ESDBEventRepository { +impl<'a, E> ESDBEventRepository { + fn new(client: &Client, stream_name: &str) -> Self { + Self { + client: client.to_owned(), + stream_name: stream_name.to_owned(), + _hidden: PhantomData::default(), + } + } + fn get_stream(&self, stream_id: Option) -> String { if let Some(id) = stream_id { format!("{}/{}", self.stream_name, id) @@ -38,19 +49,14 @@ impl<'a> ESDBEventRepository { } #[async_trait] -impl<'a, C, E> LockingEventStoreWithStreams<'a, C, E, Error> - for ESDBEventRepository +impl<'a, E> LockingEventStoreWithStreams<'a, E, Error> for ESDBEventRepository where - C: Command, E: Event + Sync + Send + Serialize + DeserializeOwned + Clone, { type StreamId = String; type Version = ExpectedRevision; - async fn load( - &self, - id: Option, - ) -> Result<(Vec, Self::Version), Error> { + async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Error> { let mut stream = self .client .read_stream(self.get_stream(id), &ReadStreamOptions::default()) @@ -75,9 +81,7 @@ where for ev in evts { let event_data = ev.get_original_event(); - let event = event_data - .as_json::() - .map_err(Error::DeserializeEvent)?; + let event = event_data.as_json::().map_err(Error::DeserializeEvent)?; pos = ExpectedRevision::Exact(event_data.revision); rv.push(event) @@ -118,9 +122,7 @@ where for ev in evts { let event_data = ev.get_original_event(); - let event = event_data - .as_json::() - .map_err(Error::DeserializeEvent)?; + let event = event_data.as_json::().map_err(Error::DeserializeEvent)?; pos = ExpectedRevision::Exact(event_data.revision); rv.push(event) @@ -163,3 +165,44 @@ where } } +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use crate::test::deciders::user::{User, UserEvent, UserId, UserName, UserCommand}; + + use super::*; + + const BASE_STREAM: &str = "decider.repository.esdb"; + + fn store_from_environment() -> eventstore::Client { + let _ = dotenv::dotenv().expect("File .env or Env Vars not found"); + let settings = dotenv::var("ESDB_CONNECTION_STRING") + .expect("ESDB to be set in env") + .parse() + .expect("ESDB connection string to parse"); + + eventstore::Client::new(settings).expect("Eventstore client") + } + + #[actix_rt::test] + async fn test_storage() { + let client = store_from_environment(); + let event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); + + let events = vec![ + UserEvent::UserAdded(User { + id: 1, + name: UserName::try_from("Mike").expect("Name is valid"), + }), + UserEvent::UserNameUpdated( + 1 as UserId, + UserName::try_from("Mike2").expect("Name is valid"), + ), + ]; + + let res: (Vec, ExpectedRevision) = event_repository.load(None).await.expect("loaded"); + + println!("Result: {:?}", res); + } +} diff --git a/src/decider/repository/in_memory.rs b/src/decider/repository/in_memory.rs index 59323c0..7435f64 100644 --- a/src/decider/repository/in_memory.rs +++ b/src/decider/repository/in_memory.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, fmt::Debug}; +use std::{fmt::Debug, marker::PhantomData}; use async_trait::async_trait; @@ -7,15 +7,18 @@ use crate::decider::{Command, Event}; use super::{EventRepository, StateRepository}; #[derive(Default)] -pub struct InMemoryEventRepository { +pub struct InMemoryEventRepository +where + E: Event + Clone + Send + Sync, +{ events: Vec, position: usize, - pd: PhantomData, } #[async_trait] -impl - EventRepository for InMemoryEventRepository +impl EventRepository for InMemoryEventRepository +where + E: Event + Clone + Send + Sync, { async fn load(&self) -> Result, InMemoryEventRepositoryError> { Ok(self.events.clone()) @@ -29,14 +32,16 @@ impl } } -impl InMemoryEventRepository { +impl InMemoryEventRepository +where + E: Event + Clone + Send + Sync, +{ pub fn new() -> Self { let events: Vec = vec![]; Self { events, position: Default::default(), - pd: PhantomData::::default(), } } } @@ -61,11 +66,10 @@ where } #[async_trait] -impl StateRepository - for InMemoryStateRepository - where - C: Command + Send + Sync, - ::State: Default + Send + Sync + Debug +impl StateRepository for InMemoryStateRepository +where + C: Command + Send + Sync, + ::State: Default + Send + Sync + Debug, { async fn reify(&self) -> ::State { self.state.clone() diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index dcdafff..ad321d2 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -9,9 +9,8 @@ pub mod in_memory; // Event Repositories #[async_trait] -pub trait EventRepository +pub trait EventRepository where - C: Command, E: Event + Sync + Send, { async fn load(&self) -> Result, Err>; @@ -19,9 +18,8 @@ where } #[async_trait] -pub trait LockingEventRepository +pub trait LockingEventRepository where - C: Command, E: Event + Sync + Send, { type Version: Eq; @@ -39,9 +37,8 @@ where // https://github.com/dtolnay/async-trait/issues/8 // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f #[async_trait] -pub trait LockingEventStoreWithStreams<'a, C, E, Err> +pub trait LockingEventStoreWithStreams<'a, E, Err> where - C: Command, E: Event + Sync + Send, { type StreamId; diff --git a/src/lib.rs b/src/lib.rs index c5e1197..3aa9a2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,4 @@ -pub mod decider; \ No newline at end of file +pub mod decider; + +#[cfg(test)] +mod test; \ No newline at end of file diff --git a/src/test/deciders.rs b/src/test/deciders.rs new file mode 100644 index 0000000..9e0b753 --- /dev/null +++ b/src/test/deciders.rs @@ -0,0 +1,141 @@ +pub(crate) mod user { + use std::collections::HashMap; + + use serde::{Serialize, Deserialize}; + use thiserror::Error; + + use crate::{test::ValueType, decider::{Decider, Command, Event}}; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub(crate) struct User { + pub id: UserId, + pub name: UserName, + } + + pub(crate) type UserId = usize; + + pub(crate) type UnvalidatedUserName = String; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub(crate) struct UserName(String); + + impl TryFrom for UserName { + type Error = UserFieldError; + + fn try_from(value: String) -> Result { + let len = value.len(); + if len < 1 { + Err(UserFieldError::EmptyName) + } else if len > 10 { + Err(UserFieldError::NameToLong(value.to_owned())) + } else { + Ok(Self(value.to_owned())) + } + } + } + + impl TryFrom<&str> for UserName { + type Error = UserFieldError; + + fn try_from(value: &str) -> Result { + let len = value.len(); + if len < 1 { + Err(UserFieldError::EmptyName) + } else if len > 10 { + Err(UserFieldError::NameToLong(value.to_owned())) + } else { + Ok(Self(value.to_owned())) + } + } + } + + impl ValueType for UserName { + fn value(&self) -> String { + self.0.to_owned() + } + } + + #[derive(Debug, Error)] + pub(crate) enum UserFieldError { + #[error("Username cannot be empty")] + EmptyName, + #[error("Username {0} is to long")] + NameToLong(String), + } + + pub(crate) struct UserDecider {} + + impl Decider for UserDecider { + fn decide( + cmd: UserCommand, + state: UserDeciderState, + ) -> Result, UserDeciderError> { + match cmd { + UserCommand::AddUser(user_name) => { + let name = UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserAdded(User { id: 1, name })]) + } + UserCommand::UpdateUserName(user_id, user_name) => { + let name = UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserNameUpdated(user_id, name)]) + } + } + } + + fn evolve(mut state: UserDeciderState, event: UserEvent) -> UserDeciderState { + match event { + UserEvent::UserAdded(user) => { + state.users.insert(user.id.to_owned(), user.to_owned()); + state + } + UserEvent::UserNameUpdated(user_id, user_name) => { + state.users.get_mut(&user_id).unwrap().name = user_name.to_owned(); + state + } + } + } + + fn init() -> UserDeciderState { + Default::default() + } + } + + #[derive(Debug, Clone, Default, PartialEq, Eq)] + pub(crate) struct UserDeciderState { + pub(crate) users: HashMap, + } + + pub(crate) enum UserCommand { + AddUser(UnvalidatedUserName), + UpdateUserName(UserId, UnvalidatedUserName), + } + + impl Command for UserCommand { + type State = UserDeciderState; + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub(crate) enum UserEvent { + UserAdded(User), + UserNameUpdated(UserId, UserName), + } + + impl Event for UserEvent { + fn event_type(&self) -> String { + match self { + UserEvent::UserAdded(_) => "UserAdded".to_string(), + UserEvent::UserNameUpdated(_, _) => "UserNameUpdated".to_string(), + } + } + } + + #[derive(Debug, Error)] + pub(crate) enum UserDeciderError { + #[error("Invalid user field {0:?}")] + UserField(UserFieldError), + } +} \ No newline at end of file diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..48ab662 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod deciders; + +pub(crate) trait ValueType { + fn value(&self) -> T; +} \ No newline at end of file From a7258be397710ff8c668257efdf72dc07c3cd526 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 12:57:07 -0700 Subject: [PATCH 09/70] Fine grained streams functioning --- src/decider/repository/esdb/mod.rs | 89 ++++++++++++++++++++++++------ src/test/deciders.rs | 2 +- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index 49bcf75..25d4bfe 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{fmt::Debug, marker::PhantomData}; use async_trait::async_trait; use eventstore::{ @@ -33,9 +33,9 @@ impl<'a, E> ESDBEventRepository { fn get_stream(&self, stream_id: Option) -> String { if let Some(id) = stream_id { - format!("{}/{}", self.stream_name, id) + format!("{}-{}", self.stream_name, id) } else { - self.stream_name.clone() + format!("$ce-{}", self.stream_name) } } @@ -51,15 +51,19 @@ impl<'a, E> ESDBEventRepository { #[async_trait] impl<'a, E> LockingEventStoreWithStreams<'a, E, Error> for ESDBEventRepository where - E: Event + Sync + Send + Serialize + DeserializeOwned + Clone, + E: Event + Sync + Send + Serialize + DeserializeOwned + Clone + Debug, { type StreamId = String; type Version = ExpectedRevision; async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Error> { + println!("Calling Stream {}", self.get_stream(id.clone())); let mut stream = self .client - .read_stream(self.get_stream(id), &ReadStreamOptions::default()) + .read_stream( + self.get_stream(id), + &ReadStreamOptions::default().resolve_link_tos(), + ) .await .map_err(Error::ESDBGeneral)?; @@ -80,11 +84,13 @@ where let mut pos = ExpectedRevision::StreamExists; for ev in evts { - let event_data = ev.get_original_event(); - let event = event_data.as_json::().map_err(Error::DeserializeEvent)?; + pos = ExpectedRevision::Exact(ev.get_original_event().revision); - pos = ExpectedRevision::Exact(event_data.revision); - rv.push(event) + if let Some(event_data) = ev.event { + if let Ok(event) = event_data.as_json::().map_err(Error::DeserializeEvent) { + rv.push(event); + } + } } Ok((rv, pos)) @@ -167,30 +173,50 @@ where #[cfg(test)] mod tests { + use core::time; + use std::thread; + use assert_matches::assert_matches; + use eventstore::DeleteStreamOptions; - use crate::test::deciders::user::{User, UserEvent, UserId, UserName, UserCommand}; + use crate::test::deciders::user::{User, UserEvent, UserId, UserName}; use super::*; - const BASE_STREAM: &str = "decider.repository.esdb"; + const BASE_STREAM: &str = "test"; - fn store_from_environment() -> eventstore::Client { + async fn store_from_environment(ids: Vec) -> eventstore::Client { let _ = dotenv::dotenv().expect("File .env or Env Vars not found"); let settings = dotenv::var("ESDB_CONNECTION_STRING") .expect("ESDB to be set in env") .parse() .expect("ESDB connection string to parse"); - eventstore::Client::new(settings).expect("Eventstore client") + let client = eventstore::Client::new(settings).expect("Eventstore client"); + + for id in ids { + let _ = client + .delete_stream( + format!("{}-{}", BASE_STREAM, id), + &DeleteStreamOptions::default(), + ) + .await; + } + + client } #[actix_rt::test] async fn test_storage() { - let client = store_from_environment(); - let event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); + let client = store_from_environment(vec![1, 2]).await; + + let mut event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); - let events = vec![ + let res: (Vec, ExpectedRevision) = + event_repository.load(None).await.expect("loaded"); + assert_matches!(res, (v, _) if v == vec![] as Vec); + + let events1 = vec![ UserEvent::UserAdded(User { id: 1, name: UserName::try_from("Mike").expect("Name is valid"), @@ -201,8 +227,35 @@ mod tests { ), ]; - let res: (Vec, ExpectedRevision) = event_repository.load(None).await.expect("loaded"); + let _ = event_repository + .append(ExpectedRevision::Any, "1".to_string(), events1) + .await; + + let events2 = vec![ + UserEvent::UserAdded(User { + id: 2, + name: UserName::try_from("Stella").expect("Name is valid"), + }), + UserEvent::UserNameUpdated( + 1 as UserId, + UserName::try_from("Stella2").expect("Name is valid"), + ), + ]; + + let _ = event_repository + .append(ExpectedRevision::Any, "2".to_string(), events2) + .await; + + // Crude but we need to wait for ESDB to catch up its "Categories" auto projection + thread::sleep(time::Duration::from_secs(1)); + + let res = event_repository.load(None).await; + println!("Result $all: {:#?}", res); + + let res = event_repository.load(Some("1".to_string())).await; + println!("Result id = 1: {:#?}", res); - println!("Result: {:?}", res); + let res = event_repository.load(Some("2".to_string())).await; + println!("Result id = 2: {:#?}", res); } } diff --git a/src/test/deciders.rs b/src/test/deciders.rs index 9e0b753..540d977 100644 --- a/src/test/deciders.rs +++ b/src/test/deciders.rs @@ -118,7 +118,7 @@ pub(crate) mod user { type State = UserDeciderState; } - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) enum UserEvent { UserAdded(User), UserNameUpdated(UserId, UserName), From 84fb3511dd84f60880af5328bb168a058b24bcf0 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 13:18:04 -0700 Subject: [PATCH 10/70] add test assertions --- src/decider/repository/esdb/mod.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index 25d4bfe..0170d9b 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -8,7 +8,7 @@ use eventstore::{ use serde::{de::DeserializeOwned, Serialize}; use uuid::Uuid; -use crate::decider::{Command, Event}; +use crate::decider::Event; use self::error::Error; @@ -16,14 +16,14 @@ use super::LockingEventStoreWithStreams; pub mod error; -struct ESDBEventRepository { +pub struct ESDBEventRepository { client: Client, stream_name: String, _hidden: PhantomData, } impl<'a, E> ESDBEventRepository { - fn new(client: &Client, stream_name: &str) -> Self { + pub fn new(client: &Client, stream_name: &str) -> Self { Self { client: client.to_owned(), stream_name: stream_name.to_owned(), @@ -87,6 +87,7 @@ where pos = ExpectedRevision::Exact(ev.get_original_event().revision); if let Some(event_data) = ev.event { + // Continue on deser failure - occasionally you'll get delete and other system types in the stream if let Ok(event) = event_data.as_json::().map_err(Error::DeserializeEvent) { rv.push(event); } @@ -228,7 +229,7 @@ mod tests { ]; let _ = event_repository - .append(ExpectedRevision::Any, "1".to_string(), events1) + .append(ExpectedRevision::Any, "1".to_string(), events1.clone()) .await; let events2 = vec![ @@ -243,19 +244,22 @@ mod tests { ]; let _ = event_repository - .append(ExpectedRevision::Any, "2".to_string(), events2) + .append(ExpectedRevision::Any, "2".to_string(), events2.clone()) .await; // Crude but we need to wait for ESDB to catch up its "Categories" auto projection thread::sleep(time::Duration::from_secs(1)); - let res = event_repository.load(None).await; - println!("Result $all: {:#?}", res); - let res = event_repository.load(Some("1".to_string())).await; - println!("Result id = 1: {:#?}", res); + assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events1); let res = event_repository.load(Some("2".to_string())).await; - println!("Result id = 2: {:#?}", res); + assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events2); + + let res = event_repository.load(None).await; + + let events_combined: Vec = + events1.into_iter().chain(events2.into_iter()).collect(); + assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events_combined); } } From f2192df8c778122b1263f76c209a9d5277330df9 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 14:40:49 -0700 Subject: [PATCH 11/70] remove all clones by taking read only refs as args --- src/decider/repository/esdb/mod.rs | 118 ++++++++++++++--------------- src/decider/repository/mod.rs | 19 ++--- 2 files changed, 66 insertions(+), 71 deletions(-) diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index 0170d9b..34b7257 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -12,7 +12,7 @@ use crate::decider::Event; use self::error::Error; -use super::LockingEventStoreWithStreams; +use super::VersionedEventRepositoryWithStreams; pub mod error; @@ -31,7 +31,7 @@ impl<'a, E> ESDBEventRepository { } } - fn get_stream(&self, stream_id: Option) -> String { + fn get_stream(&self, stream_id: &Option) -> String { if let Some(id) = stream_id { format!("{}-{}", self.stream_name, id) } else { @@ -49,20 +49,30 @@ impl<'a, E> ESDBEventRepository { } #[async_trait] -impl<'a, E> LockingEventStoreWithStreams<'a, E, Error> for ESDBEventRepository +impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> for ESDBEventRepository where E: Event + Sync + Send + Serialize + DeserializeOwned + Clone + Debug, { type StreamId = String; type Version = ExpectedRevision; - async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Error> { - println!("Calling Stream {}", self.get_stream(id.clone())); + async fn load(&self, id: &Option) -> Result<(Vec, Self::Version), Error> { + self.load_from_version(&ExpectedRevision::Any, id).await + } + + async fn load_from_version( + &self, + version: &Self::Version, + id: &Option, + ) -> Result<(Vec, Self::Version), Error> { + println!("Calling Stream {}", self.get_stream(id)); let mut stream = self .client .read_stream( self.get_stream(id), - &ReadStreamOptions::default().resolve_link_tos(), + &ReadStreamOptions::default() + .resolve_link_tos() + .position(Self::expected_revision_to_position(&version)), ) .await .map_err(Error::ESDBGeneral)?; @@ -97,52 +107,11 @@ where Ok((rv, pos)) } - async fn load_from_version( - &self, - version: Self::Version, - id: Option, - ) -> Result<(Vec, Self::Version), Error> { - let options = - ReadStreamOptions::default().position(Self::expected_revision_to_position(&version)); - - let mut stream = self - .client - .read_stream(self.get_stream(id), &options) - .await - .map_err(Error::ESDBGeneral)?; - - let mut evts: Vec = vec![]; - - loop { - match stream.next().await { - Ok(Some(event)) => evts.push(event), - Ok(None) => break, - Err(eventstore::Error::ResourceNotFound) => { - return Ok((vec![], ExpectedRevision::NoStream)) - } - Err(e) => return Err(Error::ReadStream(e)), - } - } - - let mut rv = vec![]; - let mut pos = ExpectedRevision::StreamExists; - - for ev in evts { - let event_data = ev.get_original_event(); - let event = event_data.as_json::().map_err(Error::DeserializeEvent)?; - - pos = ExpectedRevision::Exact(event_data.revision); - rv.push(event) - } - - Ok((rv, pos)) - } - async fn append( &mut self, - version: Self::Version, - stream: Self::StreamId, - events: Vec, + version: &Self::Version, + stream: &Self::StreamId, + events: &Vec, ) -> Result<(Vec, Self::Version), Error> where 'a: 'async_trait, @@ -150,8 +119,8 @@ where { let mut perpared_events = vec![]; - for e in &events { - let ed = EventData::json(e.event_type(), e.clone()) + for e in events { + let ed = EventData::json(e.event_type(), e) .map(|ed| ed.id(Uuid::new_v4())) .map_err(Error::SerializeEventDataPayload)?; @@ -161,14 +130,14 @@ where let res = self .client .append_to_stream( - self.get_stream(Some(stream.to_owned())), - &AppendToStreamOptions::default().expected_revision(version), + self.get_stream(&Some(stream.to_owned())), + &AppendToStreamOptions::default().expected_revision(*version), perpared_events, ) .await - .map_err(|e| Error::WriteStream(stream, e))?; + .map_err(|e| Error::WriteStream(stream.to_owned(), e))?; - Ok((events, ExpectedRevision::Exact(res.next_expected_version))) + Ok((events.to_owned(), ExpectedRevision::Exact(res.next_expected_version))) } } @@ -211,10 +180,13 @@ mod tests { async fn test_storage() { let client = store_from_environment(vec![1, 2]).await; + let id_1 = "1".to_string(); + let id_2 = "2".to_string(); + let mut event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); let res: (Vec, ExpectedRevision) = - event_repository.load(None).await.expect("loaded"); + event_repository.load(&None).await.expect("loaded"); assert_matches!(res, (v, _) if v == vec![] as Vec); let events1 = vec![ @@ -229,7 +201,7 @@ mod tests { ]; let _ = event_repository - .append(ExpectedRevision::Any, "1".to_string(), events1.clone()) + .append(&ExpectedRevision::Any, &id_1, &events1) .await; let events2 = vec![ @@ -244,22 +216,44 @@ mod tests { ]; let _ = event_repository - .append(ExpectedRevision::Any, "2".to_string(), events2.clone()) + .append(&ExpectedRevision::Any, &id_2, &events2) .await; // Crude but we need to wait for ESDB to catch up its "Categories" auto projection thread::sleep(time::Duration::from_secs(1)); - let res = event_repository.load(Some("1".to_string())).await; + let res = event_repository.load(&Some("1".to_string())).await; assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events1); - let res = event_repository.load(Some("2".to_string())).await; + let res = event_repository.load(&Some("2".to_string())).await; assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events2); - let res = event_repository.load(None).await; + let res = event_repository.load(&None).await; let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events_combined); + + let res = event_repository.load(&Some("1".to_string())).await; + let version = res.unwrap().1; + + let new_events = vec![UserEvent::UserNameUpdated( + 1, + UserName::try_from("Mike").expect("Name is valid"), + )]; + + let res = event_repository + .append(&version, &id_1, &new_events) + .await + .expect("Success"); + + let version = res.1; + + let (latest_events, _) = event_repository + .load_from_version(&version, &Some("1".to_string())) + .await + .expect("load success"); + + assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); } } diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index ad321d2..5b5c7c3 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -18,7 +18,7 @@ where } #[async_trait] -pub trait LockingEventRepository +pub trait VersionedEventRepository where E: Event + Sync + Send, { @@ -31,30 +31,31 @@ where events: Vec, ) -> Result<(Vec, Self::Version), Err>; } + // Lifetimes added here to fix codegen issue with macro generated lifetimes - adding 'a and 'async_trait prevents // compile errors about E not living long enough for fn append // https://stackoverflow.com/questions/69560112/how-to-use-rust-async-trait-generic-to-a-lifetime-parameter // https://github.com/dtolnay/async-trait/issues/8 // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f #[async_trait] -pub trait LockingEventStoreWithStreams<'a, E, Err> +pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where E: Event + Sync + Send, { type StreamId; type Version: Eq; - async fn load(&self, id: Option) -> Result<(Vec, Self::Version), Err>; + async fn load(&self, id: &Option) -> Result<(Vec, Self::Version), Err>; async fn load_from_version( &self, - version: Self::Version, - id: Option, + version: &Self::Version, + id: &Option, ) -> Result<(Vec, Self::Version), Err>; async fn append( &mut self, - version: Self::Version, - stream: Self::StreamId, - events: Vec, + version: &Self::Version, + stream: &Self::StreamId, + events: &Vec, ) -> Result<(Vec, Self::Version), Err> where 'a: 'async_trait, @@ -69,7 +70,7 @@ pub trait StateRepository { } #[async_trait] -pub trait LockingStateRepository { +pub trait VersionedStateRepository { type Version: Eq; async fn reify(&self) -> Result<(::State, Self::Version), Err>; From 9c202308cc9e2c02e7d9ddd14309050993f0567d Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 14:43:59 -0700 Subject: [PATCH 12/70] Nest reference in stream optional to simplify interface --- src/decider/repository/esdb/mod.rs | 20 ++++++++++---------- src/decider/repository/mod.rs | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index 34b7257..f2fb3ee 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -31,7 +31,7 @@ impl<'a, E> ESDBEventRepository { } } - fn get_stream(&self, stream_id: &Option) -> String { + fn get_stream(&self, stream_id: Option<&String>) -> String { if let Some(id) = stream_id { format!("{}-{}", self.stream_name, id) } else { @@ -56,14 +56,14 @@ where type StreamId = String; type Version = ExpectedRevision; - async fn load(&self, id: &Option) -> Result<(Vec, Self::Version), Error> { + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Error> { self.load_from_version(&ExpectedRevision::Any, id).await } async fn load_from_version( &self, version: &Self::Version, - id: &Option, + id: Option<&Self::StreamId>, ) -> Result<(Vec, Self::Version), Error> { println!("Calling Stream {}", self.get_stream(id)); let mut stream = self @@ -130,7 +130,7 @@ where let res = self .client .append_to_stream( - self.get_stream(&Some(stream.to_owned())), + self.get_stream(Some(stream)), &AppendToStreamOptions::default().expected_revision(*version), perpared_events, ) @@ -186,7 +186,7 @@ mod tests { let mut event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); let res: (Vec, ExpectedRevision) = - event_repository.load(&None).await.expect("loaded"); + event_repository.load(None).await.expect("loaded"); assert_matches!(res, (v, _) if v == vec![] as Vec); let events1 = vec![ @@ -222,19 +222,19 @@ mod tests { // Crude but we need to wait for ESDB to catch up its "Categories" auto projection thread::sleep(time::Duration::from_secs(1)); - let res = event_repository.load(&Some("1".to_string())).await; + let res = event_repository.load(Some(&id_1)).await; assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events1); - let res = event_repository.load(&Some("2".to_string())).await; + let res = event_repository.load(Some(&id_2)).await; assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events2); - let res = event_repository.load(&None).await; + let res = event_repository.load(None).await; let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events_combined); - let res = event_repository.load(&Some("1".to_string())).await; + let res = event_repository.load(Some(&id_1)).await; let version = res.unwrap().1; let new_events = vec![UserEvent::UserNameUpdated( @@ -250,7 +250,7 @@ mod tests { let version = res.1; let (latest_events, _) = event_repository - .load_from_version(&version, &Some("1".to_string())) + .load_from_version(&version, Some(&id_1)) .await .expect("load success"); diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index 5b5c7c3..8a5be07 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -45,11 +45,11 @@ where type StreamId; type Version: Eq; - async fn load(&self, id: &Option) -> Result<(Vec, Self::Version), Err>; + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Err>; async fn load_from_version( &self, version: &Self::Version, - id: &Option, + id: Option<&Self::StreamId>, ) -> Result<(Vec, Self::Version), Err>; async fn append( &mut self, From 3cf7c8c5a954311b06db4b1d9fcd6daa5aa76e4b Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 14:57:45 -0700 Subject: [PATCH 13/70] convert other repository traits to use references --- src/decider/repository/in_memory.rs | 6 +++--- src/decider/repository/mod.rs | 11 ++++++----- src/test/deciders.rs | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/decider/repository/in_memory.rs b/src/decider/repository/in_memory.rs index 7435f64..937ff57 100644 --- a/src/decider/repository/in_memory.rs +++ b/src/decider/repository/in_memory.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, marker::PhantomData}; +use std::fmt::Debug; use async_trait::async_trait; @@ -24,11 +24,11 @@ where Ok(self.events.clone()) } - async fn append(&mut self, events: Vec) -> Result, InMemoryEventRepositoryError> { + async fn append(&mut self, events: &Vec) -> Result, InMemoryEventRepositoryError> { self.events.extend(events.clone()); self.position += 1; - Ok(events) + Ok(events.to_owned()) } } diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index 8a5be07..dced3f2 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -14,7 +14,7 @@ where E: Event + Sync + Send, { async fn load(&self) -> Result, Err>; - async fn append(&mut self, events: Vec) -> Result, Err>; + async fn append(&mut self, events: &Vec) -> Result, Err>; } #[async_trait] @@ -24,11 +24,12 @@ where { type Version: Eq; - async fn load(&self) -> Result<(Vec, Self::Version), Err>; + async fn load(&self) -> Result<(Vec, &Self::Version), Err>; + async fn load_from_version(&self) -> Result<(Vec, Option<&Self::Version>), Err>; async fn append( &mut self, - version: Self::Version, - events: Vec, + version: &Self::Version, + events: &Vec, ) -> Result<(Vec, Self::Version), Err>; } @@ -76,7 +77,7 @@ pub trait VersionedStateRepository { async fn reify(&self) -> Result<(::State, Self::Version), Err>; async fn save( &mut self, - version: Self::Version, + version: &Self::Version, state: &::State, ) -> Result<::State, Err>; } diff --git a/src/test/deciders.rs b/src/test/deciders.rs index 540d977..b7e17c0 100644 --- a/src/test/deciders.rs +++ b/src/test/deciders.rs @@ -68,7 +68,7 @@ pub(crate) mod user { impl Decider for UserDecider { fn decide( cmd: UserCommand, - state: UserDeciderState, + _state: UserDeciderState, ) -> Result, UserDeciderError> { match cmd { UserCommand::AddUser(user_name) => { From cd995ba70746b7c3bd92bd1dcf4961e507b1c55f Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 15:28:10 -0700 Subject: [PATCH 14/70] move in-memory module --- src/decider/mod.rs | 10 +++--- .../{in_memory.rs => in_memory/mod.rs} | 0 src/test/deciders.rs | 36 ++++++++++++++----- 3 files changed, 32 insertions(+), 14 deletions(-) rename src/decider/repository/{in_memory.rs => in_memory/mod.rs} (100%) diff --git a/src/decider/mod.rs b/src/decider/mod.rs index 977765e..9d3fbf8 100644 --- a/src/decider/mod.rs +++ b/src/decider/mod.rs @@ -11,8 +11,8 @@ pub trait Event { #[async_trait] pub trait Decider { - fn decide(cmd: Cmd, state: S) -> Result, Err>; - fn evolve(state: S, event: E) -> S; + fn decide(cmd: &Cmd, state: &S) -> Result, Err>; + fn evolve(state: S, event: &E) -> S; fn init() -> S; } @@ -47,11 +47,11 @@ mod tests { .load() .await .expect("Empty Events Vector") - .into_iter() + .iter() .fold(UserDecider::init(), UserDecider::evolve); let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); - let events = UserDecider::decide(cmd, state.clone()).expect("Decider Success"); + let events = UserDecider::decide(&cmd, &state).expect("Decider Success"); if let Some(UserEvent::UserAdded(user::User { name, id })) = events.clone().first() { let user_id = id.clone(); @@ -59,7 +59,7 @@ mod tests { assert_eq!(name.value(), "Mike".to_string()); - let state = events.into_iter().fold(state.clone(), UserDecider::evolve); + let state = events.iter().fold(state.clone(), UserDecider::evolve); let _ = state_repository.save(&state).await; assert_eq!(state_repository.reify().await, state.clone()); diff --git a/src/decider/repository/in_memory.rs b/src/decider/repository/in_memory/mod.rs similarity index 100% rename from src/decider/repository/in_memory.rs rename to src/decider/repository/in_memory/mod.rs diff --git a/src/test/deciders.rs b/src/test/deciders.rs index b7e17c0..2a82dcb 100644 --- a/src/test/deciders.rs +++ b/src/test/deciders.rs @@ -1,10 +1,13 @@ pub(crate) mod user { use std::collections::HashMap; - use serde::{Serialize, Deserialize}; + use serde::{Deserialize, Serialize}; use thiserror::Error; - use crate::{test::ValueType, decider::{Decider, Command, Event}}; + use crate::{ + decider::{Command, Decider, Event}, + test::ValueType, + }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct User { @@ -31,7 +34,7 @@ pub(crate) mod user { } else { Ok(Self(value.to_owned())) } - } + } } impl TryFrom<&str> for UserName { @@ -49,6 +52,21 @@ pub(crate) mod user { } } + impl TryFrom<&String> for UserName { + type Error = UserFieldError; + + fn try_from(value: &String) -> Result { + let len = value.len(); + if len < 1 { + Err(UserFieldError::EmptyName) + } else if len > 10 { + Err(UserFieldError::NameToLong(value.to_owned())) + } else { + Ok(Self(value.to_owned())) + } + } + } + impl ValueType for UserName { fn value(&self) -> String { self.0.to_owned() @@ -67,8 +85,8 @@ pub(crate) mod user { impl Decider for UserDecider { fn decide( - cmd: UserCommand, - _state: UserDeciderState, + cmd: &UserCommand, + _state: &UserDeciderState, ) -> Result, UserDeciderError> { match cmd { UserCommand::AddUser(user_name) => { @@ -81,12 +99,12 @@ pub(crate) mod user { let name = UserName::try_from(user_name) .map_err(|e| UserDeciderError::UserField(e))?; - Ok(vec![UserEvent::UserNameUpdated(user_id, name)]) + Ok(vec![UserEvent::UserNameUpdated(user_id.to_owned(), name)]) } } } - fn evolve(mut state: UserDeciderState, event: UserEvent) -> UserDeciderState { + fn evolve(mut state: UserDeciderState, event: &UserEvent) -> UserDeciderState { match event { UserEvent::UserAdded(user) => { state.users.insert(user.id.to_owned(), user.to_owned()); @@ -131,11 +149,11 @@ pub(crate) mod user { UserEvent::UserNameUpdated(_, _) => "UserNameUpdated".to_string(), } } - } + } #[derive(Debug, Error)] pub(crate) enum UserDeciderError { #[error("Invalid user field {0:?}")] UserField(UserFieldError), } -} \ No newline at end of file +} From df70034335911ba833778241b66a1c0012d8d53b Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 15:51:41 -0700 Subject: [PATCH 15/70] new basic in memory state repository --- src/decider/repository/in_memory/mod.rs | 29 +++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/decider/repository/in_memory/mod.rs b/src/decider/repository/in_memory/mod.rs index 937ff57..2ca75df 100644 --- a/src/decider/repository/in_memory/mod.rs +++ b/src/decider/repository/in_memory/mod.rs @@ -1,4 +1,4 @@ -use std::fmt::Debug; +use std::{fmt::Debug, sync::{Mutex, Arc}}; use async_trait::async_trait; @@ -11,8 +11,7 @@ pub struct InMemoryEventRepository where E: Event + Clone + Send + Sync, { - events: Vec, - position: usize, + state: Arc>> , } #[async_trait] @@ -21,14 +20,16 @@ where E: Event + Clone + Send + Sync, { async fn load(&self) -> Result, InMemoryEventRepositoryError> { - Ok(self.events.clone()) + let lock = self.state.lock().unwrap(); + Ok(lock.events.clone()) } async fn append(&mut self, events: &Vec) -> Result, InMemoryEventRepositoryError> { - self.events.extend(events.clone()); - self.position += 1; + let mut lock = self.state.lock().unwrap(); + lock.events.extend(events.to_owned()); + lock.position = lock.events.len(); - Ok(events.to_owned()) + Ok(events.clone()) } } @@ -37,15 +38,21 @@ where E: Event + Clone + Send + Sync, { pub fn new() -> Self { - let events: Vec = vec![]; - Self { - events, - position: Default::default(), + state: Arc::new(Mutex::new(InMemoryEventRepositoryState { + events: vec![], + position: 0, + })) } } } +#[derive(Debug, Default)] +struct InMemoryEventRepositoryState { + events: Vec, + position: usize +} + #[derive(Debug)] pub enum InMemoryEventRepositoryError {} From b07ce0422dc4b0ac1e4990c700e36691e62805c8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 16:09:28 -0700 Subject: [PATCH 16/70] chore: rename simplistic in memory event repository & add stub for versioned in memory repository with streams --- src/decider/mod.rs | 4 +-- src/decider/repository/in_memory/mod.rs | 37 +++++++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/decider/mod.rs b/src/decider/mod.rs index 9d3fbf8..640529d 100644 --- a/src/decider/mod.rs +++ b/src/decider/mod.rs @@ -31,7 +31,7 @@ mod tests { use super::{ repository::{ - in_memory::{InMemoryEventRepository, InMemoryStateRepository}, + in_memory::{SimpleInMemoryEventRepository, InMemoryStateRepository}, EventRepository, }, *, @@ -39,7 +39,7 @@ mod tests { #[actix_rt::test] async fn test_raw_decider() { - let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); + let event_repository: SimpleInMemoryEventRepository = SimpleInMemoryEventRepository::new(); let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); diff --git a/src/decider/repository/in_memory/mod.rs b/src/decider/repository/in_memory/mod.rs index 2ca75df..f0eacfe 100644 --- a/src/decider/repository/in_memory/mod.rs +++ b/src/decider/repository/in_memory/mod.rs @@ -1,4 +1,8 @@ -use std::{fmt::Debug, sync::{Mutex, Arc}}; +use std::{ + collections::HashMap, + fmt::Debug, + sync::{Arc, Mutex}, +}; use async_trait::async_trait; @@ -6,25 +10,26 @@ use crate::decider::{Command, Event}; use super::{EventRepository, StateRepository}; +/// Events /// #[derive(Default)] -pub struct InMemoryEventRepository +pub struct SimpleInMemoryEventRepository where E: Event + Clone + Send + Sync, { - state: Arc>> , + state: Arc>>, } #[async_trait] -impl EventRepository for InMemoryEventRepository +impl EventRepository for SimpleInMemoryEventRepository where E: Event + Clone + Send + Sync, { - async fn load(&self) -> Result, InMemoryEventRepositoryError> { + async fn load(&self) -> Result, SimpleInMemoryError> { let lock = self.state.lock().unwrap(); Ok(lock.events.clone()) } - async fn append(&mut self, events: &Vec) -> Result, InMemoryEventRepositoryError> { + async fn append(&mut self, events: &Vec) -> Result, SimpleInMemoryError> { let mut lock = self.state.lock().unwrap(); lock.events.extend(events.to_owned()); lock.position = lock.events.len(); @@ -33,7 +38,7 @@ where } } -impl InMemoryEventRepository +impl SimpleInMemoryEventRepository where E: Event + Clone + Send + Sync, { @@ -42,7 +47,7 @@ where state: Arc::new(Mutex::new(InMemoryEventRepositoryState { events: vec![], position: 0, - })) + })), } } } @@ -50,12 +55,20 @@ where #[derive(Debug, Default)] struct InMemoryEventRepositoryState { events: Vec, - position: usize + position: usize, } #[derive(Debug)] -pub enum InMemoryEventRepositoryError {} +pub enum SimpleInMemoryError {} +pub struct StreamedVersionedInMemoryEventRepository +where + E: Event + Sync + Send, +{ + state: HashMap>>>, +} + +/// State /// pub struct InMemoryStateRepository { state: ::State, } @@ -73,7 +86,7 @@ where } #[async_trait] -impl StateRepository for InMemoryStateRepository +impl StateRepository for InMemoryStateRepository where C: Command + Send + Sync, ::State: Default + Send + Sync + Debug, @@ -85,7 +98,7 @@ where async fn save( &mut self, state: &::State, - ) -> Result<::State, InMemoryEventRepositoryError> { + ) -> Result<::State, SimpleInMemoryError> { self.state = state.clone(); Ok(self.state.to_owned()) } From fb6eb41ad6fc1fdb23014355d399f27b1d7a6b84 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 27 Dec 2022 22:51:07 -0700 Subject: [PATCH 17/70] VersionEventRepositoryWithStreams impl --- src/decider/repository/in_memory/mod.rs | 132 +++++++++++++++++++++++- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/src/decider/repository/in_memory/mod.rs b/src/decider/repository/in_memory/mod.rs index f0eacfe..ff65a2b 100644 --- a/src/decider/repository/in_memory/mod.rs +++ b/src/decider/repository/in_memory/mod.rs @@ -8,7 +8,7 @@ use async_trait::async_trait; use crate::decider::{Command, Event}; -use super::{EventRepository, StateRepository}; +use super::{EventRepository, StateRepository, VersionedEventRepositoryWithStreams}; /// Events /// #[derive(Default)] @@ -44,10 +44,7 @@ where { pub fn new() -> Self { Self { - state: Arc::new(Mutex::new(InMemoryEventRepositoryState { - events: vec![], - position: 0, - })), + state: Arc::new(Mutex::new(InMemoryEventRepositoryState::new())), } } } @@ -58,6 +55,15 @@ struct InMemoryEventRepositoryState { position: usize, } +impl InMemoryEventRepositoryState { + pub fn new() -> Self { + InMemoryEventRepositoryState { + events: vec![], + position: 0, + } + } +} + #[derive(Debug)] pub enum SimpleInMemoryError {} @@ -65,9 +71,125 @@ pub struct StreamedVersionedInMemoryEventRepository where E: Event + Sync + Send, { + stream_name: String, state: HashMap>>>, } +impl<'a, E> StreamedVersionedInMemoryEventRepository +where + E: Event + Sync + Send, +{ + pub fn new(stream_name: &str) -> Self { + Self { + stream_name: stream_name.to_owned(), + state: HashMap::default(), + } + } + + fn get_stream(&self, stream_id: Option<&String>) -> String { + if let Some(id) = stream_id { + format!("{}/{}", self.stream_name, id) + } else { + format!("{}", self.stream_name) + } + } + + fn index_from_version(version: &InMemoryStreamVersion) -> usize { + match version { + InMemoryStreamVersion::Any => 0, + InMemoryStreamVersion::Exact(v) => *v, + } + } +} + +#[async_trait] +impl<'a, E> + VersionedEventRepositoryWithStreams<'a, E, StreamedVersionedInMemoryEventRepositoryError> + for StreamedVersionedInMemoryEventRepository +where + E: Event + Sync + Send + Clone, +{ + type StreamId = String; + type Version = InMemoryStreamVersion; + + async fn load( + &self, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> { + self.load_from_version(&InMemoryStreamVersion::Any, id) + .await + } + async fn load_from_version( + &self, + version: &Self::Version, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> { + let stream_key = self.get_stream(id); + println!("Calling Stream {}", &stream_key); + + let stream_state = self.state.get(&stream_key); + + if let Some(m) = stream_state { + let stream_state = m.lock().unwrap(); + + let start = Self::index_from_version(version); + let end = stream_state.position; + + return Ok(( + stream_state.events[start..end].to_vec(), + Self::Version::Exact(stream_state.position), + )); + } else { + return Ok((vec![], InMemoryStreamVersion::Exact(0))); + } + } + async fn append( + &mut self, + version: &Self::Version, + stream: &Self::StreamId, + events: &Vec, + ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> + where + 'a: 'async_trait, + E: 'async_trait, + { + let stream_key = self.get_stream(Some(stream)); + println!("Calling Stream {}", &stream_key); + + let stream_state = if let Some(s) = self.state.get(&stream_key) { + s.to_owned() + } else { + let n = Arc::new(Mutex::new(InMemoryEventRepositoryState::new())); + n + }; + + let mut stream_state = stream_state.lock().unwrap(); + + if stream_state.position == Self::index_from_version(version) { + stream_state.events.extend(events.clone()); + stream_state.position = stream_state.events.len(); + + Ok(( + events.to_owned(), + InMemoryStreamVersion::Exact(stream_state.events.len()), + )) + } else { + Err(StreamedVersionedInMemoryEventRepositoryError::VersionConflict) + } + } +} + +#[derive(Debug)] +pub enum StreamedVersionedInMemoryEventRepositoryError { + VersionConflict, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum InMemoryStreamVersion { + Any, + Exact(usize), +} + /// State /// pub struct InMemoryStateRepository { state: ::State, From 298fac986f6d40069e8b91dec1794d90d9785345 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 09:04:54 -0700 Subject: [PATCH 18/70] divide modules between state and event repositories --- src/decider/mod.rs | 7 +- src/decider/repository/esdb/mod.rs | 2 +- src/decider/repository/event.rs | 58 +++++ src/decider/repository/in_memory/mod.rs | 227 +----------------- src/decider/repository/in_memory/simple.rs | 92 +++++++ .../in_memory/versioned_with_streams/error.rs | 7 + .../in_memory/versioned_with_streams/mod.rs | 121 ++++++++++ src/decider/repository/mod.rs | 86 +------ src/decider/repository/state.rs | 22 ++ 9 files changed, 316 insertions(+), 306 deletions(-) create mode 100644 src/decider/repository/event.rs create mode 100644 src/decider/repository/in_memory/simple.rs create mode 100644 src/decider/repository/in_memory/versioned_with_streams/error.rs create mode 100644 src/decider/repository/in_memory/versioned_with_streams/mod.rs create mode 100644 src/decider/repository/state.rs diff --git a/src/decider/mod.rs b/src/decider/mod.rs index 640529d..324d751 100644 --- a/src/decider/mod.rs +++ b/src/decider/mod.rs @@ -22,7 +22,7 @@ mod tests { use assert_matches::assert_matches; use crate::{ - decider::repository::StateRepository, + decider::repository::state::StateRepository, test::{ deciders::user::{self, UserCommand, UserDecider, UserEvent}, ValueType, @@ -31,15 +31,14 @@ mod tests { use super::{ repository::{ - in_memory::{SimpleInMemoryEventRepository, InMemoryStateRepository}, - EventRepository, + in_memory::{simple::{InMemoryEventRepository, InMemoryStateRepository}}, event::EventRepository, }, *, }; #[actix_rt::test] async fn test_raw_decider() { - let event_repository: SimpleInMemoryEventRepository = SimpleInMemoryEventRepository::new(); + let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); diff --git a/src/decider/repository/esdb/mod.rs b/src/decider/repository/esdb/mod.rs index f2fb3ee..964237a 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/decider/repository/esdb/mod.rs @@ -12,7 +12,7 @@ use crate::decider::Event; use self::error::Error; -use super::VersionedEventRepositoryWithStreams; +use super::event::VersionedEventRepositoryWithStreams; pub mod error; diff --git a/src/decider/repository/event.rs b/src/decider/repository/event.rs new file mode 100644 index 0000000..7a90462 --- /dev/null +++ b/src/decider/repository/event.rs @@ -0,0 +1,58 @@ +use async_trait::async_trait; + +use crate::decider::Event; + +#[async_trait] +pub trait EventRepository +where + E: Event + Sync + Send, +{ + async fn load(&self) -> Result, Err>; + async fn append(&mut self, events: &Vec) -> Result, Err>; +} + +#[async_trait] +pub trait VersionedEventRepository +where + E: Event + Sync + Send, +{ + type Version: Eq; + + async fn load(&self) -> Result<(Vec, &Self::Version), Err>; + async fn load_from_version(&self) -> Result<(Vec, Option<&Self::Version>), Err>; + async fn append( + &mut self, + version: &Self::Version, + events: &Vec, + ) -> Result<(Vec, Self::Version), Err>; +} + +// Lifetimes added here to fix codegen issue with macro generated lifetimes - adding 'a and 'async_trait prevents +// compile errors about E not living long enough for fn append +// https://stackoverflow.com/questions/69560112/how-to-use-rust-async-trait-generic-to-a-lifetime-parameter +// https://github.com/dtolnay/async-trait/issues/8 +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f +#[async_trait] +pub trait VersionedEventRepositoryWithStreams<'a, E, Err> +where + E: Event + Sync + Send, +{ + type StreamId; + type Version: Eq; + + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Err>; + async fn load_from_version( + &self, + version: &Self::Version, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, Self::Version), Err>; + async fn append( + &mut self, + version: &Self::Version, + stream: &Self::StreamId, + events: &Vec, + ) -> Result<(Vec, Self::Version), Err> + where + 'a: 'async_trait, + E: 'async_trait; +} \ No newline at end of file diff --git a/src/decider/repository/in_memory/mod.rs b/src/decider/repository/in_memory/mod.rs index ff65a2b..fd731e0 100644 --- a/src/decider/repository/in_memory/mod.rs +++ b/src/decider/repository/in_memory/mod.rs @@ -1,229 +1,10 @@ -use std::{ - collections::HashMap, - fmt::Debug, - sync::{Arc, Mutex}, -}; +use std::fmt::Debug; -use async_trait::async_trait; - -use crate::decider::{Command, Event}; - -use super::{EventRepository, StateRepository, VersionedEventRepositoryWithStreams}; - -/// Events /// -#[derive(Default)] -pub struct SimpleInMemoryEventRepository -where - E: Event + Clone + Send + Sync, -{ - state: Arc>>, -} - -#[async_trait] -impl EventRepository for SimpleInMemoryEventRepository -where - E: Event + Clone + Send + Sync, -{ - async fn load(&self) -> Result, SimpleInMemoryError> { - let lock = self.state.lock().unwrap(); - Ok(lock.events.clone()) - } - - async fn append(&mut self, events: &Vec) -> Result, SimpleInMemoryError> { - let mut lock = self.state.lock().unwrap(); - lock.events.extend(events.to_owned()); - lock.position = lock.events.len(); - - Ok(events.clone()) - } -} - -impl SimpleInMemoryEventRepository -where - E: Event + Clone + Send + Sync, -{ - pub fn new() -> Self { - Self { - state: Arc::new(Mutex::new(InMemoryEventRepositoryState::new())), - } - } -} +pub mod simple; +pub mod versioned_with_streams; #[derive(Debug, Default)] -struct InMemoryEventRepositoryState { +pub(crate) struct InMemoryEventRepositoryState { events: Vec, position: usize, } - -impl InMemoryEventRepositoryState { - pub fn new() -> Self { - InMemoryEventRepositoryState { - events: vec![], - position: 0, - } - } -} - -#[derive(Debug)] -pub enum SimpleInMemoryError {} - -pub struct StreamedVersionedInMemoryEventRepository -where - E: Event + Sync + Send, -{ - stream_name: String, - state: HashMap>>>, -} - -impl<'a, E> StreamedVersionedInMemoryEventRepository -where - E: Event + Sync + Send, -{ - pub fn new(stream_name: &str) -> Self { - Self { - stream_name: stream_name.to_owned(), - state: HashMap::default(), - } - } - - fn get_stream(&self, stream_id: Option<&String>) -> String { - if let Some(id) = stream_id { - format!("{}/{}", self.stream_name, id) - } else { - format!("{}", self.stream_name) - } - } - - fn index_from_version(version: &InMemoryStreamVersion) -> usize { - match version { - InMemoryStreamVersion::Any => 0, - InMemoryStreamVersion::Exact(v) => *v, - } - } -} - -#[async_trait] -impl<'a, E> - VersionedEventRepositoryWithStreams<'a, E, StreamedVersionedInMemoryEventRepositoryError> - for StreamedVersionedInMemoryEventRepository -where - E: Event + Sync + Send + Clone, -{ - type StreamId = String; - type Version = InMemoryStreamVersion; - - async fn load( - &self, - id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> { - self.load_from_version(&InMemoryStreamVersion::Any, id) - .await - } - async fn load_from_version( - &self, - version: &Self::Version, - id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> { - let stream_key = self.get_stream(id); - println!("Calling Stream {}", &stream_key); - - let stream_state = self.state.get(&stream_key); - - if let Some(m) = stream_state { - let stream_state = m.lock().unwrap(); - - let start = Self::index_from_version(version); - let end = stream_state.position; - - return Ok(( - stream_state.events[start..end].to_vec(), - Self::Version::Exact(stream_state.position), - )); - } else { - return Ok((vec![], InMemoryStreamVersion::Exact(0))); - } - } - async fn append( - &mut self, - version: &Self::Version, - stream: &Self::StreamId, - events: &Vec, - ) -> Result<(Vec, Self::Version), StreamedVersionedInMemoryEventRepositoryError> - where - 'a: 'async_trait, - E: 'async_trait, - { - let stream_key = self.get_stream(Some(stream)); - println!("Calling Stream {}", &stream_key); - - let stream_state = if let Some(s) = self.state.get(&stream_key) { - s.to_owned() - } else { - let n = Arc::new(Mutex::new(InMemoryEventRepositoryState::new())); - n - }; - - let mut stream_state = stream_state.lock().unwrap(); - - if stream_state.position == Self::index_from_version(version) { - stream_state.events.extend(events.clone()); - stream_state.position = stream_state.events.len(); - - Ok(( - events.to_owned(), - InMemoryStreamVersion::Exact(stream_state.events.len()), - )) - } else { - Err(StreamedVersionedInMemoryEventRepositoryError::VersionConflict) - } - } -} - -#[derive(Debug)] -pub enum StreamedVersionedInMemoryEventRepositoryError { - VersionConflict, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum InMemoryStreamVersion { - Any, - Exact(usize), -} - -/// State /// -pub struct InMemoryStateRepository { - state: ::State, -} - -impl InMemoryStateRepository -where - C: Command + Send + Sync, - ::State: Default + Send + Sync + Debug, -{ - pub fn new() -> Self { - Self { - state: ::State::default(), - } - } -} - -#[async_trait] -impl StateRepository for InMemoryStateRepository -where - C: Command + Send + Sync, - ::State: Default + Send + Sync + Debug, -{ - async fn reify(&self) -> ::State { - self.state.clone() - } - - async fn save( - &mut self, - state: &::State, - ) -> Result<::State, SimpleInMemoryError> { - self.state = state.clone(); - Ok(self.state.to_owned()) - } -} - -pub enum InMemoryStateRepositoryError {} diff --git a/src/decider/repository/in_memory/simple.rs b/src/decider/repository/in_memory/simple.rs new file mode 100644 index 0000000..fa4278f --- /dev/null +++ b/src/decider/repository/in_memory/simple.rs @@ -0,0 +1,92 @@ +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; + +use async_trait::async_trait; + +use crate::decider::{ + repository::{event::EventRepository, state::StateRepository}, + Command, Event, +}; + +use super::InMemoryEventRepositoryState; + +#[derive(Default)] +pub struct InMemoryEventRepository +where + E: Event + Clone + Send + Sync, +{ + state: Arc>>, +} + +impl InMemoryEventRepository +where + E: Event + Clone + Send + Sync, +{ + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(InMemoryEventRepositoryState::new())), + } + } +} + +#[async_trait] +impl EventRepository for InMemoryEventRepository +where + E: Event + Clone + Send + Sync, +{ + async fn load(&self) -> Result, ()> { + let lock = self.state.lock().unwrap(); + Ok(lock.events.clone()) + } + + async fn append(&mut self, events: &Vec) -> Result, ()> { + let mut lock = self.state.lock().unwrap(); + lock.events.extend(events.to_owned()); + lock.position = lock.events.len(); + + Ok(events.clone()) + } +} + +impl InMemoryEventRepositoryState { + pub fn new() -> Self { + InMemoryEventRepositoryState { + events: vec![], + position: 0, + } + } +} + +pub struct InMemoryStateRepository { + state: ::State, +} + +impl InMemoryStateRepository +where + C: Command + Send + Sync, + ::State: Default + Send + Sync + Debug, +{ + pub fn new() -> Self { + Self { + state: ::State::default(), + } + } +} + +#[async_trait] +impl StateRepository for InMemoryStateRepository +where + C: Command + Send + Sync, + ::State: Default + Send + Sync + Debug, +{ + async fn reify(&self) -> ::State { + self.state.clone() + } + + async fn save(&mut self, state: &::State) -> Result<::State, ()> { + self.state = state.clone(); + Ok(self.state.to_owned()) + } +} diff --git a/src/decider/repository/in_memory/versioned_with_streams/error.rs b/src/decider/repository/in_memory/versioned_with_streams/error.rs new file mode 100644 index 0000000..166f473 --- /dev/null +++ b/src/decider/repository/in_memory/versioned_with_streams/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Cannot append, event version is out of date")] + VersionConflict, +} \ No newline at end of file diff --git a/src/decider/repository/in_memory/versioned_with_streams/mod.rs b/src/decider/repository/in_memory/versioned_with_streams/mod.rs new file mode 100644 index 0000000..dd5d31c --- /dev/null +++ b/src/decider/repository/in_memory/versioned_with_streams/mod.rs @@ -0,0 +1,121 @@ +use async_trait::async_trait; + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use crate::decider::{ + repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}, + Event, +}; + +use super::InMemoryEventRepositoryState; +use error::Error; + +pub mod error; + +pub struct InMemoryEventRepository +where + E: Event + Sync + Send, +{ + stream_name: String, + state: HashMap>>>, +} + +impl<'a, E> InMemoryEventRepository +where + E: Event + Sync + Send, +{ + pub fn new(stream_name: &str) -> Self { + Self { + stream_name: stream_name.to_owned(), + state: HashMap::default(), + } + } + + fn get_stream(&self, stream_id: Option<&String>) -> String { + if let Some(id) = stream_id { + format!("{}/{}", self.stream_name, id) + } else { + format!("{}", self.stream_name) + } + } + + fn index_from_version(version: &RepositoryVersion) -> usize { + match version { + RepositoryVersion::Exact(v) => *v, + _ => 0, + } + } +} + +#[async_trait] +impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> + for InMemoryEventRepository +where + E: Event + Sync + Send + Clone, +{ + type StreamId = String; + type Version = RepositoryVersion; + + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Error> { + self.load_from_version(&RepositoryVersion::Any, id).await + } + async fn load_from_version( + &self, + version: &Self::Version, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, Self::Version), Error> { + let stream_key = self.get_stream(id); + println!("Calling Stream {}", &stream_key); + + if let Some(m) = self.state.get(&stream_key) { + let stream_state = m.lock().unwrap(); + + let start = Self::index_from_version(version); + let end = stream_state.position; + + return Ok(( + stream_state.events[start..end].to_vec(), + Self::Version::Exact(stream_state.position), + )); + } else { + return Ok((vec![], RepositoryVersion::NoStream)); + } + } + async fn append( + &mut self, + version: &Self::Version, + stream: &Self::StreamId, + events: &Vec, + ) -> Result<(Vec, Self::Version), Error> + where + 'a: 'async_trait, + E: 'async_trait, + { + let stream_key = self.get_stream(Some(stream)); + println!("Calling Stream {}", &stream_key); + + let stream_state = if let Some(s) = self.state.get(&stream_key) { + s.to_owned() + } else { + let n = Arc::new(Mutex::new(InMemoryEventRepositoryState::new())); + n + }; + + let mut stream_state = stream_state.lock().unwrap(); + + if stream_state.position == Self::index_from_version(version) { + stream_state.events.extend(events.clone()); + stream_state.position = stream_state.events.len(); + + Ok(( + events.to_owned(), + RepositoryVersion::Exact(stream_state.events.len()), + )) + } else { + Err(Error::VersionConflict) + } + } +} diff --git a/src/decider/repository/mod.rs b/src/decider/repository/mod.rs index dced3f2..04bf352 100644 --- a/src/decider/repository/mod.rs +++ b/src/decider/repository/mod.rs @@ -1,83 +1,13 @@ -use async_trait::async_trait; - -use super::{Command, Event}; - +pub mod event; +pub mod state; #[cfg(feature = "esdb")] pub mod esdb; #[cfg(feature = "in_memory")] pub mod in_memory; -// Event Repositories -#[async_trait] -pub trait EventRepository -where - E: Event + Sync + Send, -{ - async fn load(&self) -> Result, Err>; - async fn append(&mut self, events: &Vec) -> Result, Err>; -} - -#[async_trait] -pub trait VersionedEventRepository -where - E: Event + Sync + Send, -{ - type Version: Eq; - - async fn load(&self) -> Result<(Vec, &Self::Version), Err>; - async fn load_from_version(&self) -> Result<(Vec, Option<&Self::Version>), Err>; - async fn append( - &mut self, - version: &Self::Version, - events: &Vec, - ) -> Result<(Vec, Self::Version), Err>; -} - -// Lifetimes added here to fix codegen issue with macro generated lifetimes - adding 'a and 'async_trait prevents -// compile errors about E not living long enough for fn append -// https://stackoverflow.com/questions/69560112/how-to-use-rust-async-trait-generic-to-a-lifetime-parameter -// https://github.com/dtolnay/async-trait/issues/8 -// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f -#[async_trait] -pub trait VersionedEventRepositoryWithStreams<'a, E, Err> -where - E: Event + Sync + Send, -{ - type StreamId; - type Version: Eq; - - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Err>; - async fn load_from_version( - &self, - version: &Self::Version, - id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), Err>; - async fn append( - &mut self, - version: &Self::Version, - stream: &Self::StreamId, - events: &Vec, - ) -> Result<(Vec, Self::Version), Err> - where - 'a: 'async_trait, - E: 'async_trait; -} - -// State Repositories -#[async_trait] -pub trait StateRepository { - async fn reify(&self) -> ::State; - async fn save(&mut self, state: &::State) -> Result<::State, Err>; -} - -#[async_trait] -pub trait VersionedStateRepository { - type Version: Eq; - - async fn reify(&self) -> Result<(::State, Self::Version), Err>; - async fn save( - &mut self, - version: &Self::Version, - state: &::State, - ) -> Result<::State, Err>; -} +#[derive(Debug, PartialEq, Eq)] +pub enum RepositoryVersion { + Any, + Exact(usize), + NoStream +} \ No newline at end of file diff --git a/src/decider/repository/state.rs b/src/decider/repository/state.rs new file mode 100644 index 0000000..7e1babf --- /dev/null +++ b/src/decider/repository/state.rs @@ -0,0 +1,22 @@ +use async_trait::async_trait; + +use crate::decider::Command; + +#[async_trait] +pub trait StateRepository { + async fn reify(&self) -> ::State; + async fn save(&mut self, state: &::State) -> Result<::State, Err>; +} + +#[async_trait] +pub trait VersionedStateRepository { + type Version: Eq; + + async fn reify(&self) -> Result<(::State, Self::Version), Err>; + async fn save( + &mut self, + version: &Self::Version, + state: &::State, + ) -> Result<::State, Err>; +} + From 10c5b4d2c52874a713d2035a3d1e9b8cf1cb6cc6 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 09:21:01 -0700 Subject: [PATCH 19/70] massive reorganization - decider and repository as base crate modules --- src/{decider/mod.rs => decider.rs} | 18 ++++++++---------- src/lib.rs | 3 ++- src/{decider => }/repository/esdb/error.rs | 0 src/{decider => }/repository/esdb/mod.rs | 2 +- src/{decider => }/repository/event.rs | 0 src/{decider => }/repository/in_memory/mod.rs | 0 .../repository/in_memory/simple.rs | 6 ++---- .../in_memory/versioned_with_streams/error.rs | 0 .../in_memory/versioned_with_streams/mod.rs | 7 +++---- src/{decider => }/repository/mod.rs | 0 src/{decider => }/repository/state.rs | 0 src/{test => test_helpers}/deciders.rs | 2 +- src/{test => test_helpers}/mod.rs | 0 13 files changed, 17 insertions(+), 21 deletions(-) rename src/{decider/mod.rs => decider.rs} (87%) rename src/{decider => }/repository/esdb/error.rs (100%) rename src/{decider => }/repository/esdb/mod.rs (98%) rename src/{decider => }/repository/event.rs (100%) rename src/{decider => }/repository/in_memory/mod.rs (100%) rename src/{decider => }/repository/in_memory/simple.rs (94%) rename src/{decider => }/repository/in_memory/versioned_with_streams/error.rs (100%) rename src/{decider => }/repository/in_memory/versioned_with_streams/mod.rs (96%) rename src/{decider => }/repository/mod.rs (100%) rename src/{decider => }/repository/state.rs (100%) rename src/{test => test_helpers}/deciders.rs (99%) rename src/{test => test_helpers}/mod.rs (100%) diff --git a/src/decider/mod.rs b/src/decider.rs similarity index 87% rename from src/decider/mod.rs rename to src/decider.rs index 324d751..1299b42 100644 --- a/src/decider/mod.rs +++ b/src/decider.rs @@ -1,7 +1,5 @@ use async_trait::async_trait; -pub mod repository; - pub trait Command { type State: Clone + Send + Sync; } @@ -22,19 +20,19 @@ mod tests { use assert_matches::assert_matches; use crate::{ - decider::repository::state::StateRepository, - test::{ + repository::state::StateRepository, + test_helpers::{ deciders::user::{self, UserCommand, UserDecider, UserEvent}, ValueType, }, }; - use super::{ - repository::{ - in_memory::{simple::{InMemoryEventRepository, InMemoryStateRepository}}, event::EventRepository, - }, - *, - }; + use crate::repository::{ + event::EventRepository, + in_memory::simple::{InMemoryEventRepository, InMemoryStateRepository}, + }; + + use super::*; #[actix_rt::test] async fn test_raw_decider() { diff --git a/src/lib.rs b/src/lib.rs index 3aa9a2e..9ac1887 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod decider; +pub mod repository; #[cfg(test)] -mod test; \ No newline at end of file +mod test_helpers; \ No newline at end of file diff --git a/src/decider/repository/esdb/error.rs b/src/repository/esdb/error.rs similarity index 100% rename from src/decider/repository/esdb/error.rs rename to src/repository/esdb/error.rs diff --git a/src/decider/repository/esdb/mod.rs b/src/repository/esdb/mod.rs similarity index 98% rename from src/decider/repository/esdb/mod.rs rename to src/repository/esdb/mod.rs index 964237a..d9e9348 100644 --- a/src/decider/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -149,7 +149,7 @@ mod tests { use assert_matches::assert_matches; use eventstore::DeleteStreamOptions; - use crate::test::deciders::user::{User, UserEvent, UserId, UserName}; + use crate::test_helpers::deciders::user::{User, UserEvent, UserId, UserName}; use super::*; diff --git a/src/decider/repository/event.rs b/src/repository/event.rs similarity index 100% rename from src/decider/repository/event.rs rename to src/repository/event.rs diff --git a/src/decider/repository/in_memory/mod.rs b/src/repository/in_memory/mod.rs similarity index 100% rename from src/decider/repository/in_memory/mod.rs rename to src/repository/in_memory/mod.rs diff --git a/src/decider/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs similarity index 94% rename from src/decider/repository/in_memory/simple.rs rename to src/repository/in_memory/simple.rs index fa4278f..ba05500 100644 --- a/src/decider/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -5,10 +5,8 @@ use std::{ use async_trait::async_trait; -use crate::decider::{ - repository::{event::EventRepository, state::StateRepository}, - Command, Event, -}; +use crate::repository::{event::EventRepository, state::StateRepository}; +use crate::decider::{Command, Event,}; use super::InMemoryEventRepositoryState; diff --git a/src/decider/repository/in_memory/versioned_with_streams/error.rs b/src/repository/in_memory/versioned_with_streams/error.rs similarity index 100% rename from src/decider/repository/in_memory/versioned_with_streams/error.rs rename to src/repository/in_memory/versioned_with_streams/error.rs diff --git a/src/decider/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs similarity index 96% rename from src/decider/repository/in_memory/versioned_with_streams/mod.rs rename to src/repository/in_memory/versioned_with_streams/mod.rs index dd5d31c..8ed5509 100644 --- a/src/decider/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -5,10 +5,9 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::decider::{ - repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}, - Event, -}; +use crate::repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}; + +use crate::decider::Event; use super::InMemoryEventRepositoryState; use error::Error; diff --git a/src/decider/repository/mod.rs b/src/repository/mod.rs similarity index 100% rename from src/decider/repository/mod.rs rename to src/repository/mod.rs diff --git a/src/decider/repository/state.rs b/src/repository/state.rs similarity index 100% rename from src/decider/repository/state.rs rename to src/repository/state.rs diff --git a/src/test/deciders.rs b/src/test_helpers/deciders.rs similarity index 99% rename from src/test/deciders.rs rename to src/test_helpers/deciders.rs index 2a82dcb..643fda7 100644 --- a/src/test/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -6,7 +6,7 @@ pub(crate) mod user { use crate::{ decider::{Command, Decider, Event}, - test::ValueType, + test_helpers::ValueType, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/test/mod.rs b/src/test_helpers/mod.rs similarity index 100% rename from src/test/mod.rs rename to src/test_helpers/mod.rs From e0752b60fdd8b583d355132b27f4cc6a15a0ecac Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 09:27:11 -0700 Subject: [PATCH 20/70] import cleanup --- src/decider.rs | 11 +++++------ src/repository/esdb/mod.rs | 5 ++++- src/repository/in_memory/simple.rs | 6 ++++-- .../in_memory/versioned_with_streams/mod.rs | 7 ++++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 1299b42..61b6343 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -20,18 +20,17 @@ mod tests { use assert_matches::assert_matches; use crate::{ - repository::state::StateRepository, + repository::{ + event::EventRepository, + in_memory::simple::{InMemoryEventRepository, InMemoryStateRepository}, + state::StateRepository, + }, test_helpers::{ deciders::user::{self, UserCommand, UserDecider, UserEvent}, ValueType, }, }; - use crate::repository::{ - event::EventRepository, - in_memory::simple::{InMemoryEventRepository, InMemoryStateRepository}, - }; - use super::*; #[actix_rt::test] diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index d9e9348..89b2fdd 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -137,7 +137,10 @@ where .await .map_err(|e| Error::WriteStream(stream.to_owned(), e))?; - Ok((events.to_owned(), ExpectedRevision::Exact(res.next_expected_version))) + Ok(( + events.to_owned(), + ExpectedRevision::Exact(res.next_expected_version), + )) } } diff --git a/src/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs index ba05500..fb60396 100644 --- a/src/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -5,8 +5,10 @@ use std::{ use async_trait::async_trait; -use crate::repository::{event::EventRepository, state::StateRepository}; -use crate::decider::{Command, Event,}; +use crate::{ + decider::{Command, Event}, + repository::{event::EventRepository, state::StateRepository}, +}; use super::InMemoryEventRepositoryState; diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index 8ed5509..21a415e 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -5,9 +5,10 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}; - -use crate::decider::Event; +use crate::{ + repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}, + decider::Event, +}; use super::InMemoryEventRepositoryState; use error::Error; From 345a79f0d0e0dd3df28234f565ae276a7216a949 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 10:57:02 -0700 Subject: [PATCH 21/70] move ESDB tests to generic test_helper for all event repository implementation specs --- src/repository/esdb/mod.rs | 134 +++++------------- src/repository/event.rs | 15 +- .../in_memory/versioned_with_streams/mod.rs | 18 ++- src/repository/mod.rs | 3 +- src/test_helpers/mod.rs | 1 + src/test_helpers/repository.rs | 94 ++++++++++++ 6 files changed, 150 insertions(+), 115 deletions(-) create mode 100644 src/test_helpers/repository.rs diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 89b2fdd..8a9716d 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -12,7 +12,7 @@ use crate::decider::Event; use self::error::Error; -use super::event::VersionedEventRepositoryWithStreams; +use super::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}; pub mod error; @@ -39,13 +39,22 @@ impl<'a, E> ESDBEventRepository { } } - fn expected_revision_to_position(er: &ExpectedRevision) -> StreamPosition { - if let ExpectedRevision::Exact(u) = er { - StreamPosition::Position(u.to_owned()) + fn version_to_esdb_position(version: &RepositoryVersion) -> StreamPosition { + if let RepositoryVersion::Exact(u) = version { + StreamPosition::Position(u.to_owned().try_into().unwrap()) } else { StreamPosition::Start } } + + fn version_to_expected_revision(version: &RepositoryVersion) -> ExpectedRevision { + match version { + RepositoryVersion::Exact(u) => { + ExpectedRevision::Exact(u.to_owned().try_into().unwrap()) + } + _ => ExpectedRevision::Any, + } + } } #[async_trait] @@ -54,17 +63,19 @@ where E: Event + Sync + Send + Serialize + DeserializeOwned + Clone + Debug, { type StreamId = String; - type Version = ExpectedRevision; - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Error> { - self.load_from_version(&ExpectedRevision::Any, id).await + async fn load( + &self, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, RepositoryVersion), Error> { + self.load_from_version(&RepositoryVersion::Any, id).await } async fn load_from_version( &self, - version: &Self::Version, + version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), Error> { + ) -> Result<(Vec, RepositoryVersion), Error> { println!("Calling Stream {}", self.get_stream(id)); let mut stream = self .client @@ -72,7 +83,7 @@ where self.get_stream(id), &ReadStreamOptions::default() .resolve_link_tos() - .position(Self::expected_revision_to_position(&version)), + .position(Self::version_to_esdb_position(&version)), ) .await .map_err(Error::ESDBGeneral)?; @@ -84,17 +95,17 @@ where Ok(Some(event)) => evts.push(event), Ok(None) => break, Err(eventstore::Error::ResourceNotFound) => { - return Ok((vec![], ExpectedRevision::NoStream)) + return Ok((vec![], RepositoryVersion::NoStream)) } Err(e) => return Err(Error::ReadStream(e)), } } let mut rv = vec![]; - let mut pos = ExpectedRevision::StreamExists; + let mut pos = RepositoryVersion::StreamExists; for ev in evts { - pos = ExpectedRevision::Exact(ev.get_original_event().revision); + pos = RepositoryVersion::Exact(ev.get_original_event().revision.try_into().unwrap()); if let Some(event_data) = ev.event { // Continue on deser failure - occasionally you'll get delete and other system types in the stream @@ -109,10 +120,10 @@ where async fn append( &mut self, - version: &Self::Version, + version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, Self::Version), Error> + ) -> Result<(Vec, RepositoryVersion), Error> where 'a: 'async_trait, E: 'async_trait, @@ -131,7 +142,8 @@ where .client .append_to_stream( self.get_stream(Some(stream)), - &AppendToStreamOptions::default().expected_revision(*version), + &AppendToStreamOptions::default() + .expected_revision(Self::version_to_expected_revision(version)), perpared_events, ) .await @@ -139,20 +151,19 @@ where Ok(( events.to_owned(), - ExpectedRevision::Exact(res.next_expected_version), + RepositoryVersion::Exact(res.next_expected_version.try_into().unwrap()), )) } } #[cfg(test)] mod tests { - use core::time; - use std::thread; - - use assert_matches::assert_matches; use eventstore::DeleteStreamOptions; - use crate::test_helpers::deciders::user::{User, UserEvent, UserId, UserName}; + use crate::test_helpers::{ + deciders::user::UserEvent, + repository::test_versioned_event_repository_with_streams, + }; use super::*; @@ -180,83 +191,10 @@ mod tests { } #[actix_rt::test] - async fn test_storage() { + async fn repository_spec_test() { let client = store_from_environment(vec![1, 2]).await; - let id_1 = "1".to_string(); - let id_2 = "2".to_string(); - - let mut event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); - - let res: (Vec, ExpectedRevision) = - event_repository.load(None).await.expect("loaded"); - assert_matches!(res, (v, _) if v == vec![] as Vec); - - let events1 = vec![ - UserEvent::UserAdded(User { - id: 1, - name: UserName::try_from("Mike").expect("Name is valid"), - }), - UserEvent::UserNameUpdated( - 1 as UserId, - UserName::try_from("Mike2").expect("Name is valid"), - ), - ]; - - let _ = event_repository - .append(&ExpectedRevision::Any, &id_1, &events1) - .await; - - let events2 = vec![ - UserEvent::UserAdded(User { - id: 2, - name: UserName::try_from("Stella").expect("Name is valid"), - }), - UserEvent::UserNameUpdated( - 1 as UserId, - UserName::try_from("Stella2").expect("Name is valid"), - ), - ]; - - let _ = event_repository - .append(&ExpectedRevision::Any, &id_2, &events2) - .await; - - // Crude but we need to wait for ESDB to catch up its "Categories" auto projection - thread::sleep(time::Duration::from_secs(1)); - - let res = event_repository.load(Some(&id_1)).await; - assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events1); - - let res = event_repository.load(Some(&id_2)).await; - assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events2); - - let res = event_repository.load(None).await; - - let events_combined: Vec = - events1.into_iter().chain(events2.into_iter()).collect(); - assert_matches!(res, Ok((v, ExpectedRevision::Exact(_))) if v == events_combined); - - let res = event_repository.load(Some(&id_1)).await; - let version = res.unwrap().1; - - let new_events = vec![UserEvent::UserNameUpdated( - 1, - UserName::try_from("Mike").expect("Name is valid"), - )]; - - let res = event_repository - .append(&version, &id_1, &new_events) - .await - .expect("Success"); - - let version = res.1; - - let (latest_events, _) = event_repository - .load_from_version(&version, Some(&id_1)) - .await - .expect("load success"); - - assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); + let event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); + let _ = test_versioned_event_repository_with_streams(event_repository).await; } } diff --git a/src/repository/event.rs b/src/repository/event.rs index 7a90462..c00be29 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -1,6 +1,9 @@ +use std::fmt::Debug; + use async_trait::async_trait; use crate::decider::Event; +use super::RepositoryVersion; #[async_trait] pub trait EventRepository @@ -36,22 +39,22 @@ where pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where E: Event + Sync + Send, + Err: Debug { type StreamId; - type Version: Eq; - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Err>; + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, RepositoryVersion), Err>; async fn load_from_version( &self, - version: &Self::Version, + version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), Err>; + ) -> Result<(Vec, RepositoryVersion), Err>; async fn append( &mut self, - version: &Self::Version, + version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, Self::Version), Err> + ) -> Result<(Vec, RepositoryVersion), Err> where 'a: 'async_trait, E: 'async_trait; diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index 21a415e..b2c3085 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -6,8 +6,8 @@ use std::{ }; use crate::{ - repository::{RepositoryVersion, event::VersionedEventRepositoryWithStreams}, decider::Event, + repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}, }; use super::InMemoryEventRepositoryState; @@ -51,22 +51,20 @@ where } #[async_trait] -impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> - for InMemoryEventRepository +impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> for InMemoryEventRepository where E: Event + Sync + Send + Clone, { type StreamId = String; - type Version = RepositoryVersion; - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, Self::Version), Error> { + async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, RepositoryVersion), Error> { self.load_from_version(&RepositoryVersion::Any, id).await } async fn load_from_version( &self, - version: &Self::Version, + version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, Self::Version), Error> { + ) -> Result<(Vec, RepositoryVersion), Error> { let stream_key = self.get_stream(id); println!("Calling Stream {}", &stream_key); @@ -78,7 +76,7 @@ where return Ok(( stream_state.events[start..end].to_vec(), - Self::Version::Exact(stream_state.position), + RepositoryVersion::Exact(stream_state.position), )); } else { return Ok((vec![], RepositoryVersion::NoStream)); @@ -86,10 +84,10 @@ where } async fn append( &mut self, - version: &Self::Version, + version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, Self::Version), Error> + ) -> Result<(Vec, RepositoryVersion), Error> where 'a: 'async_trait, E: 'async_trait, diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 04bf352..b2aff72 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -9,5 +9,6 @@ pub mod in_memory; pub enum RepositoryVersion { Any, Exact(usize), - NoStream + NoStream, + StreamExists } \ No newline at end of file diff --git a/src/test_helpers/mod.rs b/src/test_helpers/mod.rs index 48ab662..5b503f1 100644 --- a/src/test_helpers/mod.rs +++ b/src/test_helpers/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod deciders; +pub(crate) mod repository; pub(crate) trait ValueType { fn value(&self) -> T; diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs new file mode 100644 index 0000000..a04d63c --- /dev/null +++ b/src/test_helpers/repository.rs @@ -0,0 +1,94 @@ +use core::time; +use std::{fmt::Debug, thread}; + +use assert_matches::assert_matches; + +use crate::{ + repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}, + test_helpers::deciders::user::{User, UserId, UserName}, +}; + +use super::deciders::user::UserEvent; + +pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug>( + mut event_repository: impl VersionedEventRepositoryWithStreams< + 'a, + UserEvent, + Err, + StreamId = String, + >, +) { + println!("RUNNING UNIVERSAL SPEC TEST FOR VersionedEventRepositoryWithStreams"); + let id_1 = "1".to_string(); + let id_2 = "2".to_string(); + + let res: (Vec, RepositoryVersion) = + event_repository.load(None).await.expect("loaded"); + assert_matches!(res, (v, _) if v == vec![] as Vec); + + let events1 = vec![ + UserEvent::UserAdded(User { + id: 1, + name: UserName::try_from("Mike").expect("Name is valid"), + }), + UserEvent::UserNameUpdated( + 1 as UserId, + UserName::try_from("Mike2").expect("Name is valid"), + ), + ]; + + let _ = event_repository + .append(&RepositoryVersion::Any, &id_1, &events1) + .await; + + let events2 = vec![ + UserEvent::UserAdded(User { + id: 2, + name: UserName::try_from("Stella").expect("Name is valid"), + }), + UserEvent::UserNameUpdated( + 1 as UserId, + UserName::try_from("Stella2").expect("Name is valid"), + ), + ]; + + let _ = event_repository + .append(&RepositoryVersion::Any, &id_2, &events2) + .await; + + // Crude but we need to wait for ESDB to catch up its "Categories" auto projection + thread::sleep(time::Duration::from_secs(1)); + + let res = event_repository.load(Some(&id_1)).await; + assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events1); + + let res = event_repository.load(Some(&id_2)).await; + assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events2); + + let res = event_repository.load(None).await; + + let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); + assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events_combined); + + let res = event_repository.load(Some(&id_1)).await; + let version = res.unwrap().1; + + let new_events = vec![UserEvent::UserNameUpdated( + 1, + UserName::try_from("Mike").expect("Name is valid"), + )]; + + let res = event_repository + .append(&version, &id_1, &new_events) + .await + .expect("Success"); + + let version = res.1; + + let (latest_events, _) = event_repository + .load_from_version(&version, Some(&id_1)) + .await + .expect("load success"); + + assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); +} From 528c3066301df059ad2d1927b135528f28e76e4b Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 12:28:56 -0700 Subject: [PATCH 22/70] in memory event store version slices bugfixes --- src/repository/event.rs | 4 +- .../in_memory/versioned_with_streams/mod.rs | 73 +++++++++++++------ src/test_helpers/repository.rs | 10 ++- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/repository/event.rs b/src/repository/event.rs index c00be29..bf0453d 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -17,7 +17,7 @@ where #[async_trait] pub trait VersionedEventRepository where - E: Event + Sync + Send, + E: Event + Sync + Send + Debug, { type Version: Eq; @@ -38,7 +38,7 @@ where #[async_trait] pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where - E: Event + Sync + Send, + E: Event + Sync + Send + Debug, Err: Debug { type StreamId; diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index b2c3085..686b598 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use std::{ collections::HashMap, - sync::{Arc, Mutex}, + sync::{Arc, Mutex}, fmt::Debug, }; use crate::{ @@ -17,7 +17,7 @@ pub mod error; pub struct InMemoryEventRepository where - E: Event + Sync + Send, + E: Event + Sync + Send + Debug, { stream_name: String, state: HashMap>>>, @@ -25,7 +25,7 @@ where impl<'a, E> InMemoryEventRepository where - E: Event + Sync + Send, + E: Event + Sync + Send + Debug, { pub fn new(stream_name: &str) -> Self { Self { @@ -34,7 +34,7 @@ where } } - fn get_stream(&self, stream_id: Option<&String>) -> String { + fn get_stream_key(&self, stream_id: Option<&String>) -> String { if let Some(id) = stream_id { format!("{}/{}", self.stream_name, id) } else { @@ -42,6 +42,17 @@ where } } + fn get_stream_or_new(&mut self, key: &str) -> &Arc>> { + if let None = self.state.get(key) { + self.state.insert( + key.to_owned(), + Arc::new(Mutex::new(InMemoryEventRepositoryState::new())), + ); + } + + self.state.get(key).unwrap() + } + fn index_from_version(version: &RepositoryVersion) -> usize { match version { RepositoryVersion::Exact(v) => *v, @@ -53,11 +64,14 @@ where #[async_trait] impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> for InMemoryEventRepository where - E: Event + Sync + Send + Clone, + E: Event + Sync + Send + Clone + Debug, { type StreamId = String; - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, RepositoryVersion), Error> { + async fn load( + &self, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, RepositoryVersion), Error> { self.load_from_version(&RepositoryVersion::Any, id).await } async fn load_from_version( @@ -65,21 +79,21 @@ where version: &RepositoryVersion, id: Option<&Self::StreamId>, ) -> Result<(Vec, RepositoryVersion), Error> { - let stream_key = self.get_stream(id); + let stream_key = self.get_stream_key(id); println!("Calling Stream {}", &stream_key); if let Some(m) = self.state.get(&stream_key) { let stream_state = m.lock().unwrap(); let start = Self::index_from_version(version); - let end = stream_state.position; - + let end = stream_state.position + 1; + return Ok(( stream_state.events[start..end].to_vec(), RepositoryVersion::Exact(stream_state.position), )); } else { - return Ok((vec![], RepositoryVersion::NoStream)); + return Ok((vec![], RepositoryVersion::Exact(0))); } } async fn append( @@ -92,28 +106,43 @@ where 'a: 'async_trait, E: 'async_trait, { - let stream_key = self.get_stream(Some(stream)); + let stream_key = self.get_stream_key(Some(stream)); println!("Calling Stream {}", &stream_key); - let stream_state = if let Some(s) = self.state.get(&stream_key) { - s.to_owned() - } else { - let n = Arc::new(Mutex::new(InMemoryEventRepositoryState::new())); - n - }; + let mut stream = self.get_stream_or_new(&stream_key).lock().unwrap(); + + if stream.position == Self::index_from_version(version) { + stream.events.extend(events.clone()); + let position = stream.events.len() - 1; + stream.position = position.clone(); - let mut stream_state = stream_state.lock().unwrap(); + drop(stream); - if stream_state.position == Self::index_from_version(version) { - stream_state.events.extend(events.clone()); - stream_state.position = stream_state.events.len(); + let stream = self.get_stream_or_new(&stream_key).lock().unwrap(); Ok(( events.to_owned(), - RepositoryVersion::Exact(stream_state.events.len()), + RepositoryVersion::Exact(position), )) } else { Err(Error::VersionConflict) } } } + +#[cfg(test)] +mod tests { + use crate::test_helpers::{ + deciders::user::UserEvent, repository::test_versioned_event_repository_with_streams, + }; + + use super::InMemoryEventRepository; + + const BASE_STREAM: &str = "test"; + + #[actix_rt::test] + async fn repository_spec_test() { + let event_repository = InMemoryEventRepository::::new(BASE_STREAM); + let _ = test_versioned_event_repository_with_streams(event_repository).await; + } +} diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index a04d63c..eb79b36 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -39,7 +39,8 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> let _ = event_repository .append(&RepositoryVersion::Any, &id_1, &events1) - .await; + .await + .expect("Successful append"); let events2 = vec![ UserEvent::UserAdded(User { @@ -54,7 +55,8 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> let _ = event_repository .append(&RepositoryVersion::Any, &id_2, &events2) - .await; + .await + .expect("Successful append"); // Crude but we need to wait for ESDB to catch up its "Categories" auto projection thread::sleep(time::Duration::from_secs(1)); @@ -67,8 +69,8 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> let res = event_repository.load(None).await; - let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); - assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events_combined); + // let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); + // assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events_combined); let res = event_repository.load(Some(&id_1)).await; let version = res.unwrap().1; From 4e6839f9a17ee08dc8a0ffdb17ca06a740ef1883 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 28 Dec 2022 13:04:48 -0700 Subject: [PATCH 23/70] fixes general stream in fine grained stream memory event repository --- .../in_memory/versioned_with_streams/mod.rs | 25 +++++++++++++------ src/test_helpers/repository.rs | 4 +-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index 686b598..413af6a 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -2,7 +2,8 @@ use async_trait::async_trait; use std::{ collections::HashMap, - sync::{Arc, Mutex}, fmt::Debug, + fmt::Debug, + sync::{Arc, Mutex}, }; use crate::{ @@ -34,6 +35,10 @@ where } } + fn get_base_stream_key(&self) -> String { + self.stream_name.to_owned() + } + fn get_stream_key(&self, stream_id: Option<&String>) -> String { if let Some(id) = stream_id { format!("{}/{}", self.stream_name, id) @@ -87,7 +92,7 @@ where let start = Self::index_from_version(version); let end = stream_state.position + 1; - + return Ok(( stream_state.events[start..end].to_vec(), RepositoryVersion::Exact(stream_state.position), @@ -107,6 +112,7 @@ where E: 'async_trait, { let stream_key = self.get_stream_key(Some(stream)); + println!("Calling Stream {}", &stream_key); let mut stream = self.get_stream_or_new(&stream_key).lock().unwrap(); @@ -116,14 +122,17 @@ where let position = stream.events.len() - 1; stream.position = position.clone(); - drop(stream); + drop(stream); // Drop mutable reference so we can pull another and write to sub_stream - let stream = self.get_stream_or_new(&stream_key).lock().unwrap(); + let mut sub_stream = self + .get_stream_or_new(&self.get_base_stream_key()) + .lock() + .unwrap(); + sub_stream.events.extend(events.clone()); + let sub_position = sub_stream.events.len() - 1; + sub_stream.position = sub_position; - Ok(( - events.to_owned(), - RepositoryVersion::Exact(position), - )) + Ok((events.to_owned(), RepositoryVersion::Exact(position))) } else { Err(Error::VersionConflict) } diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index eb79b36..082e5df 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -69,8 +69,8 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> let res = event_repository.load(None).await; - // let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); - // assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events_combined); + let events_combined: Vec = events1.into_iter().chain(events2.into_iter()).collect(); + assert_matches!(res, Ok((v, RepositoryVersion::Exact(_))) if v == events_combined); let res = event_repository.load(Some(&id_1)).await; let version = res.unwrap().1; From 167ffadeadd66a1db921480b131c3e99ff032eb1 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 29 Dec 2022 12:17:58 -0700 Subject: [PATCH 24/70] split repository naming and paths, optional deps by feature, add in memory state repository with versions --- Cargo.toml | 8 +- src/repository/esdb/mod.rs | 6 +- src/repository/event.rs | 2 +- src/repository/in_memory/mod.rs | 1 + src/repository/in_memory/state/mod.rs | 1 + src/repository/in_memory/state/versioned.rs | 120 ++++++++++++++++++ .../in_memory/versioned_with_streams/mod.rs | 3 - src/repository/mod.rs | 2 +- src/repository/state.rs | 1 - src/test_helpers/repository.rs | 10 +- 10 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 src/repository/in_memory/state/mod.rs create mode 100644 src/repository/in_memory/state/versioned.rs diff --git a/Cargo.toml b/Cargo.toml index be28ea4..77a8f6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,17 @@ edition = "2021" [features] default = ["in_memory", "esdb"] in_memory = [] -esdb = [] +esdb = ["dep:eventstore", "dep:uuid_0_8_2", "dep:serde_json"] [dependencies] async-trait = "0.1.53" -eventstore = "2.1.1" +eventstore = { version = "2.1.1", optional = true } serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.81" +serde_json = { version = "1.0.81", optional = true } thiserror = "1.0" # Use 0.8.2 only here because of a weird deps bug with eventstore # 1.x uuids are not compatible with eventstore internal uuids -uuid = { version = "0.8.2", features = ["v4", "serde"] } +uuid_0_8_2 = { package = "uuid", version = "0.8.2", features = ["v4", "serde"], optional = true } [dev-dependencies] actix-rt = "2.7.0" diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 8a9716d..0a654f8 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -6,7 +6,7 @@ use eventstore::{ StreamPosition, }; use serde::{de::DeserializeOwned, Serialize}; -use uuid::Uuid; +use uuid_0_8_2::Uuid; use crate::decider::Event; @@ -76,7 +76,6 @@ where version: &RepositoryVersion, id: Option<&Self::StreamId>, ) -> Result<(Vec, RepositoryVersion), Error> { - println!("Calling Stream {}", self.get_stream(id)); let mut stream = self .client .read_stream( @@ -161,8 +160,7 @@ mod tests { use eventstore::DeleteStreamOptions; use crate::test_helpers::{ - deciders::user::UserEvent, - repository::test_versioned_event_repository_with_streams, + deciders::user::UserEvent, repository::test_versioned_event_repository_with_streams, }; use super::*; diff --git a/src/repository/event.rs b/src/repository/event.rs index bf0453d..3eba59a 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -58,4 +58,4 @@ where where 'a: 'async_trait, E: 'async_trait; -} \ No newline at end of file +} diff --git a/src/repository/in_memory/mod.rs b/src/repository/in_memory/mod.rs index fd731e0..39844e7 100644 --- a/src/repository/in_memory/mod.rs +++ b/src/repository/in_memory/mod.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; pub mod simple; +pub mod state; pub mod versioned_with_streams; #[derive(Debug, Default)] diff --git a/src/repository/in_memory/state/mod.rs b/src/repository/in_memory/state/mod.rs new file mode 100644 index 0000000..0334bbf --- /dev/null +++ b/src/repository/in_memory/state/mod.rs @@ -0,0 +1 @@ +pub mod versioned; \ No newline at end of file diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs new file mode 100644 index 0000000..b7d05a0 --- /dev/null +++ b/src/repository/in_memory/state/versioned.rs @@ -0,0 +1,120 @@ +use std::{sync::{Arc, Mutex}, fmt::Debug}; + +use async_trait::async_trait; + +use crate::{ + decider::Command, + repository::{state::VersionedStateRepository, RepositoryVersion}, +}; + +#[derive(Debug, Clone)] +pub struct InMemoryStateRepository +where + C: Command + Debug, + ::State: Debug +{ + state: Arc>>, +} + +impl InMemoryStateRepository +where + C: Command + Debug, + ::State: Debug +{ + + fn new(state: ::State) -> Self { + Self { + state: Arc::new(Mutex::new(VersionedState::new(state))) + } + } + + fn version_to_usize(version: &RepositoryVersion) -> Result { + if let RepositoryVersion::Exact(exact) = version { + Ok(exact.to_owned()) + } else { + Err(Error::ExactStreamVersionMustBeKnown) + } + } + + fn version_check(current: &RepositoryVersion, incoming: &RepositoryVersion) -> Result<(), Error> { + if let &RepositoryVersion::StreamExists = current { + return if let &RepositoryVersion::Exact(_) = incoming { + Ok(()) + } else { + Err(Error::ExactStreamVersionMustBeKnown) + } + } + + if Self::version_to_usize(current)? < Self::version_to_usize(incoming)? { + Ok(()) + } else { + Err(Error::VersionOutOfDate) + } + } +} + +#[async_trait] +impl VersionedStateRepository for InMemoryStateRepository +where + C: Command + Debug, + ::State: Debug +{ + type Version = RepositoryVersion; + + async fn reify(&self) -> Result<(::State, Self::Version), Error> { + let handle = self.state.lock().unwrap(); + + Ok((handle.data.to_owned(), handle.version)) + } + + async fn save( + &mut self, + version: &Self::Version, + state: &::State, + ) -> Result<::State, Error> { + let handle = self.state.lock().unwrap(); + + let _ = Self::version_check(&handle.version, version)?; + + let mut handle = self.state.lock().unwrap(); + handle.data = state.to_owned(); + handle.version = version.to_owned(); + + Ok(state.to_owned()) + } +} + +#[derive(Debug, Clone)] +struct VersionedState +where + C: Command + Debug, + ::State: Debug +{ + data: ::State, + version: RepositoryVersion, +} + +impl VersionedState +where + C: Command + Debug, + ::State: Debug +{ + fn new(data: ::State) -> Self { + Self { data, version: RepositoryVersion::StreamExists } + } +} + +pub enum Error { + ExactStreamVersionMustBeKnown, + VersionOutOfDate +} + +#[cfg(test)] +mod tests { + use super::*; + + #[actix_rt::test] + async fn repository_spec_test() { + // let state_repository = InMemoryStateRepository::new() + } +} \ No newline at end of file diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index 413af6a..c3f8192 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -85,7 +85,6 @@ where id: Option<&Self::StreamId>, ) -> Result<(Vec, RepositoryVersion), Error> { let stream_key = self.get_stream_key(id); - println!("Calling Stream {}", &stream_key); if let Some(m) = self.state.get(&stream_key) { let stream_state = m.lock().unwrap(); @@ -113,8 +112,6 @@ where { let stream_key = self.get_stream_key(Some(stream)); - println!("Calling Stream {}", &stream_key); - let mut stream = self.get_stream_or_new(&stream_key).lock().unwrap(); if stream.position == Self::index_from_version(version) { diff --git a/src/repository/mod.rs b/src/repository/mod.rs index b2aff72..2b3f4dd 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -5,7 +5,7 @@ pub mod esdb; #[cfg(feature = "in_memory")] pub mod in_memory; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RepositoryVersion { Any, Exact(usize), diff --git a/src/repository/state.rs b/src/repository/state.rs index 7e1babf..b23875b 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -19,4 +19,3 @@ pub trait VersionedStateRepository { state: &::State, ) -> Result<::State, Err>; } - diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 082e5df..e2143b6 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -4,8 +4,8 @@ use std::{fmt::Debug, thread}; use assert_matches::assert_matches; use crate::{ - repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}, - test_helpers::deciders::user::{User, UserId, UserName}, + repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion, state::VersionedStateRepository}, + test_helpers::deciders::user::{User, UserId, UserName}, decider::Command, }; use super::deciders::user::UserEvent; @@ -94,3 +94,9 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); } + +pub(crate) async fn test_versioned_state_repository( + mut state_repository: impl VersionedStateRepository +) { + todo!() +} \ No newline at end of file From 1d27d30764fa75063373f429326d52681d35eade Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 29 Dec 2022 13:49:08 -0700 Subject: [PATCH 25/70] rudimentary versioned state reify/save test --- Cargo.toml | 2 +- src/repository/in_memory/state/versioned.rs | 61 +++++++++++++-------- src/test_helpers/deciders.rs | 1 + src/test_helpers/repository.rs | 38 ++++++++++--- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77a8f6b..b884f13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,4 @@ uuid_0_8_2 = { package = "uuid", version = "0.8.2", features = ["v4", "serde"], [dev-dependencies] actix-rt = "2.7.0" assert_matches = "1.5.0" -dotenv = "0.15.0" \ No newline at end of file +dotenv = "0.15.0" diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index b7d05a0..0b29467 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -1,4 +1,7 @@ -use std::{sync::{Arc, Mutex}, fmt::Debug}; +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; use async_trait::async_trait; @@ -11,20 +14,19 @@ use crate::{ pub struct InMemoryStateRepository where C: Command + Debug, - ::State: Debug + ::State: Debug, { state: Arc>>, } -impl InMemoryStateRepository +impl InMemoryStateRepository where C: Command + Debug, - ::State: Debug + ::State: Debug, { - - fn new(state: ::State) -> Self { + fn new(state: ::State) -> Self { Self { - state: Arc::new(Mutex::new(VersionedState::new(state))) + state: Arc::new(Mutex::new(VersionedState::new(state))), } } @@ -36,13 +38,16 @@ where } } - fn version_check(current: &RepositoryVersion, incoming: &RepositoryVersion) -> Result<(), Error> { + fn version_check( + current: &RepositoryVersion, + incoming: &RepositoryVersion, + ) -> Result<(), Error> { if let &RepositoryVersion::StreamExists = current { return if let &RepositoryVersion::Exact(_) = incoming { - Ok(()) + Ok(()) } else { Err(Error::ExactStreamVersionMustBeKnown) - } + }; } if Self::version_to_usize(current)? < Self::version_to_usize(incoming)? { @@ -50,14 +55,14 @@ where } else { Err(Error::VersionOutOfDate) } - } + } } #[async_trait] impl VersionedStateRepository for InMemoryStateRepository where C: Command + Debug, - ::State: Debug + ::State: Debug, { type Version = RepositoryVersion; @@ -72,14 +77,16 @@ where version: &Self::Version, state: &::State, ) -> Result<::State, Error> { - let handle = self.state.lock().unwrap(); + let handle_lock = self.state.lock(); + let mut handle = handle_lock.unwrap(); let _ = Self::version_check(&handle.version, version)?; - let mut handle = self.state.lock().unwrap(); - handle.data = state.to_owned(); + handle.data = state.clone(); handle.version = version.to_owned(); + drop(handle); + Ok(state.to_owned()) } } @@ -88,7 +95,7 @@ where struct VersionedState where C: Command + Debug, - ::State: Debug + ::State: Debug, { data: ::State, version: RepositoryVersion, @@ -97,24 +104,34 @@ where impl VersionedState where C: Command + Debug, - ::State: Debug + ::State: Debug, { fn new(data: ::State) -> Self { - Self { data, version: RepositoryVersion::StreamExists } + Self { + data, + version: RepositoryVersion::StreamExists, + } } } + +#[derive(Debug, Clone)] pub enum Error { ExactStreamVersionMustBeKnown, - VersionOutOfDate + VersionOutOfDate, } -#[cfg(test)] +#[cfg(test)] mod tests { + use crate::test_helpers::{deciders::user::{UserCommand, UserDeciderState}, repository::test_versioned_state_repository}; + use super::*; #[actix_rt::test] async fn repository_spec_test() { - // let state_repository = InMemoryStateRepository::new() + let state_repository: InMemoryStateRepository = + InMemoryStateRepository::new(UserDeciderState::default()); + + test_versioned_state_repository(state_repository).await; } -} \ No newline at end of file +} diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 643fda7..8106445 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -127,6 +127,7 @@ pub(crate) mod user { pub(crate) users: HashMap, } + #[derive(Debug)] pub(crate) enum UserCommand { AddUser(UnvalidatedUserName), UpdateUserName(UserId, UnvalidatedUserName), diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index e2143b6..b265e5b 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -1,14 +1,18 @@ use core::time; -use std::{fmt::Debug, thread}; +use std::{collections::HashMap, fmt::Debug, thread}; use assert_matches::assert_matches; use crate::{ - repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion, state::VersionedStateRepository}, - test_helpers::deciders::user::{User, UserId, UserName}, decider::Command, + decider::Command, + repository::{ + event::VersionedEventRepositoryWithStreams, state::VersionedStateRepository, + RepositoryVersion, + }, + test_helpers::deciders::user::{User, UserId, UserName}, }; -use super::deciders::user::UserEvent; +use super::deciders::user::{UserCommand, UserDeciderState, UserEvent}; pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug>( mut event_repository: impl VersionedEventRepositoryWithStreams< @@ -95,8 +99,26 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug> assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); } -pub(crate) async fn test_versioned_state_repository( - mut state_repository: impl VersionedStateRepository +pub(crate) async fn test_versioned_state_repository( + mut state_repository: impl VersionedStateRepository, ) { - todo!() -} \ No newline at end of file + let new_state = UserDeciderState { + users: HashMap::from([( + 1, + User { + id: 1, + name: UserName::try_from("Mike").expect("valid"), + }, + )]), + }; + + let version = RepositoryVersion::Exact(0); + println!("Saving: state={:?}, version={:?}", &new_state, &version); + let _ = state_repository.save(&version, &new_state).await.expect("Success"); + println!("State saved"); + + assert_eq!(state_repository.reify().await.expect("Success"), (new_state.to_owned(), version)); + + let res = state_repository.save(&version, &new_state).await; + assert_matches!(res, Err(_)); +} From 79406f4fda38d7dddff879e09edf20cb397add1c Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 2 Jan 2023 10:46:35 -0700 Subject: [PATCH 26/70] Decider with Context might be a valid use case --- Cargo.toml | 2 +- src/decider.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b884f13..e7faf10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "0.1.0" +version = "1.0.0-alpha" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index 61b6343..e257de9 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -14,6 +14,21 @@ pub trait Decider { fn init() -> S; } +#[async_trait] +pub trait DeciderWithContext { + fn decide(ctx: Ctx, cmd: &Cmd, state: &S) -> Result, Err>; + fn evolve(state: S, event: &E) -> S; + fn init() -> S; +} + +pub trait CommandState { + fn from(value: ::State) -> Self; +} + +pub trait DeciderVariant { + fn decide(cs: impl CommandState) -> Result, Err>; +} + #[cfg(test)] mod tests { From ab79e6f9d647736a9464982d43aa8016067178f3 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 2 Jan 2023 23:08:16 -0700 Subject: [PATCH 27/70] refinement of extended decider types --- src/decider.rs | 26 ++++++++++++++++---------- src/repository/event.rs | 4 ++-- src/test_helpers/repository.rs | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index e257de9..6c613f8 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -7,18 +7,24 @@ pub trait Event { fn event_type(&self) -> String; } -#[async_trait] -pub trait Decider { - fn decide(cmd: &Cmd, state: &S) -> Result, Err>; - fn evolve(state: S, event: &E) -> S; - fn init() -> S; +pub trait Decider { + fn decide(cmd: &Cmd, state: &State) -> Result, Err>; + fn evolve(state: State, event: &Evt) -> State; + fn init() -> State; } -#[async_trait] -pub trait DeciderWithContext { - fn decide(ctx: Ctx, cmd: &Cmd, state: &S) -> Result, Err>; - fn evolve(state: S, event: &E) -> S; - fn init() -> S; +pub trait DeciderWithContext +where + Evt: Event, + Cmd: Command, +{ + fn decide(ctx: Ctx, cmd: &Cmd, state: &State) -> Result, Err>; + fn evolve(state: State, event: &Evt) -> State; + fn init() -> State; +} + +pub trait Evolver { + fn evolve(state: State, event: &Evt) -> State; } pub trait CommandState { diff --git a/src/repository/event.rs b/src/repository/event.rs index 3eba59a..d3a4fb4 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -39,9 +39,9 @@ where pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where E: Event + Sync + Send + Debug, - Err: Debug + Err: Debug + Send + Sync { - type StreamId; + type StreamId: Send + Sync; async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, RepositoryVersion), Err>; async fn load_from_version( diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index b265e5b..34fe229 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -14,7 +14,7 @@ use crate::{ use super::deciders::user::{UserCommand, UserDeciderState, UserEvent}; -pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug>( +pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug + Send + Sync>( mut event_repository: impl VersionedEventRepositoryWithStreams< 'a, UserEvent, From b1efed51a2b17656a1e14f3e0a2b89d400ae3e72 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 2 Jan 2023 23:44:00 -0700 Subject: [PATCH 28/70] decider with ctx - use ref for Ctx --- src/decider.rs | 8 +++----- src/test_helpers/deciders.rs | 2 +- src/test_helpers/repository.rs | 11 ++++++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 6c613f8..60a6ebd 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,5 +1,3 @@ -use async_trait::async_trait; - pub trait Command { type State: Clone + Send + Sync; } @@ -8,7 +6,7 @@ pub trait Event { } pub trait Decider { - fn decide(cmd: &Cmd, state: &State) -> Result, Err>; + fn decide(state: &State, cmd: &Cmd) -> Result, Err>; fn evolve(state: State, event: &Evt) -> State; fn init() -> State; } @@ -18,7 +16,7 @@ where Evt: Event, Cmd: Command, { - fn decide(ctx: Ctx, cmd: &Cmd, state: &State) -> Result, Err>; + fn decide(ctx: &Ctx, state: &State, cmd: &Cmd) -> Result, Err>; fn evolve(state: State, event: &Evt) -> State; fn init() -> State; } @@ -68,7 +66,7 @@ mod tests { .fold(UserDecider::init(), UserDecider::evolve); let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); - let events = UserDecider::decide(&cmd, &state).expect("Decider Success"); + let events = UserDecider::decide(&state, &cmd).expect("Decider Success"); if let Some(UserEvent::UserAdded(user::User { name, id })) = events.clone().first() { let user_id = id.clone(); diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 8106445..0791a73 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -85,8 +85,8 @@ pub(crate) mod user { impl Decider for UserDecider { fn decide( - cmd: &UserCommand, _state: &UserDeciderState, + cmd: &UserCommand, ) -> Result, UserDeciderError> { match cmd { UserCommand::AddUser(user_name) => { diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 34fe229..3982f39 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -4,7 +4,6 @@ use std::{collections::HashMap, fmt::Debug, thread}; use assert_matches::assert_matches; use crate::{ - decider::Command, repository::{ event::VersionedEventRepositoryWithStreams, state::VersionedStateRepository, RepositoryVersion, @@ -114,10 +113,16 @@ pub(crate) async fn test_versioned_state_repository( let version = RepositoryVersion::Exact(0); println!("Saving: state={:?}, version={:?}", &new_state, &version); - let _ = state_repository.save(&version, &new_state).await.expect("Success"); + let _ = state_repository + .save(&version, &new_state) + .await + .expect("Success"); println!("State saved"); - assert_eq!(state_repository.reify().await.expect("Success"), (new_state.to_owned(), version)); + assert_eq!( + state_repository.reify().await.expect("Success"), + (new_state.to_owned(), version) + ); let res = state_repository.save(&version, &new_state).await; assert_matches!(res, Err(_)); From 5b3fca721974ec4968bec08608ebb4ad3307beaa Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 3 Jan 2023 13:32:09 -0700 Subject: [PATCH 29/70] add clone to ESDB repository --- Cargo.toml | 2 +- src/repository/esdb/mod.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e7faf10..44fded4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha" +version = "1.0.0-alpha.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 0a654f8..40ef292 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -16,6 +16,7 @@ use super::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}; pub mod error; +#[derive(Clone)] pub struct ESDBEventRepository { client: Client, stream_name: String, From 3867e8b96a5fa9039a6cb5904de4552e306b88e1 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 3 Jan 2023 21:16:55 -0700 Subject: [PATCH 30/70] refactor to associated types --- Cargo.toml | 2 +- src/decider.rs | 47 +++++++++--------- src/lib.rs | 2 +- src/repository/esdb/error.rs | 4 +- src/repository/event.rs | 4 +- src/repository/in_memory/simple.rs | 22 ++++----- src/repository/in_memory/state/mod.rs | 2 +- src/repository/in_memory/state/versioned.rs | 48 ++++++++----------- .../in_memory/versioned_with_streams/error.rs | 2 +- src/repository/mod.rs | 8 ++-- src/repository/state.rs | 18 +++---- src/test_helpers/deciders.rs | 10 +++- src/test_helpers/mod.rs | 2 +- src/test_helpers/repository.rs | 8 +++- 14 files changed, 89 insertions(+), 90 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 44fded4..98c08cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.1" +version = "1.0.0-alpha.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index 60a6ebd..e29cb0a 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -5,34 +5,37 @@ pub trait Event { fn event_type(&self) -> String; } -pub trait Decider { - fn decide(state: &State, cmd: &Cmd) -> Result, Err>; - fn evolve(state: State, event: &Evt) -> State; - fn init() -> State; +pub trait Decider { + type State; + type Cmd: Send + Sync; + type Evt: Event; + type Err; + + fn decide(state: &Self::State, cmd: &Self::Cmd) -> Result, Self::Err>; + fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; + fn init() -> Self::State; } -pub trait DeciderWithContext -where - Evt: Event, - Cmd: Command, -{ - fn decide(ctx: &Ctx, state: &State, cmd: &Cmd) -> Result, Err>; - fn evolve(state: State, event: &Evt) -> State; - fn init() -> State; +pub trait DeciderWithContext { + type Ctx; + type State; + type Cmd: Send + Sync; + type Evt: Event; + type Err; + + fn decide( + ctx: &Self::Ctx, + state: &Self::State, + cmd: &Self::Cmd, + ) -> Result, Self::Err>; + fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; + fn init() -> Self::State; } pub trait Evolver { fn evolve(state: State, event: &Evt) -> State; } -pub trait CommandState { - fn from(value: ::State) -> Self; -} - -pub trait DeciderVariant { - fn decide(cs: impl CommandState) -> Result, Err>; -} - #[cfg(test)] mod tests { @@ -45,7 +48,7 @@ mod tests { state::StateRepository, }, test_helpers::{ - deciders::user::{self, UserCommand, UserDecider, UserEvent}, + deciders::user::{self, UserCommand, UserDecider, UserDeciderState, UserEvent}, ValueType, }, }; @@ -55,7 +58,7 @@ mod tests { #[actix_rt::test] async fn test_raw_decider() { let event_repository: InMemoryEventRepository = InMemoryEventRepository::new(); - let mut state_repository: InMemoryStateRepository = + let mut state_repository: InMemoryStateRepository = InMemoryStateRepository::new(); let state = event_repository diff --git a/src/lib.rs b/src/lib.rs index 9ac1887..a476313 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,4 @@ pub mod decider; pub mod repository; #[cfg(test)] -mod test_helpers; \ No newline at end of file +mod test_helpers; diff --git a/src/repository/esdb/error.rs b/src/repository/esdb/error.rs index c7fdb6d..c49f468 100644 --- a/src/repository/esdb/error.rs +++ b/src/repository/esdb/error.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum Error{ +pub enum Error { #[error("ESDB Error {0}")] ESDBGeneral(eventstore::Error), #[error("Error reading stream: {0}")] @@ -12,4 +12,4 @@ pub enum Error{ SerializeEventDataPayload(serde_json::Error), #[error("Could not write to stream {0}: {1}")] WriteStream(String, eventstore::Error), -} \ No newline at end of file +} diff --git a/src/repository/event.rs b/src/repository/event.rs index d3a4fb4..6be9687 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -2,8 +2,8 @@ use std::fmt::Debug; use async_trait::async_trait; -use crate::decider::Event; use super::RepositoryVersion; +use crate::decider::Event; #[async_trait] pub trait EventRepository @@ -39,7 +39,7 @@ where pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where E: Event + Sync + Send + Debug, - Err: Debug + Send + Sync + Err: Debug + Send + Sync, { type StreamId: Send + Sync; diff --git a/src/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs index fb60396..0ed70c2 100644 --- a/src/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -6,7 +6,7 @@ use std::{ use async_trait::async_trait; use crate::{ - decider::{Command, Event}, + decider::Event, repository::{event::EventRepository, state::StateRepository}, }; @@ -59,33 +59,31 @@ impl InMemoryEventRepositoryState { } } -pub struct InMemoryStateRepository { - state: ::State, +pub struct InMemoryStateRepository { + state: State, } -impl InMemoryStateRepository +impl InMemoryStateRepository where - C: Command + Send + Sync, - ::State: Default + Send + Sync + Debug, + State: Default + Send + Sync + Debug + Clone, { pub fn new() -> Self { Self { - state: ::State::default(), + state: State::default(), } } } #[async_trait] -impl StateRepository for InMemoryStateRepository +impl StateRepository for InMemoryStateRepository where - C: Command + Send + Sync, - ::State: Default + Send + Sync + Debug, + State: Default + Send + Sync + Debug + Clone, { - async fn reify(&self) -> ::State { + async fn reify(&self) -> State { self.state.clone() } - async fn save(&mut self, state: &::State) -> Result<::State, ()> { + async fn save(&mut self, state: &State) -> Result { self.state = state.clone(); Ok(self.state.to_owned()) } diff --git a/src/repository/in_memory/state/mod.rs b/src/repository/in_memory/state/mod.rs index 0334bbf..abb8335 100644 --- a/src/repository/in_memory/state/mod.rs +++ b/src/repository/in_memory/state/mod.rs @@ -1 +1 @@ -pub mod versioned; \ No newline at end of file +pub mod versioned; diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 0b29467..3d9eca0 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -11,20 +11,18 @@ use crate::{ }; #[derive(Debug, Clone)] -pub struct InMemoryStateRepository +pub struct InMemoryStateRepository where - C: Command + Debug, - ::State: Debug, + State: Debug, { - state: Arc>>, + state: Arc>>, } -impl InMemoryStateRepository +impl InMemoryStateRepository where - C: Command + Debug, - ::State: Debug, + State: Debug, { - fn new(state: ::State) -> Self { + fn new(state: State) -> Self { Self { state: Arc::new(Mutex::new(VersionedState::new(state))), } @@ -59,24 +57,19 @@ where } #[async_trait] -impl VersionedStateRepository for InMemoryStateRepository +impl VersionedStateRepository for InMemoryStateRepository where - C: Command + Debug, - ::State: Debug, + State: Debug + Clone + Send + Sync, { type Version = RepositoryVersion; - async fn reify(&self) -> Result<(::State, Self::Version), Error> { + async fn reify(&self) -> Result<(State, Self::Version), Error> { let handle = self.state.lock().unwrap(); Ok((handle.data.to_owned(), handle.version)) } - async fn save( - &mut self, - version: &Self::Version, - state: &::State, - ) -> Result<::State, Error> { + async fn save(&mut self, version: &Self::Version, state: &State) -> Result { let handle_lock = self.state.lock(); let mut handle = handle_lock.unwrap(); @@ -92,21 +85,19 @@ where } #[derive(Debug, Clone)] -struct VersionedState +struct VersionedState where - C: Command + Debug, - ::State: Debug, + State: Debug, { - data: ::State, + data: State, version: RepositoryVersion, } -impl VersionedState +impl VersionedState where - C: Command + Debug, - ::State: Debug, + State: Debug, { - fn new(data: ::State) -> Self { + fn new(data: State) -> Self { Self { data, version: RepositoryVersion::StreamExists, @@ -114,7 +105,6 @@ where } } - #[derive(Debug, Clone)] pub enum Error { ExactStreamVersionMustBeKnown, @@ -123,13 +113,15 @@ pub enum Error { #[cfg(test)] mod tests { - use crate::test_helpers::{deciders::user::{UserCommand, UserDeciderState}, repository::test_versioned_state_repository}; + use crate::test_helpers::{ + deciders::user::UserDeciderState, repository::test_versioned_state_repository, + }; use super::*; #[actix_rt::test] async fn repository_spec_test() { - let state_repository: InMemoryStateRepository = + let state_repository: InMemoryStateRepository = InMemoryStateRepository::new(UserDeciderState::default()); test_versioned_state_repository(state_repository).await; diff --git a/src/repository/in_memory/versioned_with_streams/error.rs b/src/repository/in_memory/versioned_with_streams/error.rs index 166f473..07dd0e6 100644 --- a/src/repository/in_memory/versioned_with_streams/error.rs +++ b/src/repository/in_memory/versioned_with_streams/error.rs @@ -4,4 +4,4 @@ use thiserror::Error; pub enum Error { #[error("Cannot append, event version is out of date")] VersionConflict, -} \ No newline at end of file +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 2b3f4dd..05a4035 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,14 +1,14 @@ -pub mod event; -pub mod state; #[cfg(feature = "esdb")] pub mod esdb; +pub mod event; #[cfg(feature = "in_memory")] pub mod in_memory; +pub mod state; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RepositoryVersion { Any, Exact(usize), NoStream, - StreamExists -} \ No newline at end of file + StreamExists, +} diff --git a/src/repository/state.rs b/src/repository/state.rs index b23875b..19d68d0 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -1,21 +1,15 @@ use async_trait::async_trait; -use crate::decider::Command; - #[async_trait] -pub trait StateRepository { - async fn reify(&self) -> ::State; - async fn save(&mut self, state: &::State) -> Result<::State, Err>; +pub trait StateRepository { + async fn reify(&self) -> State; + async fn save(&mut self, state: &State) -> Result; } #[async_trait] -pub trait VersionedStateRepository { +pub trait VersionedStateRepository { type Version: Eq; - async fn reify(&self) -> Result<(::State, Self::Version), Err>; - async fn save( - &mut self, - version: &Self::Version, - state: &::State, - ) -> Result<::State, Err>; + async fn reify(&self) -> Result<(State, Self::Version), Err>; + async fn save(&mut self, version: &Self::Version, state: &State) -> Result; } diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 0791a73..48832fb 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -83,7 +83,15 @@ pub(crate) mod user { pub(crate) struct UserDecider {} - impl Decider for UserDecider { + impl Decider for UserDecider { + type State = UserDeciderState; + + type Cmd = UserCommand; + + type Evt = UserEvent; + + type Err = UserDeciderError; + fn decide( _state: &UserDeciderState, cmd: &UserCommand, diff --git a/src/test_helpers/mod.rs b/src/test_helpers/mod.rs index 5b503f1..6a2af23 100644 --- a/src/test_helpers/mod.rs +++ b/src/test_helpers/mod.rs @@ -3,4 +3,4 @@ pub(crate) mod repository; pub(crate) trait ValueType { fn value(&self) -> T; -} \ No newline at end of file +} diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 3982f39..8e9a538 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -11,7 +11,7 @@ use crate::{ test_helpers::deciders::user::{User, UserId, UserName}, }; -use super::deciders::user::{UserCommand, UserDeciderState, UserEvent}; +use super::deciders::user::{UserDeciderState, UserEvent}; pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug + Send + Sync>( mut event_repository: impl VersionedEventRepositoryWithStreams< @@ -99,7 +99,11 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug } pub(crate) async fn test_versioned_state_repository( - mut state_repository: impl VersionedStateRepository, + mut state_repository: impl VersionedStateRepository< + UserDeciderState, + Err, + Version = RepositoryVersion, + >, ) { let new_state = UserDeciderState { users: HashMap::from([( From e7fc3e70f89acb35f897f972f2828a36e12581a2 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 3 Jan 2023 22:41:54 -0700 Subject: [PATCH 31/70] Evolve to associated types --- Cargo.toml | 2 +- src/decider.rs | 9 ++++----- src/repository/in_memory/state/versioned.rs | 5 +---- src/test_helpers/deciders.rs | 6 +----- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 98c08cd..6da5b8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.2" +version = "1.0.0-alpha.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index e29cb0a..46f33ce 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,6 +1,3 @@ -pub trait Command { - type State: Clone + Send + Sync; -} pub trait Event { fn event_type(&self) -> String; } @@ -32,8 +29,10 @@ pub trait DeciderWithContext { fn init() -> Self::State; } -pub trait Evolver { - fn evolve(state: State, event: &Evt) -> State; +pub trait Evolver { + type State; + type Evt: Event; + fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; } #[cfg(test)] diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 3d9eca0..6c3e994 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -5,10 +5,7 @@ use std::{ use async_trait::async_trait; -use crate::{ - decider::Command, - repository::{state::VersionedStateRepository, RepositoryVersion}, -}; +use crate::repository::{state::VersionedStateRepository, RepositoryVersion}; #[derive(Debug, Clone)] pub struct InMemoryStateRepository diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 48832fb..7b2beac 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -5,7 +5,7 @@ pub(crate) mod user { use thiserror::Error; use crate::{ - decider::{Command, Decider, Event}, + decider::{Decider, Event}, test_helpers::ValueType, }; @@ -141,10 +141,6 @@ pub(crate) mod user { UpdateUserName(UserId, UnvalidatedUserName), } - impl Command for UserCommand { - type State = UserDeciderState; - } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) enum UserEvent { UserAdded(User), From f36ce481abe3276daa49ede4a8e818e288ebe072 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 4 Jan 2023 07:03:30 -0700 Subject: [PATCH 32/70] implement decide in terms of evolve --- Cargo.toml | 2 +- src/decider.rs | 13 +++---------- src/test_helpers/deciders.rs | 12 +++++++----- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6da5b8c..56511d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.4" +version = "1.0.0-alpha.5" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index 46f33ce..965afe3 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -2,22 +2,16 @@ pub trait Event { fn event_type(&self) -> String; } -pub trait Decider { - type State; +pub trait Decider: Evolver { type Cmd: Send + Sync; - type Evt: Event; type Err; fn decide(state: &Self::State, cmd: &Self::Cmd) -> Result, Self::Err>; - fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; - fn init() -> Self::State; } -pub trait DeciderWithContext { +pub trait DeciderWithContext: Evolver { type Ctx; - type State; type Cmd: Send + Sync; - type Evt: Event; type Err; fn decide( @@ -25,14 +19,13 @@ pub trait DeciderWithContext { state: &Self::State, cmd: &Self::Cmd, ) -> Result, Self::Err>; - fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; - fn init() -> Self::State; } pub trait Evolver { type State; type Evt: Event; fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; + fn init() -> Self::State; } #[cfg(test)] diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 7b2beac..475c91e 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -5,7 +5,7 @@ pub(crate) mod user { use thiserror::Error; use crate::{ - decider::{Decider, Event}, + decider::{Decider, Event, Evolver}, test_helpers::ValueType, }; @@ -84,12 +84,8 @@ pub(crate) mod user { pub(crate) struct UserDecider {} impl Decider for UserDecider { - type State = UserDeciderState; - type Cmd = UserCommand; - type Evt = UserEvent; - type Err = UserDeciderError; fn decide( @@ -111,6 +107,12 @@ pub(crate) mod user { } } } + } + + impl Evolver for UserDecider { + type State = UserDeciderState; + + type Evt = UserEvent; fn evolve(mut state: UserDeciderState, event: &UserEvent) -> UserDeciderState { match event { From 3ffba9344413a58f9dbdeed7cb57c23cb10df0e8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 4 Jan 2023 09:52:00 -0700 Subject: [PATCH 33/70] adds basic strategies --- Cargo.toml | 2 +- src/lib.rs | 1 + src/strategies.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/strategies.rs diff --git a/Cargo.toml b/Cargo.toml index 56511d3..6f5f255 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.5" +version = "1.0.0-alpha.6" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/lib.rs b/src/lib.rs index a476313..6f34de6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod decider; pub mod repository; +pub mod strategies; #[cfg(test)] mod test_helpers; diff --git a/src/strategies.rs b/src/strategies.rs new file mode 100644 index 0000000..1a75eb0 --- /dev/null +++ b/src/strategies.rs @@ -0,0 +1,114 @@ +use std::fmt::Debug; + +use crate::{ + decider::{Decider, DeciderWithContext, Evolver}, + repository, +}; +use async_trait::async_trait; +use repository::event::VersionedEventRepositoryWithStreams; + +#[async_trait] +pub trait StateFromEventRepository +where + ::Evt: Send + Sync + Debug, + ::State: Send + Sync + Debug + Default, +{ + type Ev: Evolver + Send + Sync + Default; + + async fn load<'a, Err>( + event_repository: &(impl VersionedEventRepositoryWithStreams<'a, ::Evt, Err> + + Send + + Sync), + ) -> Result<::State, Err> + where + Err: Debug + Send + Sync, + { + Ok(event_repository + .load(None) + .await? + .0 + .iter() + .fold(::State::default(), Self::Ev::evolve)) + } + + async fn load_by_id<'a, Err, StreamId>( + event_repository: &(impl VersionedEventRepositoryWithStreams< + 'a, + ::Evt, + Err, + StreamId = StreamId, + > + Send + + Sync), + stream_id: Option<&StreamId>, + ) -> Result<::State, Err> + where + Err: Debug + Send + Sync, + StreamId: Send + Sync, + { + Ok(event_repository + .load(stream_id) + .await? + .0 + .iter() + .fold(::State::default(), Self::Ev::evolve)) + } +} + +#[async_trait] +pub trait LoadDecideAppend +where + ::State: Send + Sync + Debug + Default, + ::Ctx: Send + Sync + Debug + Clone, + ::Cmd: Send + Sync + Debug + Clone, + ::Evt: Send + Sync + Debug, + ::Err: Send + Sync + Debug, +{ + type Decide: DeciderWithContext + Send + Sync; + + async fn execute<'a, RepoErr, StreamId>( + event_repository: &mut (impl VersionedEventRepositoryWithStreams< + 'a, + ::Evt, + RepoErr, + StreamId = StreamId, + > + Send + + Sync), + stream_id: &StreamId, + ctx: &<::Decide as DeciderWithContext>::Ctx, + cmd: &<::Decide as DeciderWithContext>::Cmd, + ) -> Result< + Vec<::Evt>, + LoadDecideAppendError<::Err, RepoErr>, + > + where + RepoErr: Debug + Send + Sync, + StreamId: Send + Sync, + { + let stream = Some(stream_id); + let (decider_evts, version) = event_repository + .load(stream) + .await + .map_err(LoadDecideAppendError::RepositoryErr)?; + + let state = decider_evts.iter().fold( + ::init(), + ::evolve, + ); + + let evts = ::decide(&ctx, &state, &cmd) + .map_err(LoadDecideAppendError::DecideErr)?; + + let (evts, _) = event_repository + .append(&version, stream_id, &evts) + .await + .map_err(LoadDecideAppendError::RepositoryErr)?; + + Ok(evts) + } +} + +#[derive(Debug)] +pub enum LoadDecideAppendError { + DecideErr(DecideErr), + RepositoryErr(RepoErr), +} From 38778ba01e369247dd93f6970d2acd7ffae1d568 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 4 Jan 2023 10:22:31 -0700 Subject: [PATCH 34/70] unessesary trait bounds --- Cargo.toml | 2 +- src/strategies.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f5f255..5d5a942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.6" +version = "1.0.0-alpha.7" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/strategies.rs b/src/strategies.rs index 1a75eb0..ba8b468 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -59,7 +59,7 @@ pub trait LoadDecideAppend where ::State: Send + Sync + Debug + Default, ::Ctx: Send + Sync + Debug + Clone, - ::Cmd: Send + Sync + Debug + Clone, + ::Cmd: Send + Sync + Debug, ::Evt: Send + Sync + Debug, ::Err: Send + Sync + Debug, { From 2f600f3d2cc6e974159d5393abe7997cb8112f17 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 4 Jan 2023 19:03:09 -0700 Subject: [PATCH 35/70] auto retry versioned event repository strategy --- Cargo.toml | 2 +- src/repository/esdb/mod.rs | 47 +++++++++++--- src/repository/event.rs | 38 ++++++++++- .../in_memory/versioned_with_streams/error.rs | 4 +- .../in_memory/versioned_with_streams/mod.rs | 31 +++++++-- src/strategies.rs | 63 ++++++++++++++----- 6 files changed, 148 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d5a942..390195b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.7" +version = "1.0.0-alpha.8" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 40ef292..9e767e2 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -2,8 +2,8 @@ use std::{fmt::Debug, marker::PhantomData}; use async_trait::async_trait; use eventstore::{ - AppendToStreamOptions, Client, EventData, ExpectedRevision, ReadStreamOptions, ResolvedEvent, - StreamPosition, + AppendToStreamOptions, Client, CurrentRevision, EventData, ExpectedRevision, ReadStreamOptions, + ResolvedEvent, StreamPosition, }; use serde::{de::DeserializeOwned, Serialize}; use uuid_0_8_2::Uuid; @@ -12,7 +12,10 @@ use crate::decider::Event; use self::error::Error; -use super::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}; +use super::{ + event::{VersionDiff, VersionedEventRepositoryWithStreams, VersionedRepositoryError}, + RepositoryVersion, +}; pub mod error; @@ -56,6 +59,13 @@ impl<'a, E> ESDBEventRepository { _ => ExpectedRevision::Any, } } + + fn current_revision_to_version(revision: &CurrentRevision) -> RepositoryVersion { + match revision { + CurrentRevision::Current(val) => RepositoryVersion::Exact(*val as usize), + CurrentRevision::NoStream => RepositoryVersion::NoStream, + } + } } #[async_trait] @@ -68,7 +78,7 @@ where async fn load( &self, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), Error> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { self.load_from_version(&RepositoryVersion::Any, id).await } @@ -76,7 +86,7 @@ where &self, version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), Error> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { let mut stream = self .client .read_stream( @@ -86,7 +96,8 @@ where .position(Self::version_to_esdb_position(&version)), ) .await - .map_err(Error::ESDBGeneral)?; + .map_err(Error::ESDBGeneral) + .map_err(VersionedRepositoryError::RepoErr)?; let mut evts: Vec = vec![]; @@ -97,7 +108,7 @@ where Err(eventstore::Error::ResourceNotFound) => { return Ok((vec![], RepositoryVersion::NoStream)) } - Err(e) => return Err(Error::ReadStream(e)), + Err(e) => return Err(VersionedRepositoryError::RepoErr(Error::ReadStream(e))), } } @@ -123,7 +134,7 @@ where version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, RepositoryVersion), Error> + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> where 'a: 'async_trait, E: 'async_trait, @@ -133,7 +144,8 @@ where for e in events { let ed = EventData::json(e.event_type(), e) .map(|ed| ed.id(Uuid::new_v4())) - .map_err(Error::SerializeEventDataPayload)?; + .map_err(Error::SerializeEventDataPayload) + .map_err(VersionedRepositoryError::RepoErr)?; perpared_events.push(ed); } @@ -147,7 +159,16 @@ where perpared_events, ) .await - .map_err(|e| Error::WriteStream(stream.to_owned(), e))?; + .map_err(|e| { + if let eventstore::Error::WrongExpectedVersion { current, .. } = e { + VersionedRepositoryError::VersionConflict(VersionDiff::new( + *version, + Self::current_revision_to_version(¤t), + )) + } else { + VersionedRepositoryError::RepoErr(Error::WriteStream(stream.to_owned(), e)) + } + })?; Ok(( events.to_owned(), @@ -156,6 +177,12 @@ where } } +// impl Into> for Error { +// fn into(self) -> VersionedRepositoryError { +// todo!() +// } +// } + #[cfg(test)] mod tests { use eventstore::DeleteStreamOptions; diff --git a/src/repository/event.rs b/src/repository/event.rs index 6be9687..42523a6 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use async_trait::async_trait; +use thiserror::Error; use super::RepositoryVersion; use crate::decider::Event; @@ -43,19 +44,50 @@ where { type StreamId: Send + Sync; - async fn load(&self, id: Option<&Self::StreamId>) -> Result<(Vec, RepositoryVersion), Err>; + async fn load( + &self, + id: Option<&Self::StreamId>, + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError>; + async fn load_from_version( &self, version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), Err>; + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError>; + async fn append( &mut self, version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, RepositoryVersion), Err> + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> where 'a: 'async_trait, E: 'async_trait; } + +#[derive(Debug, Error)] +pub enum VersionedRepositoryError { + VersionConflict(VersionDiff), + RepoErr(RepoErr), +} + +#[derive(Debug)] +pub struct VersionDiff { + expected: RepositoryVersion, + actual: RepositoryVersion, +} + +impl VersionDiff { + pub fn new(expected: RepositoryVersion, actual: RepositoryVersion) -> Self { + Self { expected, actual } + } + + pub fn expected(&self) -> RepositoryVersion { + self.expected.to_owned() + } + + pub fn actual(&self) -> RepositoryVersion { + self.actual.to_owned() + } +} diff --git a/src/repository/in_memory/versioned_with_streams/error.rs b/src/repository/in_memory/versioned_with_streams/error.rs index 07dd0e6..e623ee5 100644 --- a/src/repository/in_memory/versioned_with_streams/error.rs +++ b/src/repository/in_memory/versioned_with_streams/error.rs @@ -1,7 +1,9 @@ use thiserror::Error; +use crate::repository::event::VersionDiff; + #[derive(Debug, Error)] pub enum Error { #[error("Cannot append, event version is out of date")] - VersionConflict, + VersionConflict(VersionDiff), } diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index c3f8192..7d1e696 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -8,7 +8,10 @@ use std::{ use crate::{ decider::Event, - repository::{event::VersionedEventRepositoryWithStreams, RepositoryVersion}, + repository::{ + event::{VersionDiff, VersionedEventRepositoryWithStreams, VersionedRepositoryError}, + RepositoryVersion, + }, }; use super::InMemoryEventRepositoryState; @@ -64,6 +67,10 @@ where _ => 0, } } + + fn version_from_index(index: &usize) -> RepositoryVersion { + RepositoryVersion::Exact(*index) + } } #[async_trait] @@ -76,14 +83,14 @@ where async fn load( &self, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), Error> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { self.load_from_version(&RepositoryVersion::Any, id).await } async fn load_from_version( &self, version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), Error> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { let stream_key = self.get_stream_key(id); if let Some(m) = self.state.get(&stream_key) { @@ -105,7 +112,7 @@ where version: &RepositoryVersion, stream: &Self::StreamId, events: &Vec, - ) -> Result<(Vec, RepositoryVersion), Error> + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> where 'a: 'async_trait, E: 'async_trait, @@ -131,7 +138,21 @@ where Ok((events.to_owned(), RepositoryVersion::Exact(position))) } else { - Err(Error::VersionConflict) + Err(Error::VersionConflict(VersionDiff::new( + *version, + Self::version_from_index(&stream.position), + )) + .into()) + } + } +} + +impl Into> for Error { + fn into(self) -> VersionedRepositoryError { + if let Error::VersionConflict(diff) = self { + VersionedRepositoryError::VersionConflict(diff) + } else { + VersionedRepositoryError::RepoErr(self) } } } diff --git a/src/strategies.rs b/src/strategies.rs index ba8b468..150b4ea 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -1,8 +1,8 @@ use std::fmt::Debug; use crate::{ - decider::{Decider, DeciderWithContext, Evolver}, - repository, + decider::{DeciderWithContext, Evolver}, + repository::{self, event::VersionedRepositoryError}, }; use async_trait::async_trait; use repository::event::VersionedEventRepositoryWithStreams; @@ -19,7 +19,7 @@ where event_repository: &(impl VersionedEventRepositoryWithStreams<'a, ::Evt, Err> + Send + Sync), - ) -> Result<::State, Err> + ) -> Result<::State, VersionedRepositoryError> where Err: Debug + Send + Sync, { @@ -40,7 +40,7 @@ where > + Send + Sync), stream_id: Option<&StreamId>, - ) -> Result<::State, Err> + ) -> Result<::State, VersionedRepositoryError> where Err: Debug + Send + Sync, StreamId: Send + Sync, @@ -65,6 +65,15 @@ where { type Decide: DeciderWithContext + Send + Sync; + fn to_lda_error( + err: VersionedRepositoryError, + ) -> LoadDecideAppendError { + match err { + VersionedRepositoryError::VersionConflict(_) => LoadDecideAppendError::VersionError, + VersionedRepositoryError::RepoErr(e) => LoadDecideAppendError::RepositoryErr(e), + } + } + async fn execute<'a, RepoErr, StreamId>( event_repository: &mut (impl VersionedEventRepositoryWithStreams< 'a, @@ -76,6 +85,7 @@ where stream_id: &StreamId, ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, + retrys: Option, ) -> Result< Vec<::Evt>, LoadDecideAppendError<::Err, RepoErr>, @@ -85,30 +95,49 @@ where StreamId: Send + Sync, { let stream = Some(stream_id); - let (decider_evts, version) = event_repository + + let (mut decider_evts, mut version) = event_repository .load(stream) .await - .map_err(LoadDecideAppendError::RepositoryErr)?; + .map_err(Self::to_lda_error)?; - let state = decider_evts.iter().fold( - ::init(), - ::evolve, - ); + let mut state = ::init(); - let evts = ::decide(&ctx, &state, &cmd) - .map_err(LoadDecideAppendError::DecideErr)?; + for _ in 1..retrys.unwrap_or(4) { + state = decider_evts + .iter() + .fold(state, ::evolve); - let (evts, _) = event_repository - .append(&version, stream_id, &evts) - .await - .map_err(LoadDecideAppendError::RepositoryErr)?; + let new_evts = ::decide(&ctx, &state, &cmd) + .map_err(LoadDecideAppendError::DecideErr)?; + + match event_repository + .append(&version, stream_id, &new_evts) + .await + { + Ok((appended_evts, _)) => return Ok(appended_evts), + Err(VersionedRepositoryError::RepoErr(e)) => { + return Err(LoadDecideAppendError::RepositoryErr(e)); + } + Err(VersionedRepositoryError::VersionConflict(_)) => { + let (mut catchup_evts, new_version) = event_repository + .load_from_version(&version, stream) + .await + .map_err(Self::to_lda_error)?; + + version = new_version; + decider_evts.append(&mut catchup_evts); + } + }; + } - Ok(evts) + Ok(vec![]) } } #[derive(Debug)] pub enum LoadDecideAppendError { + VersionError, DecideErr(DecideErr), RepositoryErr(RepoErr), } From 196bcf0bbdc9ef78d77a76f4fe9ac6edb73a8929 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 4 Jan 2023 22:00:53 -0700 Subject: [PATCH 36/70] impl Error for VersionedRepositoryError --- src/repository/event.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/repository/event.rs b/src/repository/event.rs index 42523a6..bc9e35d 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -68,7 +68,9 @@ where #[derive(Debug, Error)] pub enum VersionedRepositoryError { + #[error("Version conflict {0:?}")] VersionConflict(VersionDiff), + #[error("Repository Error {0}")] RepoErr(RepoErr), } From 7649b9712248178605cff1ed03992afc9915181f Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 5 Jan 2023 17:16:57 -0700 Subject: [PATCH 37/70] test OCC --- Cargo.toml | 1 + src/decider.rs | 2 +- .../in_memory/versioned_with_streams/mod.rs | 1 + src/strategies.rs | 110 ++++++++++++++++-- src/test_helpers/deciders.rs | 96 ++++++++++++++- src/test_helpers/repository.rs | 16 ++- 6 files changed, 202 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 390195b..48b5684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,5 @@ uuid_0_8_2 = { package = "uuid", version = "0.8.2", features = ["v4", "serde"], [dev-dependencies] actix-rt = "2.7.0" assert_matches = "1.5.0" +autoincrement = "1" dotenv = "0.15.0" diff --git a/src/decider.rs b/src/decider.rs index 965afe3..6be8457 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -61,7 +61,7 @@ mod tests { .fold(UserDecider::init(), UserDecider::evolve); let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); - let events = UserDecider::decide(&state, &cmd).expect("Decider Success"); + let events = ::decide(&state, &cmd).expect("Decider Success"); if let Some(UserEvent::UserAdded(user::User { name, id })) = events.clone().first() { let user_id = id.clone(); diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index 7d1e696..a8cc06c 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -19,6 +19,7 @@ use error::Error; pub mod error; +#[derive(Clone)] pub struct InMemoryEventRepository where E: Event + Sync + Send + Debug, diff --git a/src/strategies.rs b/src/strategies.rs index 150b4ea..734f0ab 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -11,9 +11,9 @@ use repository::event::VersionedEventRepositoryWithStreams; pub trait StateFromEventRepository where ::Evt: Send + Sync + Debug, - ::State: Send + Sync + Debug + Default, + ::State: Send + Sync + Debug, { - type Ev: Evolver + Send + Sync + Default; + type Ev: Evolver + Send + Sync; async fn load<'a, Err>( event_repository: &(impl VersionedEventRepositoryWithStreams<'a, ::Evt, Err> @@ -28,7 +28,7 @@ where .await? .0 .iter() - .fold(::State::default(), Self::Ev::evolve)) + .fold(::init(), Self::Ev::evolve)) } async fn load_by_id<'a, Err, StreamId>( @@ -39,26 +39,26 @@ where StreamId = StreamId, > + Send + Sync), - stream_id: Option<&StreamId>, + stream_id: &StreamId, ) -> Result<::State, VersionedRepositoryError> where Err: Debug + Send + Sync, StreamId: Send + Sync, { Ok(event_repository - .load(stream_id) + .load(Some(stream_id)) .await? .0 .iter() - .fold(::State::default(), Self::Ev::evolve)) + .fold(::init(), Self::Ev::evolve)) } } #[async_trait] pub trait LoadDecideAppend where - ::State: Send + Sync + Debug + Default, - ::Ctx: Send + Sync + Debug + Clone, + ::State: Send + Sync + Debug, + ::Ctx: Send + Sync + Debug, ::Cmd: Send + Sync + Debug, ::Evt: Send + Sync + Debug, ::Err: Send + Sync + Debug, @@ -120,6 +120,7 @@ where return Err(LoadDecideAppendError::RepositoryErr(e)); } Err(VersionedRepositoryError::VersionConflict(_)) => { + println!("RETRY IT!!"); let (mut catchup_evts, new_version) = event_repository .load_from_version(&version, stream) .await @@ -131,13 +132,104 @@ where }; } - Ok(vec![]) + Err(LoadDecideAppendError::OccMaxRetries) } } #[derive(Debug)] pub enum LoadDecideAppendError { + OccMaxRetries, VersionError, DecideErr(DecideErr), RepositoryErr(RepoErr), } + +#[cfg(test)] +mod tests { + use std::{thread, cell::Cell}; + + use crate::{ + repository::in_memory::versioned_with_streams::InMemoryEventRepository, + test_helpers::deciders::user::{ + IdGen, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, UserEvent, + }, + }; + + use super::*; + + #[actix_rt::test] + async fn test_occ() { + let ctx = UserDeciderCtx::new(); + + let mut event_repository = InMemoryEventRepository::::new("test"); + + let cmd1 = UserCommand::AddUser("Mike".to_string()); + + let evts = + UserDecider::execute(&mut event_repository, &1.to_string(), &ctx, &cmd1, None).await; + + println!("New Events: {:?}", evts); + println!( + "State Stream -1: {:?}", + UserDeciderState::load_by_id(&event_repository, &1.to_string()).await + ); + + let cmd2 = UserCommand::AddUser("Dmitiry".to_string()); + let evts = + UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd2, None).await; + + println!("New Events: {:?}", evts); + println!( + "State Stream -2: {:?}", + UserDeciderState::load_by_id(&event_repository, &2.to_string()).await + ); + + println!( + "State All: {:?}", + UserDeciderState::load(&event_repository).await + ); + + + + let cmd3 = UserCommand::UpdateUserName(1, "MikeUpdated1".to_string()); + let cmd4 = UserCommand::UpdateUserName(1, "MikeUpdated4".to_string()); + let cmd5 = UserCommand::UpdateUserName(1, "MikeUpdated5".to_string()); + let cmd6 = UserCommand::UpdateUserName(1, "MikeUpdated6".to_string()); + let cmd7 = UserCommand::UpdateUserName(1, "MikeUpdated7".to_string()); + let cmd8 = UserCommand::UpdateUserName(1, "MikeUpdated8".to_string()); + let cmd9 = UserCommand::UpdateUserName(1, "MikeUpdated9".to_string()); + let cmd10 = UserCommand::UpdateUserName(1, "MikeUpdated10".to_string()); + + let mut event_repository2 = event_repository.clone(); + let mut ctx2 = ctx.clone(); + + let handle = actix_rt::spawn(async move { + let mut thread_repo = event_repository.clone(); + let mut thread_ctx = ctx.clone(); + + let id = 2.to_string(); + + for cmd in vec![cmd3, cmd4, cmd5, cmd6] { + println!("Calling in Spawn Thread: {:?}", cmd); + let _ = UserDecider::execute(&mut thread_repo, &id, &thread_ctx, &cmd, None).await; + } + }); + + let id = 2.to_string(); + for cmd in vec![cmd7, cmd8, cmd9, cmd10] { + println!("Calling in Main Thread: {:?}", cmd); + let _ = UserDecider::execute(&mut event_repository2, &id, &ctx2, &cmd, None).await; + } + + // let paralell_cmds = ( + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd3, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd4, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd5, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd6, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd7, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd8, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd9, None), + // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd10, None), + // ); + } +} diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 475c91e..8d93d53 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -1,11 +1,13 @@ pub(crate) mod user { - use std::collections::HashMap; + use std::{collections::HashMap, cell::RefCell, sync::{Arc, Mutex},}; + use autoincrement::{AsyncIncremental, AutoIncrement, AsyncIncrement}; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{ - decider::{Decider, Event, Evolver}, + decider::{Decider, DeciderWithContext, Event, Evolver}, + strategies::{LoadDecideAppend, StateFromEventRepository}, test_helpers::ValueType, }; @@ -81,7 +83,7 @@ pub(crate) mod user { NameToLong(String), } - pub(crate) struct UserDecider {} + pub(crate) struct UserDecider; impl Decider for UserDecider { type Cmd = UserCommand; @@ -128,15 +130,96 @@ pub(crate) mod user { } fn init() -> UserDeciderState { - Default::default() + UserDeciderState { + users: Default::default(), + } + } + } + + impl DeciderWithContext for UserDecider { + type Ctx = UserDeciderCtx; + + type Cmd = UserCommand; + + type Err = UserDeciderError; + + fn decide( + ctx: &UserDeciderCtx, + _state: &UserDeciderState, + cmd: &UserCommand, + ) -> Result, UserDeciderError> { + match cmd { + UserCommand::AddUser(user_name) => { + let seq = ctx.id_sequence.lock().unwrap(); + let IdGen(id) = seq.pull(); + + let name = UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserAdded(User { id, name })]) + } + UserCommand::UpdateUserName(user_id, user_name) => { + let name = UserName::try_from(user_name) + .map_err(|e| UserDeciderError::UserField(e))?; + + Ok(vec![UserEvent::UserNameUpdated(user_id.to_owned(), name)]) + } + } } } - #[derive(Debug, Clone, Default, PartialEq, Eq)] + impl LoadDecideAppend for UserDecider { + type Decide = Self; + } + + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct UserDeciderState { pub(crate) users: HashMap, } + impl UserDeciderState { + pub fn new(users: HashMap) -> Self { + Self::default().set_users(users) + } + + pub fn set_users(&self, users: HashMap) -> Self { + Self { + users, + ..*self + } + } + } + + impl Default for UserDeciderState { + fn default() -> Self { + Self { + users: HashMap::new().to_owned(), + } + } + } + + #[derive(Clone, Debug)] + pub(crate) struct UserDeciderCtx { + id_sequence: Arc>> + } + + impl UserDeciderCtx { + pub fn new() -> Self { + Self { + id_sequence: Arc::new(Mutex::new(IdGen::init())) + } + } + + pub fn current(&self) -> usize { + let IdGen(id) = self.id_sequence.lock().unwrap().current(); + id + } + } + + impl StateFromEventRepository for UserDeciderState { + type Ev = UserDecider; + } + #[derive(Debug)] pub(crate) enum UserCommand { AddUser(UnvalidatedUserName), @@ -163,4 +246,7 @@ pub(crate) mod user { #[error("Invalid user field {0:?}")] UserField(UserFieldError), } + + #[derive(AsyncIncremental, PartialEq, Eq, Clone, Debug)] + pub struct IdGen(usize); } diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 8e9a538..3c6404e 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -105,15 +105,13 @@ pub(crate) async fn test_versioned_state_repository( Version = RepositoryVersion, >, ) { - let new_state = UserDeciderState { - users: HashMap::from([( - 1, - User { - id: 1, - name: UserName::try_from("Mike").expect("valid"), - }, - )]), - }; + let new_state = UserDeciderState::new(HashMap::from([( + 1, + User { + id: 1, + name: UserName::try_from("Mike").expect("valid"), + }, + )])); let version = RepositoryVersion::Exact(0); println!("Saving: state={:?}, version={:?}", &new_state, &version); From 28eb03cb1289c97537688625d492a32584302abe Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 5 Jan 2023 17:50:43 -0700 Subject: [PATCH 38/70] more LoadDecideAppend basic functionality tests --- src/strategies.rs | 164 ++++++++++++++++++++++++++++++---------------- 1 file changed, 109 insertions(+), 55 deletions(-) diff --git a/src/strategies.rs b/src/strategies.rs index 734f0ab..7fd993e 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -146,90 +146,144 @@ pub enum LoadDecideAppendError { #[cfg(test)] mod tests { - use std::{thread, cell::Cell}; + use std::{collections::HashMap, hash::Hash}; + + use assert_matches::assert_matches; use crate::{ repository::in_memory::versioned_with_streams::InMemoryEventRepository, - test_helpers::deciders::user::{ - IdGen, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, UserEvent, + test_helpers::{ + deciders::user::{ + User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderError, UserDeciderState, + UserEvent, UserFieldError, UserName, + }, + ValueType, }, }; use super::*; #[actix_rt::test] - async fn test_occ() { + async fn load_decide_append_basic_function() { let ctx = UserDeciderCtx::new(); let mut event_repository = InMemoryEventRepository::::new("test"); let cmd1 = UserCommand::AddUser("Mike".to_string()); - let evts = - UserDecider::execute(&mut event_repository, &1.to_string(), &ctx, &cmd1, None).await; - - println!("New Events: {:?}", evts); - println!( - "State Stream -1: {:?}", - UserDeciderState::load_by_id(&event_repository, &1.to_string()).await + let first_id = ctx.current(); + let evts = UserDecider::execute( + &mut event_repository, + &first_id.to_string(), + &ctx, + &cmd1, + None, + ) + .await + .expect("command_succeeds"); + + assert_matches!( + evts.first().expect("one event"), + UserEvent::UserAdded(User { id, name }) if (&first_id == id) && (name.value() == "Mike".to_string()) ); - let cmd2 = UserCommand::AddUser("Dmitiry".to_string()); - let evts = - UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd2, None).await; + let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) + .await + .expect("state is loaded"); - println!("New Events: {:?}", evts); - println!( - "State Stream -2: {:?}", - UserDeciderState::load_by_id(&event_repository, &2.to_string()).await + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([(first_id.clone(), User { id: first_id, name: UserName::try_from("Mike".to_string()).unwrap() })]) ); - println!( - "State All: {:?}", - UserDeciderState::load(&event_repository).await + let second_id = ctx.current(); + + let cmd2 = UserCommand::AddUser("Dmitiry".to_string()); + let evts = UserDecider::execute( + &mut event_repository, + &second_id.to_string(), + &ctx, + &cmd2, + None, + ) + .await + .expect("command_succeeds"); + + assert_matches!( + evts.first().expect("one event"), + UserEvent::UserAdded(User { id, name }) if (&second_id == id) && (name.value() == "Dmitiry".to_string()) ); + let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) + .await + .expect("state is loaded"); + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id.clone(), name: UserName::try_from("Dmitiry".to_string()).unwrap() })]) + ); - let cmd3 = UserCommand::UpdateUserName(1, "MikeUpdated1".to_string()); - let cmd4 = UserCommand::UpdateUserName(1, "MikeUpdated4".to_string()); - let cmd5 = UserCommand::UpdateUserName(1, "MikeUpdated5".to_string()); - let cmd6 = UserCommand::UpdateUserName(1, "MikeUpdated6".to_string()); - let cmd7 = UserCommand::UpdateUserName(1, "MikeUpdated7".to_string()); - let cmd8 = UserCommand::UpdateUserName(1, "MikeUpdated8".to_string()); - let cmd9 = UserCommand::UpdateUserName(1, "MikeUpdated9".to_string()); - let cmd10 = UserCommand::UpdateUserName(1, "MikeUpdated10".to_string()); + let cmd3 = UserCommand::UpdateUserName(second_id.clone(), "Dmitiry2".to_string()); + let evts = UserDecider::execute( + &mut event_repository, + &second_id.to_string(), + &ctx, + &cmd3, + None, + ) + .await + .expect("command_succeeds"); + + assert_matches!( + evts.first().expect("one event"), + UserEvent::UserNameUpdated(id, name) if (id == &second_id) && (name == &UserName::try_from("Dmitiry2".to_string()).unwrap()) + ); - let mut event_repository2 = event_repository.clone(); - let mut ctx2 = ctx.clone(); + let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) + .await + .expect("state is loaded"); - let handle = actix_rt::spawn(async move { - let mut thread_repo = event_repository.clone(); - let mut thread_ctx = ctx.clone(); + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() })]) + ); - let id = 2.to_string(); + let cmd4 = + UserCommand::UpdateUserName(second_id.clone(), "DmitiryWayToLongToSucceed".to_string()); + + let res = UserDecider::execute( + &mut event_repository, + &second_id.to_string(), + &ctx, + &cmd4, + None, + ) + .await; + + assert_matches!( + res, + Err(LoadDecideAppendError::DecideErr(UserDeciderError::UserField(UserFieldError::NameToLong(n)))) if n == "DmitiryWayToLongToSucceed".to_string() + ); - for cmd in vec![cmd3, cmd4, cmd5, cmd6] { - println!("Calling in Spawn Thread: {:?}", cmd); - let _ = UserDecider::execute(&mut thread_repo, &id, &thread_ctx, &cmd, None).await; - } - }); + let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) + .await + .expect("state is loaded"); - let id = 2.to_string(); - for cmd in vec![cmd7, cmd8, cmd9, cmd10] { - println!("Calling in Main Thread: {:?}", cmd); - let _ = UserDecider::execute(&mut event_repository2, &id, &ctx2, &cmd, None).await; - } + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() })]) + ); - // let paralell_cmds = ( - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd3, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd4, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd5, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd6, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd7, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd8, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd9, None), - // UserDecider::execute(&mut event_repository, &2.to_string(), &ctx, &cmd10, None), - // ); + let state = UserDeciderState::load(&event_repository) + .await + .expect("state is loaded"); + + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([ + (first_id.clone(), User { id: first_id, name: UserName::try_from("Mike".to_string()).unwrap() }), + (second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() }) + ]) + ); } } From be29fb668a45c3d55a74c53573ad601a5411f0e8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Fri, 6 Jan 2023 11:53:05 -0700 Subject: [PATCH 39/70] concurrency tests pass! --- Cargo.toml | 1 + src/decider.rs | 5 +- src/repository/esdb/mod.rs | 145 +++++++++++++++++++++++++++++++-- src/strategies.rs | 26 +++--- src/test_helpers/deciders.rs | 95 ++++++++++++++++++--- src/test_helpers/repository.rs | 21 ++--- 6 files changed, 248 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 48b5684..9af2d45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ actix-rt = "2.7.0" assert_matches = "1.5.0" autoincrement = "1" dotenv = "0.15.0" +futures = "0.3.25" \ No newline at end of file diff --git a/src/decider.rs b/src/decider.rs index 6be8457..31e9c2d 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -63,7 +63,7 @@ mod tests { let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); let events = ::decide(&state, &cmd).expect("Decider Success"); - if let Some(UserEvent::UserAdded(user::User { name, id })) = events.clone().first() { + if let Some(UserEvent::UserAdded(user::User { name, id, .. })) = events.clone().first() { let user_id = id.clone(); let user_name = name.clone(); @@ -76,7 +76,8 @@ mod tests { assert_matches!(state.users.get(&id).expect("User exists"), user::User { id, - name + name, + .. } if (id == &user_id && name == &user_name)); } else { panic!("Events not produced") diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 9e767e2..6eb3e1b 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -185,17 +185,36 @@ where #[cfg(test)] mod tests { + use core::time; + use futures::{ + future::{self, BoxFuture}, + FutureExt, + }; + use std::{ + collections::{HashMap, HashSet}, + thread, + }; + + use assert_matches::assert_matches; use eventstore::DeleteStreamOptions; - use crate::test_helpers::{ - deciders::user::UserEvent, repository::test_versioned_event_repository_with_streams, + use crate::{ + strategies::{LoadDecideAppend, StateFromEventRepository}, + test_helpers::{ + deciders::user::{ + Guitar, User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, + UserEvent, UserId, UserName, + }, + repository::test_versioned_event_repository_with_streams, + ValueType, + }, }; use super::*; const BASE_STREAM: &str = "test"; - async fn store_from_environment(ids: Vec) -> eventstore::Client { + async fn store_from_environment(base_stream: &str, ids: Vec) -> eventstore::Client { let _ = dotenv::dotenv().expect("File .env or Env Vars not found"); let settings = dotenv::var("ESDB_CONNECTION_STRING") .expect("ESDB to be set in env") @@ -207,7 +226,7 @@ mod tests { for id in ids { let _ = client .delete_stream( - format!("{}-{}", BASE_STREAM, id), + format!("{}-{}", base_stream, id), &DeleteStreamOptions::default(), ) .await; @@ -216,11 +235,125 @@ mod tests { client } + async fn add_guitar(base_stream: String, user_id: UserId, guitar: Guitar) { + let ctx = UserDeciderCtx::new(); + let client = store_from_environment(&base_stream, vec![]).await; + let mut event_repository = ESDBEventRepository::::new(&client, &base_stream); + + println!("Adding Guitar {:?} for user {}", &guitar.brand, &user_id); + + let cmd = UserCommand::AddGuitar(user_id, guitar.to_owned()); + + let res = UserDecider::execute( + &mut event_repository, + &user_id.to_string(), + &ctx, + &cmd, + None, + ) + .await; + + println!( + "Result for Guitar {:?} for user {}: {:?}", + &guitar.brand, &user_id, res + ); + } + #[actix_rt::test] async fn repository_spec_test() { - let client = store_from_environment(vec![1, 2]).await; + let base_stream = BASE_STREAM; + let client = store_from_environment(&base_stream, vec![1, 2]).await; + let event_repository = ESDBEventRepository::::new(&client, base_stream); - let event_repository = ESDBEventRepository::::new(&client, BASE_STREAM); let _ = test_versioned_event_repository_with_streams(event_repository).await; } + + #[actix_rt::test] + async fn test_occ() { + let base_stream = format!("{}_with_occ", BASE_STREAM); + let client = store_from_environment(&base_stream, vec![1]).await; + let mut event_repository = ESDBEventRepository::::new(&client, &base_stream); + let ctx = UserDeciderCtx::new(); + + let cmd1 = UserCommand::AddUser("Mike".to_string()); + + let first_id = ctx.current(); + let evts = UserDecider::execute( + &mut event_repository, + &first_id.to_string(), + &ctx, + &cmd1, + None, + ) + .await + .expect("command_succeeds"); + + assert_matches!( + evts.first().expect("one event"), + UserEvent::UserAdded(User { id, name, .. }) if (&first_id == id) && (name.value() == "Mike".to_string()) + ); + + let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) + .await + .expect("state is loaded"); + + assert_matches!( + state, + UserDeciderState { users } if users == HashMap::from([(first_id.clone(), User::new(first_id, UserName::try_from("Mike".to_string()).unwrap()))]) + ); + + let guitars = vec![ + Guitar { + brand: "Ibanez".to_string(), + }, + Guitar { + brand: "Gibson".to_string(), + }, + Guitar { + brand: "Fender".to_string(), + }, + Guitar { + brand: "Eastman".to_string(), + }, + Guitar { + brand: "Meyones".to_string(), + }, + Guitar { + brand: "PRS".to_string(), + }, + Guitar { + brand: "Yamaha".to_string(), + }, + Guitar { + brand: "Benedetto".to_string(), + }, + Guitar { + brand: "Strandberg".to_string(), + }, + ]; + + let futures = guitars + .iter() + .cloned() + .map(|g| add_guitar(base_stream.clone(), first_id.clone(), g).boxed()) + .collect::>>(); + + future::join_all(futures).await; + + thread::sleep(time::Duration::from_secs(1)); + + let res = UserDeciderState::load(&event_repository) + .await + .expect("Success!!"); + println!("Users result {:?}", res); + + let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) + .await + .expect("state is loaded"); + + assert_eq!( + state.users.get(&first_id).unwrap().guitars, + HashSet::from_iter(guitars.iter().cloned()) + ); + } } diff --git a/src/strategies.rs b/src/strategies.rs index 7fd993e..4850c5e 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -1,4 +1,5 @@ -use std::fmt::Debug; +use core::time; +use std::{fmt::Debug, thread}; use crate::{ decider::{DeciderWithContext, Evolver}, @@ -103,7 +104,7 @@ where let mut state = ::init(); - for _ in 1..retrys.unwrap_or(4) { + for r in 1..retrys.unwrap_or(20) { state = decider_evts .iter() .fold(state, ::evolve); @@ -120,7 +121,8 @@ where return Err(LoadDecideAppendError::RepositoryErr(e)); } Err(VersionedRepositoryError::VersionConflict(_)) => { - println!("RETRY IT!!"); + println!("RETRY #{}!!", &r); + thread::sleep(time::Duration::new(0, 100000000 * r)); let (mut catchup_evts, new_version) = event_repository .load_from_version(&version, stream) .await @@ -146,7 +148,7 @@ pub enum LoadDecideAppendError { #[cfg(test)] mod tests { - use std::{collections::HashMap, hash::Hash}; + use std::collections::HashMap; use assert_matches::assert_matches; @@ -184,7 +186,7 @@ mod tests { assert_matches!( evts.first().expect("one event"), - UserEvent::UserAdded(User { id, name }) if (&first_id == id) && (name.value() == "Mike".to_string()) + UserEvent::UserAdded(User { id, name, .. }) if (&first_id == id) && (name.value() == "Mike".to_string()) ); let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) @@ -193,7 +195,7 @@ mod tests { assert_matches!( state, - UserDeciderState { users } if users == HashMap::from([(first_id.clone(), User { id: first_id, name: UserName::try_from("Mike".to_string()).unwrap() })]) + UserDeciderState { users } if users == HashMap::from([(first_id.clone(), User::new(first_id, UserName::try_from("Mike".to_string()).unwrap()))]) ); let second_id = ctx.current(); @@ -211,7 +213,7 @@ mod tests { assert_matches!( evts.first().expect("one event"), - UserEvent::UserAdded(User { id, name }) if (&second_id == id) && (name.value() == "Dmitiry".to_string()) + UserEvent::UserAdded(User { id, name, .. }) if (&second_id == id) && (name.value() == "Dmitiry".to_string()) ); let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) @@ -220,7 +222,7 @@ mod tests { assert_matches!( state, - UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id.clone(), name: UserName::try_from("Dmitiry".to_string()).unwrap() })]) + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User::new(second_id, UserName::try_from("Dmitiry".to_string()).unwrap()))]) ); let cmd3 = UserCommand::UpdateUserName(second_id.clone(), "Dmitiry2".to_string()); @@ -245,7 +247,7 @@ mod tests { assert_matches!( state, - UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() })]) + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User::new(second_id, UserName::try_from("Dmitiry2".to_string()).unwrap()))]) ); let cmd4 = @@ -271,7 +273,7 @@ mod tests { assert_matches!( state, - UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() })]) + UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User::new(second_id, UserName::try_from("Dmitiry2".to_string()).unwrap()))]) ); let state = UserDeciderState::load(&event_repository) @@ -281,8 +283,8 @@ mod tests { assert_matches!( state, UserDeciderState { users } if users == HashMap::from([ - (first_id.clone(), User { id: first_id, name: UserName::try_from("Mike".to_string()).unwrap() }), - (second_id.clone(), User { id: second_id, name: UserName::try_from("Dmitiry2".to_string()).unwrap() }) + (first_id.clone(), User::new(first_id, UserName::try_from("Mike".to_string()).unwrap())), + (second_id.clone(), User::new(second_id, UserName::try_from("Dmitiry2".to_string()).unwrap())) ]) ); } diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 8d93d53..edbd940 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -1,7 +1,10 @@ pub(crate) mod user { - use std::{collections::HashMap, cell::RefCell, sync::{Arc, Mutex},}; + use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, + }; - use autoincrement::{AsyncIncremental, AutoIncrement, AsyncIncrement}; + use autoincrement::{AsyncIncrement, AsyncIncremental}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -15,12 +18,28 @@ pub(crate) mod user { pub(crate) struct User { pub id: UserId, pub name: UserName, + pub guitars: HashSet, + } + + impl User { + pub fn new(id: UserId, name: UserName) -> Self { + Self { + id, + name, + guitars: HashSet::new(), + } + } } pub(crate) type UserId = usize; pub(crate) type UnvalidatedUserName = String; + #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)] + pub(crate) struct Guitar { + pub brand: String, + } + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct UserName(String); @@ -91,7 +110,7 @@ pub(crate) mod user { type Err = UserDeciderError; fn decide( - _state: &UserDeciderState, + state: &UserDeciderState, cmd: &UserCommand, ) -> Result, UserDeciderError> { match cmd { @@ -99,7 +118,11 @@ pub(crate) mod user { let name = UserName::try_from(user_name) .map_err(|e| UserDeciderError::UserField(e))?; - Ok(vec![UserEvent::UserAdded(User { id: 1, name })]) + Ok(vec![UserEvent::UserAdded(User { + id: 1, + name, + guitars: HashSet::new(), + })]) } UserCommand::UpdateUserName(user_id, user_name) => { let name = UserName::try_from(user_name) @@ -107,6 +130,21 @@ pub(crate) mod user { Ok(vec![UserEvent::UserNameUpdated(user_id.to_owned(), name)]) } + UserCommand::AddGuitar(user_id, guitar) => { + println!("ADD GUITAR STATE: {:?}", &state); + let user = state + .users + .get(&user_id) + .ok_or(UserDeciderError::NotFound(*user_id))?; + if user.guitars.contains(&guitar) { + Err(UserDeciderError::AlreadyHasGuitar(guitar.to_owned())) + } else { + Ok(vec![UserEvent::UserGuitarAdded( + *user_id, + guitar.to_owned(), + )]) + } + } } } } @@ -126,6 +164,15 @@ pub(crate) mod user { state.users.get_mut(&user_id).unwrap().name = user_name.to_owned(); state } + UserEvent::UserGuitarAdded(user_id, guitar) => { + state + .users + .get_mut(&user_id) + .unwrap() + .guitars + .insert(guitar.to_owned()); + state + } } } @@ -145,7 +192,7 @@ pub(crate) mod user { fn decide( ctx: &UserDeciderCtx, - _state: &UserDeciderState, + state: &UserDeciderState, cmd: &UserCommand, ) -> Result, UserDeciderError> { match cmd { @@ -156,7 +203,11 @@ pub(crate) mod user { let name = UserName::try_from(user_name) .map_err(|e| UserDeciderError::UserField(e))?; - Ok(vec![UserEvent::UserAdded(User { id, name })]) + Ok(vec![UserEvent::UserAdded(User { + id, + name, + guitars: HashSet::new(), + })]) } UserCommand::UpdateUserName(user_id, user_name) => { let name = UserName::try_from(user_name) @@ -164,6 +215,20 @@ pub(crate) mod user { Ok(vec![UserEvent::UserNameUpdated(user_id.to_owned(), name)]) } + UserCommand::AddGuitar(user_id, guitar) => { + let user = state + .users + .get(&user_id) + .ok_or(UserDeciderError::NotFound(*user_id))?; + if user.guitars.contains(&guitar) { + Err(UserDeciderError::AlreadyHasGuitar(guitar.to_owned())) + } else { + Ok(vec![UserEvent::UserGuitarAdded( + *user_id, + guitar.to_owned(), + )]) + } + } } } } @@ -177,16 +242,13 @@ pub(crate) mod user { pub(crate) users: HashMap, } - impl UserDeciderState { + impl UserDeciderState { pub fn new(users: HashMap) -> Self { Self::default().set_users(users) } pub fn set_users(&self, users: HashMap) -> Self { - Self { - users, - ..*self - } + Self { users, ..*self } } } @@ -200,13 +262,13 @@ pub(crate) mod user { #[derive(Clone, Debug)] pub(crate) struct UserDeciderCtx { - id_sequence: Arc>> + id_sequence: Arc>>, } impl UserDeciderCtx { pub fn new() -> Self { Self { - id_sequence: Arc::new(Mutex::new(IdGen::init())) + id_sequence: Arc::new(Mutex::new(IdGen::init())), } } @@ -224,12 +286,14 @@ pub(crate) mod user { pub(crate) enum UserCommand { AddUser(UnvalidatedUserName), UpdateUserName(UserId, UnvalidatedUserName), + AddGuitar(UserId, Guitar), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub(crate) enum UserEvent { UserAdded(User), UserNameUpdated(UserId, UserName), + UserGuitarAdded(UserId, Guitar), } impl Event for UserEvent { @@ -237,6 +301,7 @@ pub(crate) mod user { match self { UserEvent::UserAdded(_) => "UserAdded".to_string(), UserEvent::UserNameUpdated(_, _) => "UserNameUpdated".to_string(), + UserEvent::UserGuitarAdded(_, _) => "UserGuitarAdded".to_string(), } } } @@ -245,6 +310,10 @@ pub(crate) mod user { pub(crate) enum UserDeciderError { #[error("Invalid user field {0:?}")] UserField(UserFieldError), + #[error("User id {0} not found")] + NotFound(UserId), + #[error("Already has guitar {0:?}")] + AlreadyHasGuitar(Guitar), } #[derive(AsyncIncremental, PartialEq, Eq, Clone, Debug)] diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 3c6404e..387bf0c 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -30,10 +30,10 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug assert_matches!(res, (v, _) if v == vec![] as Vec); let events1 = vec![ - UserEvent::UserAdded(User { - id: 1, - name: UserName::try_from("Mike").expect("Name is valid"), - }), + UserEvent::UserAdded(User::new( + 1, + UserName::try_from("Mike").expect("Name is valid"), + )), UserEvent::UserNameUpdated( 1 as UserId, UserName::try_from("Mike2").expect("Name is valid"), @@ -46,10 +46,10 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug .expect("Successful append"); let events2 = vec![ - UserEvent::UserAdded(User { - id: 2, - name: UserName::try_from("Stella").expect("Name is valid"), - }), + UserEvent::UserAdded(User::new( + 2, + UserName::try_from("Stella").expect("Name is valid"), + )), UserEvent::UserNameUpdated( 1 as UserId, UserName::try_from("Stella2").expect("Name is valid"), @@ -107,10 +107,7 @@ pub(crate) async fn test_versioned_state_repository( ) { let new_state = UserDeciderState::new(HashMap::from([( 1, - User { - id: 1, - name: UserName::try_from("Mike").expect("valid"), - }, + User::new(1, UserName::try_from("Mike").expect("valid")), )])); let version = RepositoryVersion::Exact(0); From 5caa3eb6e09fddbcfaeee5e2fe6d4afbd43030a8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Fri, 6 Jan 2023 11:56:16 -0700 Subject: [PATCH 40/70] clean up logging --- src/repository/esdb/mod.rs | 5 ----- src/strategies.rs | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 6eb3e1b..3e8682b 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -342,11 +342,6 @@ mod tests { thread::sleep(time::Duration::from_secs(1)); - let res = UserDeciderState::load(&event_repository) - .await - .expect("Success!!"); - println!("Users result {:?}", res); - let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) .await .expect("state is loaded"); diff --git a/src/strategies.rs b/src/strategies.rs index 4850c5e..9ac7311 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -118,10 +118,11 @@ where { Ok((appended_evts, _)) => return Ok(appended_evts), Err(VersionedRepositoryError::RepoErr(e)) => { + println!("Max Retries for {:?}!!", &cmd); return Err(LoadDecideAppendError::RepositoryErr(e)); } Err(VersionedRepositoryError::VersionConflict(_)) => { - println!("RETRY #{}!!", &r); + println!("RETRY #{} for {:?}!!", &r, &cmd); thread::sleep(time::Duration::new(0, 100000000 * r)); let (mut catchup_evts, new_version) = event_repository .load_from_version(&version, stream) From ea819e55cfb6d3caaae48e5eeb47002d1d3f9749 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 10 Jan 2023 11:09:10 -0700 Subject: [PATCH 41/70] Adds compose setup for tests --- docker-compose.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aef9042 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.9" +services: + redis: + image: "redislabs/rejson:latest" + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning --loadmodule /usr/lib/redis/modules/rejson.so + volumes: + - redis:/data + eventstore.db: + image: eventstore/eventstore:20.10.2-buster-slim + environment: + - EVENTSTORE_CLUSTER_SIZE=1 + - EVENTSTORE_RUN_PROJECTIONS=All + - EVENTSTORE_START_STANDARD_PROJECTIONS=true + - EVENTSTORE_EXT_TCP_PORT=1113 + - EVENTSTORE_HTTP_PORT=2113 + - EVENTSTORE_INSECURE=true + - EVENTSTORE_ENABLE_EXTERNAL_TCP=true + - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true + ports: + - "1113:1113" + - "2113:2113" + volumes: + - type: volume + source: eventstore-volume-data + target: /var/lib/eventstore + - type: volume + source: eventstore-volume-logs + target: /var/log/eventstore + +volumes: + redis: + driver: local + eventstore-volume-data: + driver: local + eventstore-volume-logs: + driver: local From 73e4c5c4d5689c454ac91cf958af30e170471573 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 12 Jan 2023 12:41:50 -0700 Subject: [PATCH 42/70] version bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9af2d45..0abeca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.8" +version = "1.0.0-alpha.9" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From c5e0f44deffca01b9a12e02947a439ab4425add4 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 12 Jan 2023 13:14:11 -0700 Subject: [PATCH 43/70] make new public --- src/repository/in_memory/state/versioned.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 6c3e994..6b8d05f 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -19,7 +19,7 @@ impl InMemoryStateRepository where State: Debug, { - fn new(state: State) -> Self { + pub fn new(state: State) -> Self { Self { state: Arc::new(Mutex::new(VersionedState::new(state))), } From c647b800deca3b096d769ca5b736dc206cf463aa Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 17 Jan 2023 17:16:42 -0700 Subject: [PATCH 44/70] bump version - added StreamState as argument to execute --- Cargo.toml | 6 +-- src/decider.rs | 3 ++ src/repository/esdb/mod.rs | 26 ++++------- src/repository/event.rs | 8 ++++ src/strategies.rs | 90 ++++++++++++++++++++---------------- src/test_helpers/deciders.rs | 18 ++++++++ 6 files changed, 91 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0abeca7..70ad21d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.9" +version = "1.0.0-alpha.10" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,9 +12,9 @@ esdb = ["dep:eventstore", "dep:uuid_0_8_2", "dep:serde_json"] [dependencies] async-trait = "0.1.53" -eventstore = { version = "2.1.1", optional = true } +eventstore = { version = "2.1.1", optional = true } serde = { version = "1.0.136", features = ["derive"] } -serde_json = { version = "1.0.81", optional = true } +serde_json = { version = "1.0.81", optional = true } thiserror = "1.0" # Use 0.8.2 only here because of a weird deps bug with eventstore # 1.x uuids are not compatible with eventstore internal uuids diff --git a/src/decider.rs b/src/decider.rs index 31e9c2d..203c42b 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,5 +1,8 @@ pub trait Event { + type EntityId; + fn event_type(&self) -> String; + fn get_id(&self) -> Self::EntityId; } pub trait Decider: Evolver { diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 3e8682b..2041a16 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -177,12 +177,6 @@ where } } -// impl Into> for Error { -// fn into(self) -> VersionedRepositoryError { -// todo!() -// } -// } - #[cfg(test)] mod tests { use core::time; @@ -199,7 +193,7 @@ mod tests { use eventstore::DeleteStreamOptions; use crate::{ - strategies::{LoadDecideAppend, StateFromEventRepository}, + strategies::{LoadDecideAppend, StateFromEventRepository, StreamState}, test_helpers::{ deciders::user::{ Guitar, User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, @@ -246,7 +240,7 @@ mod tests { let res = UserDecider::execute( &mut event_repository, - &user_id.to_string(), + &StreamState::Existing(user_id.to_string()), &ctx, &cmd, None, @@ -277,16 +271,12 @@ mod tests { let cmd1 = UserCommand::AddUser("Mike".to_string()); - let first_id = ctx.current(); - let evts = UserDecider::execute( - &mut event_repository, - &first_id.to_string(), - &ctx, - &cmd1, - None, - ) - .await - .expect("command_succeeds"); + let evts = + UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd1, None) + .await + .expect("command_succeeds"); + + let first_id = evts.first().unwrap().get_id(); assert_matches!( evts.first().expect("one event"), diff --git a/src/repository/event.rs b/src/repository/event.rs index bc9e35d..2e1e85a 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -74,6 +74,14 @@ pub enum VersionedRepositoryError { RepoErr(RepoErr), } +pub trait SteamIdFromEvent: Sized { + fn from(e: Evt) -> Self { + Self::event_entity_id_into(e.get_id()) + } + + fn event_entity_id_into(id: ::EntityId) -> Self; +} + #[derive(Debug)] pub struct VersionDiff { expected: RepositoryVersion, diff --git a/src/strategies.rs b/src/strategies.rs index 9ac7311..ebb205c 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -2,11 +2,11 @@ use core::time; use std::{fmt::Debug, thread}; use crate::{ - decider::{DeciderWithContext, Evolver}, - repository::{self, event::VersionedRepositoryError}, + decider::{DeciderWithContext, Event, Evolver}, + repository::{self, event::VersionedRepositoryError, RepositoryVersion}, }; use async_trait::async_trait; -use repository::event::VersionedEventRepositoryWithStreams; +use repository::event::{SteamIdFromEvent, VersionedEventRepositoryWithStreams}; #[async_trait] pub trait StateFromEventRepository @@ -61,7 +61,7 @@ where ::State: Send + Sync + Debug, ::Ctx: Send + Sync + Debug, ::Cmd: Send + Sync + Debug, - ::Evt: Send + Sync + Debug, + ::Evt: Clone + Send + Sync + Debug, ::Err: Send + Sync + Debug, { type Decide: DeciderWithContext + Send + Sync; @@ -83,7 +83,7 @@ where StreamId = StreamId, > + Send + Sync), - stream_id: &StreamId, + stream_id: &StreamState, ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, retrys: Option, @@ -93,14 +93,23 @@ where > where RepoErr: Debug + Send + Sync, - StreamId: Send + Sync, + StreamId: Send + + Sync + + Clone + + SteamIdFromEvent<<::Decide as Evolver>::Evt>, { - let stream = Some(stream_id); + let (mut decider_evts, mut version) = match stream_id { + StreamState::New => (vec![], RepositoryVersion::NoStream), + StreamState::Existing(sid) => event_repository + .load(Some(sid)) + .await + .map_err(Self::to_lda_error)?, + }; - let (mut decider_evts, mut version) = event_repository - .load(stream) - .await - .map_err(Self::to_lda_error)?; + // let (mut decider_evts, mut version) = event_repository + // .load(stream) + // .await + // .map_err(Self::to_lda_error)?; let mut state = ::init(); @@ -112,10 +121,17 @@ where let new_evts = ::decide(&ctx, &state, &cmd) .map_err(LoadDecideAppendError::DecideErr)?; - match event_repository - .append(&version, stream_id, &new_evts) - .await - { + let stream = match stream_id { + StreamState::New => match new_evts.first() { + None => { + return Ok(vec![]); + } + Some(evt) => StreamId::from(evt.clone()), + }, + StreamState::Existing(sid) => sid.clone(), + }; + + match event_repository.append(&version, &stream, &new_evts).await { Ok((appended_evts, _)) => return Ok(appended_evts), Err(VersionedRepositoryError::RepoErr(e)) => { println!("Max Retries for {:?}!!", &cmd); @@ -125,7 +141,7 @@ where println!("RETRY #{} for {:?}!!", &r, &cmd); thread::sleep(time::Duration::new(0, 100000000 * r)); let (mut catchup_evts, new_version) = event_repository - .load_from_version(&version, stream) + .load_from_version(&version, Some(&stream)) .await .map_err(Self::to_lda_error)?; @@ -139,6 +155,11 @@ where } } +pub enum StreamState { + New, + Existing(T), +} + #[derive(Debug)] pub enum LoadDecideAppendError { OccMaxRetries, @@ -174,16 +195,12 @@ mod tests { let cmd1 = UserCommand::AddUser("Mike".to_string()); - let first_id = ctx.current(); - let evts = UserDecider::execute( - &mut event_repository, - &first_id.to_string(), - &ctx, - &cmd1, - None, - ) - .await - .expect("command_succeeds"); + let evts = + UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd1, None) + .await + .expect("command_succeeds"); + + let first_id = evts.first().unwrap().get_id(); assert_matches!( evts.first().expect("one event"), @@ -199,18 +216,13 @@ mod tests { UserDeciderState { users } if users == HashMap::from([(first_id.clone(), User::new(first_id, UserName::try_from("Mike".to_string()).unwrap()))]) ); - let second_id = ctx.current(); - let cmd2 = UserCommand::AddUser("Dmitiry".to_string()); - let evts = UserDecider::execute( - &mut event_repository, - &second_id.to_string(), - &ctx, - &cmd2, - None, - ) - .await - .expect("command_succeeds"); + let evts = + UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd2, None) + .await + .expect("command_succeeds"); + + let second_id = evts.first().unwrap().get_id(); assert_matches!( evts.first().expect("one event"), @@ -229,7 +241,7 @@ mod tests { let cmd3 = UserCommand::UpdateUserName(second_id.clone(), "Dmitiry2".to_string()); let evts = UserDecider::execute( &mut event_repository, - &second_id.to_string(), + &StreamState::Existing(second_id.to_string()), &ctx, &cmd3, None, @@ -256,7 +268,7 @@ mod tests { let res = UserDecider::execute( &mut event_repository, - &second_id.to_string(), + &StreamState::Existing(second_id.to_string()), &ctx, &cmd4, None, diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index edbd940..02c0db3 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -10,6 +10,7 @@ pub(crate) mod user { use crate::{ decider::{Decider, DeciderWithContext, Event, Evolver}, + repository::event::SteamIdFromEvent, strategies::{LoadDecideAppend, StateFromEventRepository}, test_helpers::ValueType, }; @@ -297,6 +298,8 @@ pub(crate) mod user { } impl Event for UserEvent { + type EntityId = UserId; + fn event_type(&self) -> String { match self { UserEvent::UserAdded(_) => "UserAdded".to_string(), @@ -304,6 +307,21 @@ pub(crate) mod user { UserEvent::UserGuitarAdded(_, _) => "UserGuitarAdded".to_string(), } } + + fn get_id(&self) -> Self::EntityId { + match self { + UserEvent::UserAdded(User { id, .. }) => id, + UserEvent::UserNameUpdated(id, _) => id, + UserEvent::UserGuitarAdded(id, _) => id, + } + .to_owned() + } + } + + impl SteamIdFromEvent for String { + fn event_entity_id_into(id: ::EntityId) -> Self { + id.to_string() + } } #[derive(Debug, Error)] From 2810528597cd394a22a2f5fc9e752fcfb75fad53 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 18 Jan 2023 13:11:31 -0700 Subject: [PATCH 45/70] typo and add randomized base stream to ESDB tests --- Cargo.toml | 3 ++- src/repository/esdb/mod.rs | 18 +++++++++++------- src/repository/event.rs | 2 +- src/strategies.rs | 4 ++-- src/test_helpers/deciders.rs | 4 ++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70ad21d..fc25bef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.10" +version = "1.0.0-alpha.12" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,6 +23,7 @@ uuid_0_8_2 = { package = "uuid", version = "0.8.2", features = ["v4", "serde"], [dev-dependencies] actix-rt = "2.7.0" assert_matches = "1.5.0" +const-random = "0.1.15" autoincrement = "1" dotenv = "0.15.0" futures = "0.3.25" \ No newline at end of file diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 2041a16..c67373c 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -179,6 +179,7 @@ where #[cfg(test)] mod tests { + use const_random::const_random; use core::time; use futures::{ future::{self, BoxFuture}, @@ -206,7 +207,7 @@ mod tests { use super::*; - const BASE_STREAM: &str = "test"; + const BASE_STREAM: u32 = const_random!(u32); async fn store_from_environment(base_stream: &str, ids: Vec) -> eventstore::Client { let _ = dotenv::dotenv().expect("File .env or Env Vars not found"); @@ -231,8 +232,9 @@ mod tests { async fn add_guitar(base_stream: String, user_id: UserId, guitar: Guitar) { let ctx = UserDeciderCtx::new(); - let client = store_from_environment(&base_stream, vec![]).await; - let mut event_repository = ESDBEventRepository::::new(&client, &base_stream); + let client = store_from_environment(&base_stream.to_string(), vec![]).await; + let mut event_repository = + ESDBEventRepository::::new(&client, &base_stream.to_string()); println!("Adding Guitar {:?} for user {}", &guitar.brand, &user_id); @@ -256,8 +258,9 @@ mod tests { #[actix_rt::test] async fn repository_spec_test() { let base_stream = BASE_STREAM; - let client = store_from_environment(&base_stream, vec![1, 2]).await; - let event_repository = ESDBEventRepository::::new(&client, base_stream); + let client = store_from_environment(&base_stream.to_string(), vec![1, 2]).await; + let event_repository = + ESDBEventRepository::::new(&client, &base_stream.to_string()); let _ = test_versioned_event_repository_with_streams(event_repository).await; } @@ -265,8 +268,9 @@ mod tests { #[actix_rt::test] async fn test_occ() { let base_stream = format!("{}_with_occ", BASE_STREAM); - let client = store_from_environment(&base_stream, vec![1]).await; - let mut event_repository = ESDBEventRepository::::new(&client, &base_stream); + let client = store_from_environment(&base_stream.to_string(), vec![1]).await; + let mut event_repository = + ESDBEventRepository::::new(&client, &base_stream.to_string()); let ctx = UserDeciderCtx::new(); let cmd1 = UserCommand::AddUser("Mike".to_string()); diff --git a/src/repository/event.rs b/src/repository/event.rs index 2e1e85a..3e5c3d7 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -74,7 +74,7 @@ pub enum VersionedRepositoryError { RepoErr(RepoErr), } -pub trait SteamIdFromEvent: Sized { +pub trait StreamIdFromEvent: Sized { fn from(e: Evt) -> Self { Self::event_entity_id_into(e.get_id()) } diff --git a/src/strategies.rs b/src/strategies.rs index ebb205c..3b58e80 100644 --- a/src/strategies.rs +++ b/src/strategies.rs @@ -6,7 +6,7 @@ use crate::{ repository::{self, event::VersionedRepositoryError, RepositoryVersion}, }; use async_trait::async_trait; -use repository::event::{SteamIdFromEvent, VersionedEventRepositoryWithStreams}; +use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; #[async_trait] pub trait StateFromEventRepository @@ -96,7 +96,7 @@ where StreamId: Send + Sync + Clone - + SteamIdFromEvent<<::Decide as Evolver>::Evt>, + + StreamIdFromEvent<<::Decide as Evolver>::Evt>, { let (mut decider_evts, mut version) = match stream_id { StreamState::New => (vec![], RepositoryVersion::NoStream), diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 02c0db3..b769f54 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -10,7 +10,7 @@ pub(crate) mod user { use crate::{ decider::{Decider, DeciderWithContext, Event, Evolver}, - repository::event::SteamIdFromEvent, + repository::event::StreamIdFromEvent, strategies::{LoadDecideAppend, StateFromEventRepository}, test_helpers::ValueType, }; @@ -318,7 +318,7 @@ pub(crate) mod user { } } - impl SteamIdFromEvent for String { + impl StreamIdFromEvent for String { fn event_entity_id_into(id: ::EntityId) -> Self { id.to_string() } From 25c0324661db8b9853fbbfd495003dcd41d1a98f Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 8 Mar 2023 18:12:08 -0700 Subject: [PATCH 46/70] reloacate strategies to mod to diversify strategies without polluting fn/trait names --- src/repository/esdb/mod.rs | 17 ++++++++--------- src/{strategies.rs => strategies/mod.rs} | 10 +++------- 2 files changed, 11 insertions(+), 16 deletions(-) rename src/{strategies.rs => strategies/mod.rs} (97%) diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index c67373c..9625631 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -193,16 +193,15 @@ mod tests { use assert_matches::assert_matches; use eventstore::DeleteStreamOptions; - use crate::{ - strategies::{LoadDecideAppend, StateFromEventRepository, StreamState}, - test_helpers::{ - deciders::user::{ - Guitar, User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, - UserEvent, UserId, UserName, - }, - repository::test_versioned_event_repository_with_streams, - ValueType, + use crate::strategies::{LoadDecideAppend, StateFromEventRepository, StreamState}; + + use crate::test_helpers::{ + deciders::user::{ + Guitar, User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderState, UserEvent, + UserId, UserName, }, + repository::test_versioned_event_repository_with_streams, + ValueType, }; use super::*; diff --git a/src/strategies.rs b/src/strategies/mod.rs similarity index 97% rename from src/strategies.rs rename to src/strategies/mod.rs index 3b58e80..39f5114 100644 --- a/src/strategies.rs +++ b/src/strategies/mod.rs @@ -2,14 +2,14 @@ use core::time; use std::{fmt::Debug, thread}; use crate::{ - decider::{DeciderWithContext, Event, Evolver}, + decider::{DeciderWithContext, Evolver}, repository::{self, event::VersionedRepositoryError, RepositoryVersion}, }; use async_trait::async_trait; use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; #[async_trait] -pub trait StateFromEventRepository +pub(crate) trait StateFromEventRepository where ::Evt: Send + Sync + Debug, ::State: Send + Sync + Debug, @@ -106,11 +106,6 @@ where .map_err(Self::to_lda_error)?, }; - // let (mut decider_evts, mut version) = event_repository - // .load(stream) - // .await - // .map_err(Self::to_lda_error)?; - let mut state = ::init(); for r in 1..retrys.unwrap_or(20) { @@ -175,6 +170,7 @@ mod tests { use assert_matches::assert_matches; use crate::{ + decider::Event, repository::in_memory::versioned_with_streams::InMemoryEventRepository, test_helpers::{ deciders::user::{ From d71c4448e63f5142cfe0ae9ecde65e18def2fa50 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sat, 11 Mar 2023 15:43:57 -0700 Subject: [PATCH 47/70] epoch - command response strategy --- src/strategies/mod.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 39f5114..eb3d7a1 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -2,7 +2,7 @@ use core::time; use std::{fmt::Debug, thread}; use crate::{ - decider::{DeciderWithContext, Evolver}, + decider::{Decider, DeciderWithContext, Evolver}, repository::{self, event::VersionedRepositoryError, RepositoryVersion}, }; use async_trait::async_trait; @@ -150,6 +150,41 @@ where } } +pub struct CommandResponse( + ::Cmd, + Vec<::Evt>, + ::State, +); + +#[async_trait] +pub trait DecideEvolveWithCommandResponse +where + ::State: Send + Sync + Debug + Clone, + ::Ctx: Send + Sync + Debug, + ::Cmd: Send + Sync + Debug, + ::Evt: Clone + Send + Sync + Debug, + ::Err: Send + Sync + Debug, +{ + type Decide: DeciderWithContext + Send + Sync; + + async fn response( + ctx: &<::Decide as DeciderWithContext>::Ctx, + state: &<::Decide as Evolver>::State, + cmd: <::Decide as DeciderWithContext>::Cmd, + ) -> Result< + CommandResponse<::Decide>, + <::Decide as DeciderWithContext>::Err, + > { + let evts = ::decide(ctx, &state.clone(), &cmd)?; + let state = evts + .iter() + .fold(state.clone(), ::evolve); + + Ok(CommandResponse(cmd, evts, state)) + // todo!() + } +} + pub enum StreamState { New, Existing(T), From 3258ab483e64ccecb077902b78c42f3c0a86aad8 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sat, 11 Mar 2023 15:47:01 -0700 Subject: [PATCH 48/70] reorg arguments - owned first --- src/strategies/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index eb3d7a1..4a135ee 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -168,9 +168,9 @@ where type Decide: DeciderWithContext + Send + Sync; async fn response( - ctx: &<::Decide as DeciderWithContext>::Ctx, - state: &<::Decide as Evolver>::State, cmd: <::Decide as DeciderWithContext>::Cmd, + state: &<::Decide as Evolver>::State, + ctx: &<::Decide as DeciderWithContext>::Ctx, ) -> Result< CommandResponse<::Decide>, <::Decide as DeciderWithContext>::Err, @@ -181,7 +181,6 @@ where .fold(state.clone(), ::evolve); Ok(CommandResponse(cmd, evts, state)) - // todo!() } } From c75afac0875345bee9c799450c5e96ccb0302c07 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sat, 11 Mar 2023 15:54:08 -0700 Subject: [PATCH 49/70] unnecessary clone --- src/strategies/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 4a135ee..c2b536e 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -175,10 +175,10 @@ where CommandResponse<::Decide>, <::Decide as DeciderWithContext>::Err, > { - let evts = ::decide(ctx, &state.clone(), &cmd)?; + let evts = ::decide(ctx, state, &cmd)?; let state = evts .iter() - .fold(state.clone(), ::evolve); + .fold(state.to_owned(), ::evolve); Ok(CommandResponse(cmd, evts, state)) } From 35ba0e31f15db5447f33907734201baaaf546c92 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 13 Mar 2023 13:50:15 -0600 Subject: [PATCH 50/70] test for DecideEvolveWithCommandResponse - clippy cleanup --- src/decider.rs | 14 +++++++------ src/repository/esdb/mod.rs | 4 ++-- src/repository/in_memory/simple.rs | 9 +++++++++ .../in_memory/versioned_with_streams/mod.rs | 20 +++++++++---------- src/strategies/mod.rs | 18 ++++++++++++++--- src/test_helpers/deciders.rs | 7 ++++++- 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 203c42b..6031eca 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + pub trait Event { type EntityId; @@ -12,10 +14,10 @@ pub trait Decider: Evolver { fn decide(state: &Self::State, cmd: &Self::Cmd) -> Result, Self::Err>; } -pub trait DeciderWithContext: Evolver { - type Ctx; - type Cmd: Send + Sync; - type Err; +pub trait DeciderWithContext: Evolver + Debug { + type Ctx: std::fmt::Debug; + type Cmd: Send + Sync + std::fmt::Debug; + type Err: std::fmt::Debug; fn decide( ctx: &Self::Ctx, @@ -25,8 +27,8 @@ pub trait DeciderWithContext: Evolver { } pub trait Evolver { - type State; - type Evt: Event; + type State: Debug; + type Evt: Event + Debug; fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; fn init() -> Self::State; } diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 9625631..407a5aa 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -26,7 +26,7 @@ pub struct ESDBEventRepository { _hidden: PhantomData, } -impl<'a, E> ESDBEventRepository { +impl ESDBEventRepository { pub fn new(client: &Client, stream_name: &str) -> Self { Self { client: client.to_owned(), @@ -93,7 +93,7 @@ where self.get_stream(id), &ReadStreamOptions::default() .resolve_link_tos() - .position(Self::version_to_esdb_position(&version)), + .position(Self::version_to_esdb_position(version)), ) .await .map_err(Error::ESDBGeneral) diff --git a/src/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs index 0ed70c2..373ba09 100644 --- a/src/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -68,6 +68,15 @@ where State: Default + Send + Sync + Debug + Clone, { pub fn new() -> Self { + Self::default() + } +} + +impl Default for InMemoryStateRepository +where + State: Default, +{ + fn default() -> Self { Self { state: State::default(), } diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index a8cc06c..a6b078c 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -28,7 +28,7 @@ where state: HashMap>>>, } -impl<'a, E> InMemoryEventRepository +impl InMemoryEventRepository where E: Event + Sync + Send + Debug, { @@ -47,12 +47,12 @@ where if let Some(id) = stream_id { format!("{}/{}", self.stream_name, id) } else { - format!("{}", self.stream_name) + self.stream_name.to_string() } } fn get_stream_or_new(&mut self, key: &str) -> &Arc>> { - if let None = self.state.get(key) { + if self.state.get(key).is_none() { self.state.insert( key.to_owned(), Arc::new(Mutex::new(InMemoryEventRepositoryState::new())), @@ -125,7 +125,7 @@ where if stream.position == Self::index_from_version(version) { stream.events.extend(events.clone()); let position = stream.events.len() - 1; - stream.position = position.clone(); + stream.position = position; drop(stream); // Drop mutable reference so we can pull another and write to sub_stream @@ -148,13 +148,11 @@ where } } -impl Into> for Error { - fn into(self) -> VersionedRepositoryError { - if let Error::VersionConflict(diff) = self { - VersionedRepositoryError::VersionConflict(diff) - } else { - VersionedRepositoryError::RepoErr(self) - } +impl From for VersionedRepositoryError { + fn from(value: Error) -> Self { + let Error::VersionConflict(diff) = value; + + VersionedRepositoryError::VersionConflict(diff) } } diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index c2b536e..6f13e2b 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -2,7 +2,7 @@ use core::time; use std::{fmt::Debug, thread}; use crate::{ - decider::{Decider, DeciderWithContext, Evolver}, + decider::{DeciderWithContext, Evolver}, repository::{self, event::VersionedRepositoryError, RepositoryVersion}, }; use async_trait::async_trait; @@ -113,7 +113,7 @@ where .iter() .fold(state, ::evolve); - let new_evts = ::decide(&ctx, &state, &cmd) + let new_evts = ::decide(ctx, &state, cmd) .map_err(LoadDecideAppendError::DecideErr)?; let stream = match stream_id { @@ -150,7 +150,8 @@ where } } -pub struct CommandResponse( +#[derive(Debug)] +pub struct CommandResponse( ::Cmd, Vec<::Evt>, ::State, @@ -331,4 +332,15 @@ mod tests { ]) ); } + + #[actix_rt::test] + async fn decide_evolve_with_command_response() { + let ctx = UserDeciderCtx::new(); + let state = UserDeciderState::default(); + + let cmd1 = UserCommand::AddUser("Mike".to_string()); + let res = UserDecider::response(cmd1, &state, &ctx).await; + + assert_matches!(res, Ok(CommandResponse(UserCommand::AddUser(_), _, _))); + } } diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index b769f54..e6e75b1 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -11,7 +11,7 @@ pub(crate) mod user { use crate::{ decider::{Decider, DeciderWithContext, Event, Evolver}, repository::event::StreamIdFromEvent, - strategies::{LoadDecideAppend, StateFromEventRepository}, + strategies::{DecideEvolveWithCommandResponse, LoadDecideAppend, StateFromEventRepository}, test_helpers::ValueType, }; @@ -103,6 +103,7 @@ pub(crate) mod user { NameToLong(String), } + #[derive(Debug)] pub(crate) struct UserDecider; impl Decider for UserDecider { @@ -238,6 +239,10 @@ pub(crate) mod user { type Decide = Self; } + impl DecideEvolveWithCommandResponse for UserDecider { + type Decide = Self; + } + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct UserDeciderState { pub(crate) users: HashMap, From d56720ad5936ba6bee31696539b5478f6fed0a0e Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Mon, 13 Mar 2023 17:30:05 -0600 Subject: [PATCH 51/70] fix pub crate on StateFromEventRepository - pub so we can reach it outside of crate --- src/strategies/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 6f13e2b..69fe9f8 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; #[async_trait] -pub(crate) trait StateFromEventRepository +pub trait StateFromEventRepository where ::Evt: Send + Sync + Debug, ::State: Send + Sync + Debug, From 50f47d9793c7fbe51ec2abb32f8d69755d5d1c49 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 22 Mar 2023 12:59:15 -0600 Subject: [PATCH 52/70] state based persistance strategy ReifyDecideSave --- src/strategies/mod.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 69fe9f8..6acf939 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -3,7 +3,9 @@ use std::{fmt::Debug, thread}; use crate::{ decider::{DeciderWithContext, Evolver}, - repository::{self, event::VersionedRepositoryError, RepositoryVersion}, + repository::{ + self, event::VersionedRepositoryError, state::VersionedStateRepository, RepositoryVersion, + }, }; use async_trait::async_trait; use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; @@ -150,6 +152,26 @@ where } } +#[async_trait] +pub trait ReifyDecideSave +where + <::Decide as DeciderWithContext>::Err: Send + Sync, +{ + type Decide: DeciderWithContext + Send + Sync; + + async fn execute<'a, RepoErr>( + state_repository: &mut (impl VersionedStateRepository< + ::State, + RepoErr, + >), + ctx: &<::Decide as DeciderWithContext>::Ctx, + cmd: &<::Decide as DeciderWithContext>::Cmd, + ) -> Result< + Vec<::Evt>, + ReifyDecideSaveError<::Err, RepoErr>, + >; +} + #[derive(Debug)] pub struct CommandResponse( ::Cmd, @@ -198,6 +220,12 @@ pub enum LoadDecideAppendError { RepositoryErr(RepoErr), } +#[derive(Debug)] +pub enum ReifyDecideSaveError { + DecideErr(DecideErr), + RepositoryErr(RepoErr), +} + #[cfg(test)] mod tests { use std::collections::HashMap; From dadf0a4410bc0a765398399c075e23072ccc6fde Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 22 Mar 2023 13:18:50 -0600 Subject: [PATCH 53/70] more trait bounds --- src/strategies/mod.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 6acf939..edd1f51 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -155,7 +155,11 @@ where #[async_trait] pub trait ReifyDecideSave where + <::Decide as DeciderWithContext>::Ctx: Send + Sync, + <::Decide as DeciderWithContext>::Cmd: Send + Sync, <::Decide as DeciderWithContext>::Err: Send + Sync, + <::Decide as Evolver>::Evt: Send + Sync, + <::Decide as Evolver>::State: Send + Sync, { type Decide: DeciderWithContext + Send + Sync; @@ -163,13 +167,18 @@ where state_repository: &mut (impl VersionedStateRepository< ::State, RepoErr, - >), + > + Send + Sync), ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, ) -> Result< Vec<::Evt>, ReifyDecideSaveError<::Err, RepoErr>, - >; + > + where + RepoErr: Send + Sync + { + todo!() + } } #[derive(Debug)] From ee1ac5abb82dc2b5f555124f33240527d305b54f Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 22 Mar 2023 13:38:09 -0600 Subject: [PATCH 54/70] load state/version from in memory state repo --- src/strategies/mod.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index edd1f51..23a0d87 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -163,11 +163,14 @@ where { type Decide: DeciderWithContext + Send + Sync; + fn repo_to_rds_error( + err: RepoErr, + ) -> ReifyDecideSaveError; + async fn execute<'a, RepoErr>( - state_repository: &mut (impl VersionedStateRepository< - ::State, - RepoErr, - > + Send + Sync), + state_repository: &mut (impl VersionedStateRepository<::State, RepoErr> + + Send + + Sync), ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, ) -> Result< @@ -175,8 +178,12 @@ where ReifyDecideSaveError<::Err, RepoErr>, > where - RepoErr: Send + Sync + RepoErr: Send + Sync, { + let (state, version) = state_repository.reify().await.map_err( + Self::repo_to_rds_error::<::Err, RepoErr>, + )?; + todo!() } } From 12efe9d85366a487d04e9613c04dacba725b2160 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 22 Mar 2023 16:10:24 -0600 Subject: [PATCH 55/70] add lifetimes to versioned state repository trait to make it send + sync safe for strategies --- src/repository/in_memory/state/versioned.rs | 2 +- src/repository/state.rs | 14 ++++++++-- src/strategies/mod.rs | 31 +++++++++++++++------ src/test_helpers/repository.rs | 3 +- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 6b8d05f..0bae021 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -54,7 +54,7 @@ where } #[async_trait] -impl VersionedStateRepository for InMemoryStateRepository +impl<'a, State> VersionedStateRepository<'a, State, Error> for InMemoryStateRepository where State: Debug + Clone + Send + Sync, { diff --git a/src/repository/state.rs b/src/repository/state.rs index 19d68d0..38d9820 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -7,9 +7,17 @@ pub trait StateRepository { } #[async_trait] -pub trait VersionedStateRepository { - type Version: Eq; +pub trait VersionedStateRepository<'a, State, Err> +where + State: Send + Sync, + Err: Send + Sync, +{ + type Version: Eq + Send + Sync; async fn reify(&self) -> Result<(State, Self::Version), Err>; - async fn save(&mut self, version: &Self::Version, state: &State) -> Result; + async fn save(&mut self, version: &Self::Version, state: &State) -> Result + where + 'a: 'async_trait, + State: 'async_trait, + Err: 'async_trait; } diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 23a0d87..4b803ac 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -159,20 +159,17 @@ where <::Decide as DeciderWithContext>::Cmd: Send + Sync, <::Decide as DeciderWithContext>::Err: Send + Sync, <::Decide as Evolver>::Evt: Send + Sync, - <::Decide as Evolver>::State: Send + Sync, + <::Decide as Evolver>::State: Send + Sync + Clone, { type Decide: DeciderWithContext + Send + Sync; - fn repo_to_rds_error( - err: RepoErr, - ) -> ReifyDecideSaveError; - async fn execute<'a, RepoErr>( - state_repository: &mut (impl VersionedStateRepository<::State, RepoErr> + state_repository: &mut (impl VersionedStateRepository<'a, ::State, RepoErr> + Send + Sync), ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, + retrys: Option, ) -> Result< Vec<::Evt>, ReifyDecideSaveError<::Err, RepoErr>, @@ -180,9 +177,25 @@ where where RepoErr: Send + Sync, { - let (state, version) = state_repository.reify().await.map_err( - Self::repo_to_rds_error::<::Err, RepoErr>, - )?; + let (mut state, mut version) = state_repository + .reify() + .await + .map_err(ReifyDecideSaveError::RepositoryErr)?; + + for r in 1..retrys.unwrap_or(20) { + let local_state = state.clone(); + let evts = ::decide(ctx, &local_state, cmd) + .map_err(ReifyDecideSaveError::DecideErr)?; + + let new_state = evts + .iter() + .fold(local_state, ::evolve); + + match state_repository.save(&version, &new_state).await { + Ok(_) => todo!(), + Err(_) => todo!(), + }; + } todo!() } diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 387bf0c..1a7b9dd 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -98,8 +98,9 @@ pub(crate) async fn test_versioned_event_repository_with_streams<'a, Err: Debug assert_eq!(latest_events.first().unwrap(), new_events.first().unwrap()); } -pub(crate) async fn test_versioned_state_repository( +pub(crate) async fn test_versioned_state_repository<'a, Err: Debug + Send + Sync>( mut state_repository: impl VersionedStateRepository< + 'a, UserDeciderState, Err, Version = RepositoryVersion, From ade9a7ec9dd25c0222db3443786cfda96d43bdac Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Wed, 22 Mar 2023 17:00:45 -0600 Subject: [PATCH 56/70] happy path for ReifyDecideSave strategy --- src/strategies/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 4b803ac..5be7266 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -171,7 +171,7 @@ where cmd: &<::Decide as DeciderWithContext>::Cmd, retrys: Option, ) -> Result< - Vec<::Evt>, + ::State, ReifyDecideSaveError<::Err, RepoErr>, > where @@ -192,10 +192,10 @@ where .fold(local_state, ::evolve); match state_repository.save(&version, &new_state).await { - Ok(_) => todo!(), + Ok(s) => return Ok(s), Err(_) => todo!(), - }; - } + } + }; todo!() } From 17460f89f3f0d4a03ce336c876a91d3bb4cfd54c Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 23 Mar 2023 14:04:41 -0600 Subject: [PATCH 57/70] use versioned repository error in state based storage traits --- src/repository/in_memory/state/versioned.rs | 32 +++++++++++++++------ src/repository/state.rs | 8 +++++- src/strategies/mod.rs | 6 ++-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 0bae021..a64bf8d 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -5,7 +5,11 @@ use std::{ use async_trait::async_trait; -use crate::repository::{state::VersionedStateRepository, RepositoryVersion}; +use crate::repository::{ + event::{VersionDiff, VersionedRepositoryError}, + state::VersionedStateRepository, + RepositoryVersion, +}; #[derive(Debug, Clone)] pub struct InMemoryStateRepository @@ -25,30 +29,39 @@ where } } - fn version_to_usize(version: &RepositoryVersion) -> Result { + fn version_to_usize( + version: &RepositoryVersion, + ) -> Result> { if let RepositoryVersion::Exact(exact) = version { Ok(exact.to_owned()) } else { - Err(Error::ExactStreamVersionMustBeKnown) + Err(VersionedRepositoryError::RepoErr( + Error::ExactStreamVersionMustBeKnown, + )) } } fn version_check( current: &RepositoryVersion, incoming: &RepositoryVersion, - ) -> Result<(), Error> { + ) -> Result<(), VersionedRepositoryError> { if let &RepositoryVersion::StreamExists = current { return if let &RepositoryVersion::Exact(_) = incoming { Ok(()) } else { - Err(Error::ExactStreamVersionMustBeKnown) + Err(VersionedRepositoryError::RepoErr( + Error::ExactStreamVersionMustBeKnown, + )) }; } if Self::version_to_usize(current)? < Self::version_to_usize(incoming)? { Ok(()) } else { - Err(Error::VersionOutOfDate) + Err(VersionedRepositoryError::VersionConflict(VersionDiff::new( + current.to_owned(), + incoming.to_owned(), + ))) } } } @@ -66,7 +79,11 @@ where Ok((handle.data.to_owned(), handle.version)) } - async fn save(&mut self, version: &Self::Version, state: &State) -> Result { + async fn save( + &mut self, + version: &Self::Version, + state: &State, + ) -> Result> { let handle_lock = self.state.lock(); let mut handle = handle_lock.unwrap(); @@ -105,7 +122,6 @@ where #[derive(Debug, Clone)] pub enum Error { ExactStreamVersionMustBeKnown, - VersionOutOfDate, } #[cfg(test)] diff --git a/src/repository/state.rs b/src/repository/state.rs index 38d9820..cfd81ee 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; +use super::event::VersionedRepositoryError; + #[async_trait] pub trait StateRepository { async fn reify(&self) -> State; @@ -15,7 +17,11 @@ where type Version: Eq + Send + Sync; async fn reify(&self) -> Result<(State, Self::Version), Err>; - async fn save(&mut self, version: &Self::Version, state: &State) -> Result + async fn save( + &mut self, + version: &Self::Version, + state: &State, + ) -> Result> where 'a: 'async_trait, State: 'async_trait, diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 5be7266..56249d2 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -193,9 +193,11 @@ where match state_repository.save(&version, &new_state).await { Ok(s) => return Ok(s), - Err(_) => todo!(), + Err(_) => { + todo!() + } } - }; + } todo!() } From b8c99b9dcdb6e175f1288cf92c80ece56a5b44f7 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 23 Mar 2023 14:12:35 -0600 Subject: [PATCH 58/70] retry in state based strategy --- src/strategies/mod.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 56249d2..4ea9536 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -156,7 +156,7 @@ where pub trait ReifyDecideSave where <::Decide as DeciderWithContext>::Ctx: Send + Sync, - <::Decide as DeciderWithContext>::Cmd: Send + Sync, + <::Decide as DeciderWithContext>::Cmd: Send + Sync + Debug, <::Decide as DeciderWithContext>::Err: Send + Sync, <::Decide as Evolver>::Evt: Send + Sync, <::Decide as Evolver>::State: Send + Sync + Clone, @@ -193,13 +193,20 @@ where match state_repository.save(&version, &new_state).await { Ok(s) => return Ok(s), - Err(_) => { - todo!() + Err(VersionedRepositoryError::RepoErr(e)) => { + return Err(ReifyDecideSaveError::RepositoryErr(e)) + } + Err(VersionedRepositoryError::VersionConflict(_)) => { + println!("Retry #{} for {:?} - Reload State", &r, &cmd); + (state, version) = state_repository + .reify() + .await + .map_err(ReifyDecideSaveError::RepositoryErr)?; } } } - todo!() + Err(ReifyDecideSaveError::OccMaxRetries) } } @@ -253,6 +260,7 @@ pub enum LoadDecideAppendError { #[derive(Debug)] pub enum ReifyDecideSaveError { + OccMaxRetries, DecideErr(DecideErr), RepositoryErr(RepoErr), } From ed64c9603a868b3cde7b31d54fcbac375e1f2417 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 23 Mar 2023 18:17:42 -0600 Subject: [PATCH 59/70] passes tests --- src/repository/in_memory/state/versioned.rs | 31 +++++++++------------ src/strategies/mod.rs | 20 +++++++++++-- src/test_helpers/deciders.rs | 6 +++- src/test_helpers/repository.rs | 2 +- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index a64bf8d..30fbdf2 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -32,12 +32,11 @@ where fn version_to_usize( version: &RepositoryVersion, ) -> Result> { - if let RepositoryVersion::Exact(exact) = version { - Ok(exact.to_owned()) - } else { - Err(VersionedRepositoryError::RepoErr( - Error::ExactStreamVersionMustBeKnown, - )) + match version { + RepositoryVersion::Exact(exact) => Ok(exact.to_owned()), + RepositoryVersion::NoStream => Ok(0), + RepositoryVersion::StreamExists => Ok(0), + RepositoryVersion::Any => Err(VersionedRepositoryError::RepoErr(Error::ExactStreamVersionMustBeKnown)) } } @@ -45,17 +44,7 @@ where current: &RepositoryVersion, incoming: &RepositoryVersion, ) -> Result<(), VersionedRepositoryError> { - if let &RepositoryVersion::StreamExists = current { - return if let &RepositoryVersion::Exact(_) = incoming { - Ok(()) - } else { - Err(VersionedRepositoryError::RepoErr( - Error::ExactStreamVersionMustBeKnown, - )) - }; - } - - if Self::version_to_usize(current)? < Self::version_to_usize(incoming)? { + if Self::version_to_usize(current)? == Self::version_to_usize(incoming)? { Ok(()) } else { Err(VersionedRepositoryError::VersionConflict(VersionDiff::new( @@ -64,6 +53,12 @@ where ))) } } + + fn bump_version( + version: &RepositoryVersion + ) -> Result> { + Ok(RepositoryVersion::Exact(Self::version_to_usize(&version)? + 1)) + } } #[async_trait] @@ -90,7 +85,7 @@ where let _ = Self::version_check(&handle.version, version)?; handle.data = state.clone(); - handle.version = version.to_owned(); + handle.version = Self::bump_version(&handle.version)?; drop(handle); diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 4ea9536..726a4cd 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -163,7 +163,7 @@ where { type Decide: DeciderWithContext + Send + Sync; - async fn execute<'a, RepoErr>( + async fn execute_reify_decide<'a, RepoErr>( state_repository: &mut (impl VersionedStateRepository<'a, ::State, RepoErr> + Send + Sync), @@ -273,7 +273,7 @@ mod tests { use crate::{ decider::Event, - repository::in_memory::versioned_with_streams::InMemoryEventRepository, + repository::in_memory::{versioned_with_streams::InMemoryEventRepository, state::versioned::InMemoryStateRepository}, test_helpers::{ deciders::user::{ User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderError, UserDeciderState, @@ -410,4 +410,20 @@ mod tests { assert_matches!(res, Ok(CommandResponse(UserCommand::AddUser(_), _, _))); } + + #[actix_rt::test] + async fn reify_decide_save_basic_functionality() { + let ctx = UserDeciderCtx::new(); + + let mut state_repository = InMemoryStateRepository::::new(UserDeciderState::default()); + + let cmd1 = UserCommand::AddUser("Mike".to_string()); + + let res = UserDecider::execute_reify_decide(&mut state_repository, &ctx, &cmd1, None).await.unwrap(); + + assert_eq!( + res.users.len(), + 1 + ); + } } diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index e6e75b1..4c24ccd 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -11,7 +11,7 @@ pub(crate) mod user { use crate::{ decider::{Decider, DeciderWithContext, Event, Evolver}, repository::event::StreamIdFromEvent, - strategies::{DecideEvolveWithCommandResponse, LoadDecideAppend, StateFromEventRepository}, + strategies::{DecideEvolveWithCommandResponse, LoadDecideAppend, StateFromEventRepository, ReifyDecideSave}, test_helpers::ValueType, }; @@ -243,6 +243,10 @@ pub(crate) mod user { type Decide = Self; } + impl ReifyDecideSave for UserDecider { + type Decide = Self; + } + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct UserDeciderState { pub(crate) users: HashMap, diff --git a/src/test_helpers/repository.rs b/src/test_helpers/repository.rs index 1a7b9dd..72987ab 100644 --- a/src/test_helpers/repository.rs +++ b/src/test_helpers/repository.rs @@ -121,7 +121,7 @@ pub(crate) async fn test_versioned_state_repository<'a, Err: Debug + Send + Sync assert_eq!( state_repository.reify().await.expect("Success"), - (new_state.to_owned(), version) + (new_state.to_owned(), RepositoryVersion::Exact(1)) ); let res = state_repository.save(&version, &new_state).await; From 096721aa8b069e0fc56170b6a152650fd7a78ce2 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Thu, 23 Mar 2023 18:20:45 -0600 Subject: [PATCH 60/70] version bump --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc25bef..b401b18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.12" +version = "1.0.0-alpha.13" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -26,4 +26,4 @@ assert_matches = "1.5.0" const-random = "0.1.15" autoincrement = "1" dotenv = "0.15.0" -futures = "0.3.25" \ No newline at end of file +futures = "0.3.25" From 263a9b99dfecf075db1f3823cbd10f24596d0971 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Fri, 24 Mar 2023 10:17:07 -0600 Subject: [PATCH 61/70] clippy --- src/repository/in_memory/state/versioned.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 30fbdf2..2f77c23 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -57,7 +57,7 @@ where fn bump_version( version: &RepositoryVersion ) -> Result> { - Ok(RepositoryVersion::Exact(Self::version_to_usize(&version)? + 1)) + Ok(RepositoryVersion::Exact(Self::version_to_usize(version)? + 1)) } } From 09bd3f6cd7a5fde98f1d196c85154596aacaca7a Mon Sep 17 00:00:00 2001 From: Bobby Calderwood <8336+bobby@users.noreply.github.com> Date: Fri, 24 Mar 2023 13:50:02 -0600 Subject: [PATCH 62/70] Upgrade ESDB dependency to resolve weird Uuid version pinning --- Cargo.toml | 8 +++----- src/repository/esdb/mod.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b401b18..e817238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,15 @@ edition = "2021" [features] default = ["in_memory", "esdb"] in_memory = [] -esdb = ["dep:eventstore", "dep:uuid_0_8_2", "dep:serde_json"] +esdb = ["dep:eventstore", "dep:uuid", "dep:serde_json"] [dependencies] async-trait = "0.1.53" -eventstore = { version = "2.1.1", optional = true } +eventstore = { version = "2.2.0", optional = true } serde = { version = "1.0.136", features = ["derive"] } serde_json = { version = "1.0.81", optional = true } thiserror = "1.0" -# Use 0.8.2 only here because of a weird deps bug with eventstore -# 1.x uuids are not compatible with eventstore internal uuids -uuid_0_8_2 = { package = "uuid", version = "0.8.2", features = ["v4", "serde"], optional = true } +uuid = { package = "uuid", version = "1.2.2", features = ["v4", "serde"], optional = true } [dev-dependencies] actix-rt = "2.7.0" diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 407a5aa..ce8f7e2 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -6,7 +6,7 @@ use eventstore::{ ResolvedEvent, StreamPosition, }; use serde::{de::DeserializeOwned, Serialize}; -use uuid_0_8_2::Uuid; +use uuid::Uuid; use crate::decider::Event; From 5834bf16db4c11229f59abb861df2a3b657df6a5 Mon Sep 17 00:00:00 2001 From: Bobby Calderwood <8336+bobby@users.noreply.github.com> Date: Mon, 27 Mar 2023 23:07:16 -0600 Subject: [PATCH 63/70] RepositoryVersion derives Serialize/Deserialize --- src/repository/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 05a4035..d3e763a 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + #[cfg(feature = "esdb")] pub mod esdb; pub mod event; @@ -5,7 +7,7 @@ pub mod event; pub mod in_memory; pub mod state; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum RepositoryVersion { Any, Exact(usize), From 34f798135438d19ec27bdcea33331cc303b12331 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Tue, 28 Mar 2023 10:40:42 -0600 Subject: [PATCH 64/70] version bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e817238..17187ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.13" +version = "1.0.0-alpha.14" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From c731f85d55a8a0852f2a02d255f77db29604cc6e Mon Sep 17 00:00:00 2001 From: Bobby Calderwood <8336+bobby@users.noreply.github.com> Date: Tue, 28 Mar 2023 19:59:19 -0600 Subject: [PATCH 65/70] Initial state (#3) * Allow instance access to product initial decider state * Resolve some minor test warnings * Be more specific on eventstore crate version * Remove initial value entirely from Decider * version bump --------- Co-authored-by: Mike Shearer Co-authored-by: Mike Shearer --- Cargo.toml | 2 +- src/decider.rs | 7 ++- src/repository/esdb/mod.rs | 35 +++++++++---- src/strategies/mod.rs | 98 ++++++++++++++++++++++++------------ src/test_helpers/deciders.rs | 11 ++-- 5 files changed, 100 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17187ec..8333698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.14" +version = "1.0.0-alpha.15" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index 6031eca..ca51c06 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -30,7 +30,6 @@ pub trait Evolver { type State: Debug; type Evt: Event + Debug; fn evolve(state: Self::State, event: &Self::Evt) -> Self::State; - fn init() -> Self::State; } #[cfg(test)] @@ -63,13 +62,13 @@ mod tests { .await .expect("Empty Events Vector") .iter() - .fold(UserDecider::init(), UserDecider::evolve); + .fold(UserDeciderState::default(), UserDecider::evolve); let cmd = UserCommand::AddUser("Mike".to_string() as user::UnvalidatedUserName); let events = ::decide(&state, &cmd).expect("Decider Success"); if let Some(UserEvent::UserAdded(user::User { name, id, .. })) = events.clone().first() { - let user_id = id.clone(); + let user_id = *id; let user_name = name.clone(); assert_eq!(name.value(), "Mike".to_string()); @@ -79,7 +78,7 @@ mod tests { let _ = state_repository.save(&state).await; assert_eq!(state_repository.reify().await, state.clone()); - assert_matches!(state.users.get(&id).expect("User exists"), user::User { + assert_matches!(state.users.get(&user_id).expect("User exists"), user::User { id, name, .. diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index ce8f7e2..b8b4865 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -240,6 +240,7 @@ mod tests { let cmd = UserCommand::AddGuitar(user_id, guitar.to_owned()); let res = UserDecider::execute( + UserDeciderState::default(), &mut event_repository, &StreamState::Existing(user_id.to_string()), &ctx, @@ -274,10 +275,16 @@ mod tests { let cmd1 = UserCommand::AddUser("Mike".to_string()); - let evts = - UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd1, None) - .await - .expect("command_succeeds"); + let evts = UserDecider::execute( + UserDeciderState::default(), + &mut event_repository, + &StreamState::New, + &ctx, + &cmd1, + None, + ) + .await + .expect("command_succeeds"); let first_id = evts.first().unwrap().get_id(); @@ -286,9 +293,13 @@ mod tests { UserEvent::UserAdded(User { id, name, .. }) if (&first_id == id) && (name.value() == "Mike".to_string()) ); - let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &first_id.to_string(), + ) + .await + .expect("state is loaded"); assert_matches!( state, @@ -335,9 +346,13 @@ mod tests { thread::sleep(time::Duration::from_secs(1)); - let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &first_id.to_string(), + ) + .await + .expect("state is loaded"); assert_eq!( state.users.get(&first_id).unwrap().guitars, diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 726a4cd..872c075 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -19,6 +19,7 @@ where type Ev: Evolver + Send + Sync; async fn load<'a, Err>( + initial: ::State, event_repository: &(impl VersionedEventRepositoryWithStreams<'a, ::Evt, Err> + Send + Sync), @@ -31,10 +32,11 @@ where .await? .0 .iter() - .fold(::init(), Self::Ev::evolve)) + .fold(initial, Self::Ev::evolve)) } async fn load_by_id<'a, Err, StreamId>( + initial: ::State, event_repository: &(impl VersionedEventRepositoryWithStreams< 'a, ::Evt, @@ -53,7 +55,7 @@ where .await? .0 .iter() - .fold(::init(), Self::Ev::evolve)) + .fold(initial, Self::Ev::evolve)) } } @@ -78,6 +80,7 @@ where } async fn execute<'a, RepoErr, StreamId>( + initial: ::State, event_repository: &mut (impl VersionedEventRepositoryWithStreams< 'a, ::Evt, @@ -108,7 +111,7 @@ where .map_err(Self::to_lda_error)?, }; - let mut state = ::init(); + let mut state = initial; for r in 1..retrys.unwrap_or(20) { state = decider_evts @@ -273,7 +276,10 @@ mod tests { use crate::{ decider::Event, - repository::in_memory::{versioned_with_streams::InMemoryEventRepository, state::versioned::InMemoryStateRepository}, + repository::in_memory::{ + state::versioned::InMemoryStateRepository, + versioned_with_streams::InMemoryEventRepository, + }, test_helpers::{ deciders::user::{ User, UserCommand, UserDecider, UserDeciderCtx, UserDeciderError, UserDeciderState, @@ -293,10 +299,16 @@ mod tests { let cmd1 = UserCommand::AddUser("Mike".to_string()); - let evts = - UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd1, None) - .await - .expect("command_succeeds"); + let evts = UserDecider::execute( + UserDeciderState::default(), + &mut event_repository, + &StreamState::New, + &ctx, + &cmd1, + None, + ) + .await + .expect("command_succeeds"); let first_id = evts.first().unwrap().get_id(); @@ -305,9 +317,13 @@ mod tests { UserEvent::UserAdded(User { id, name, .. }) if (&first_id == id) && (name.value() == "Mike".to_string()) ); - let state = UserDeciderState::load_by_id(&event_repository, &first_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &first_id.to_string(), + ) + .await + .expect("state is loaded"); assert_matches!( state, @@ -315,10 +331,16 @@ mod tests { ); let cmd2 = UserCommand::AddUser("Dmitiry".to_string()); - let evts = - UserDecider::execute(&mut event_repository, &StreamState::New, &ctx, &cmd2, None) - .await - .expect("command_succeeds"); + let evts = UserDecider::execute( + UserDeciderState::default(), + &mut event_repository, + &StreamState::New, + &ctx, + &cmd2, + None, + ) + .await + .expect("command_succeeds"); let second_id = evts.first().unwrap().get_id(); @@ -327,9 +349,13 @@ mod tests { UserEvent::UserAdded(User { id, name, .. }) if (&second_id == id) && (name.value() == "Dmitiry".to_string()) ); - let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &second_id.to_string(), + ) + .await + .expect("state is loaded"); assert_matches!( state, @@ -338,6 +364,7 @@ mod tests { let cmd3 = UserCommand::UpdateUserName(second_id.clone(), "Dmitiry2".to_string()); let evts = UserDecider::execute( + UserDeciderState::default(), &mut event_repository, &StreamState::Existing(second_id.to_string()), &ctx, @@ -352,9 +379,13 @@ mod tests { UserEvent::UserNameUpdated(id, name) if (id == &second_id) && (name == &UserName::try_from("Dmitiry2".to_string()).unwrap()) ); - let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &second_id.to_string(), + ) + .await + .expect("state is loaded"); assert_matches!( state, @@ -365,6 +396,7 @@ mod tests { UserCommand::UpdateUserName(second_id.clone(), "DmitiryWayToLongToSucceed".to_string()); let res = UserDecider::execute( + UserDeciderState::default(), &mut event_repository, &StreamState::Existing(second_id.to_string()), &ctx, @@ -378,16 +410,20 @@ mod tests { Err(LoadDecideAppendError::DecideErr(UserDeciderError::UserField(UserFieldError::NameToLong(n)))) if n == "DmitiryWayToLongToSucceed".to_string() ); - let state = UserDeciderState::load_by_id(&event_repository, &second_id.to_string()) - .await - .expect("state is loaded"); + let state = UserDeciderState::load_by_id( + UserDeciderState::default(), + &event_repository, + &second_id.to_string(), + ) + .await + .expect("state is loaded"); assert_matches!( state, UserDeciderState { users } if users == HashMap::from([(second_id.clone(), User::new(second_id, UserName::try_from("Dmitiry2".to_string()).unwrap()))]) ); - let state = UserDeciderState::load(&event_repository) + let state = UserDeciderState::load(UserDeciderState::default(), &event_repository) .await .expect("state is loaded"); @@ -415,15 +451,15 @@ mod tests { async fn reify_decide_save_basic_functionality() { let ctx = UserDeciderCtx::new(); - let mut state_repository = InMemoryStateRepository::::new(UserDeciderState::default()); + let mut state_repository = + InMemoryStateRepository::::new(UserDeciderState::default()); let cmd1 = UserCommand::AddUser("Mike".to_string()); - let res = UserDecider::execute_reify_decide(&mut state_repository, &ctx, &cmd1, None).await.unwrap(); + let res = UserDecider::execute_reify_decide(&mut state_repository, &ctx, &cmd1, None) + .await + .unwrap(); - assert_eq!( - res.users.len(), - 1 - ); + assert_eq!(res.users.len(), 1); } } diff --git a/src/test_helpers/deciders.rs b/src/test_helpers/deciders.rs index 4c24ccd..2f8ebae 100644 --- a/src/test_helpers/deciders.rs +++ b/src/test_helpers/deciders.rs @@ -11,7 +11,10 @@ pub(crate) mod user { use crate::{ decider::{Decider, DeciderWithContext, Event, Evolver}, repository::event::StreamIdFromEvent, - strategies::{DecideEvolveWithCommandResponse, LoadDecideAppend, StateFromEventRepository, ReifyDecideSave}, + strategies::{ + DecideEvolveWithCommandResponse, LoadDecideAppend, ReifyDecideSave, + StateFromEventRepository, + }, test_helpers::ValueType, }; @@ -177,12 +180,6 @@ pub(crate) mod user { } } } - - fn init() -> UserDeciderState { - UserDeciderState { - users: Default::default(), - } - } } impl DeciderWithContext for UserDecider { From 25e8a1758d838843d046d71988008e08aa6efd8a Mon Sep 17 00:00:00 2001 From: Bobby Calderwood <8336+bobby@users.noreply.github.com> Date: Thu, 20 Apr 2023 11:52:32 -0700 Subject: [PATCH 66/70] State repo reify result (#7) * StateRepository.reify returns an error, as it can also fail * Fix tests and simple in memory impl * version bump and cargo fmt --------- Co-authored-by: Mike Shearer --- Cargo.toml | 2 +- src/decider.rs | 2 +- src/repository/in_memory/simple.rs | 4 ++-- src/repository/in_memory/state/versioned.rs | 10 +++++++--- src/repository/state.rs | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8333698..66ef288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epoch" -version = "1.0.0-alpha.15" +version = "1.0.0-alpha.16" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/decider.rs b/src/decider.rs index ca51c06..2bdafc8 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -76,7 +76,7 @@ mod tests { let state = events.iter().fold(state.clone(), UserDecider::evolve); let _ = state_repository.save(&state).await; - assert_eq!(state_repository.reify().await, state.clone()); + assert_eq!(state_repository.reify().await.unwrap(), state.clone()); assert_matches!(state.users.get(&user_id).expect("User exists"), user::User { id, diff --git a/src/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs index 373ba09..5dca007 100644 --- a/src/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -88,8 +88,8 @@ impl StateRepository for InMemoryStateRepository where State: Default + Send + Sync + Debug + Clone, { - async fn reify(&self) -> State { - self.state.clone() + async fn reify(&self) -> Result { + Ok(self.state.clone()) } async fn save(&mut self, state: &State) -> Result { diff --git a/src/repository/in_memory/state/versioned.rs b/src/repository/in_memory/state/versioned.rs index 2f77c23..c573308 100644 --- a/src/repository/in_memory/state/versioned.rs +++ b/src/repository/in_memory/state/versioned.rs @@ -36,7 +36,9 @@ where RepositoryVersion::Exact(exact) => Ok(exact.to_owned()), RepositoryVersion::NoStream => Ok(0), RepositoryVersion::StreamExists => Ok(0), - RepositoryVersion::Any => Err(VersionedRepositoryError::RepoErr(Error::ExactStreamVersionMustBeKnown)) + RepositoryVersion::Any => Err(VersionedRepositoryError::RepoErr( + Error::ExactStreamVersionMustBeKnown, + )), } } @@ -55,9 +57,11 @@ where } fn bump_version( - version: &RepositoryVersion + version: &RepositoryVersion, ) -> Result> { - Ok(RepositoryVersion::Exact(Self::version_to_usize(version)? + 1)) + Ok(RepositoryVersion::Exact( + Self::version_to_usize(version)? + 1, + )) } } diff --git a/src/repository/state.rs b/src/repository/state.rs index cfd81ee..98d264b 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -4,7 +4,7 @@ use super::event::VersionedRepositoryError; #[async_trait] pub trait StateRepository { - async fn reify(&self) -> State; + async fn reify(&self) -> Result; async fn save(&mut self, state: &State) -> Result; } From ddd3c984ed5a3e4622b64b99349b92b1c3e1a237 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sun, 4 Jun 2023 10:15:27 -0600 Subject: [PATCH 67/70] remove send sync from ReifyDecideSave and versioned state repository --- src/repository/state.rs | 8 ++------ src/strategies/mod.rs | 26 +++++++++++--------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/repository/state.rs b/src/repository/state.rs index 98d264b..b164f7b 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -9,12 +9,8 @@ pub trait StateRepository { } #[async_trait] -pub trait VersionedStateRepository<'a, State, Err> -where - State: Send + Sync, - Err: Send + Sync, -{ - type Version: Eq + Send + Sync; +pub trait VersionedStateRepository<'a, State, Err> { + type Version; async fn reify(&self) -> Result<(State, Self::Version), Err>; async fn save( diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 872c075..078d01a 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -155,31 +155,27 @@ where } } -#[async_trait] +#[async_trait(?Send)] pub trait ReifyDecideSave where - <::Decide as DeciderWithContext>::Ctx: Send + Sync, - <::Decide as DeciderWithContext>::Cmd: Send + Sync + Debug, - <::Decide as DeciderWithContext>::Err: Send + Sync, - <::Decide as Evolver>::Evt: Send + Sync, - <::Decide as Evolver>::State: Send + Sync + Clone, + <::Decide as Evolver>::State: Clone, + <::Decide as DeciderWithContext>::Cmd: Debug, { - type Decide: DeciderWithContext + Send + Sync; + type Decide: DeciderWithContext; async fn execute_reify_decide<'a, RepoErr>( - state_repository: &mut (impl VersionedStateRepository<'a, ::State, RepoErr> - + Send - + Sync), + state_repository: &mut (impl VersionedStateRepository< + 'a, + ::State, + RepoErr, + >), ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, retrys: Option, ) -> Result< ::State, ReifyDecideSaveError<::Err, RepoErr>, - > - where - RepoErr: Send + Sync, - { + > { let (mut state, mut version) = state_repository .reify() .await @@ -262,7 +258,7 @@ pub enum LoadDecideAppendError { } #[derive(Debug)] -pub enum ReifyDecideSaveError { +pub enum ReifyDecideSaveError { OccMaxRetries, DecideErr(DecideErr), RepositoryErr(RepoErr), From 8a315bca92f3a1d34257ab9bf29db6f1bff56635 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sun, 4 Jun 2023 10:18:28 -0600 Subject: [PATCH 68/70] remove send sync from DecideEvolveWithCommandResponse --- src/strategies/mod.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 078d01a..f58005a 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -216,16 +216,12 @@ pub struct CommandResponse( ::State, ); -#[async_trait] +#[async_trait(?Send)] pub trait DecideEvolveWithCommandResponse where - ::State: Send + Sync + Debug + Clone, - ::Ctx: Send + Sync + Debug, - ::Cmd: Send + Sync + Debug, - ::Evt: Clone + Send + Sync + Debug, - ::Err: Send + Sync + Debug, + ::State: Clone { - type Decide: DeciderWithContext + Send + Sync; + type Decide: DeciderWithContext; async fn response( cmd: <::Decide as DeciderWithContext>::Cmd, From 4f33aa1cb4cc5317d9a473269905e2cfa062e219 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sun, 4 Jun 2023 11:37:33 -0600 Subject: [PATCH 69/70] WIP: placeholder check in - I cant' get ?Send to work in a true thread safe async application --- src/repository/esdb/mod.rs | 2 +- src/repository/event.rs | 16 +++---- src/repository/in_memory/simple.rs | 14 +++--- .../in_memory/versioned_with_streams/mod.rs | 8 ++-- src/repository/state.rs | 4 +- src/strategies/mod.rs | 44 +++++++------------ 6 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index b8b4865..2ab4ba0 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -137,7 +137,7 @@ where ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> where 'a: 'async_trait, - E: 'async_trait, + E: 'async_trait + Send + Sync, { let mut perpared_events = vec![]; diff --git a/src/repository/event.rs b/src/repository/event.rs index 3e5c3d7..4be7c7f 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -6,19 +6,19 @@ use thiserror::Error; use super::RepositoryVersion; use crate::decider::Event; -#[async_trait] +#[async_trait(?Send)] pub trait EventRepository where - E: Event + Sync + Send, + E: Event, { async fn load(&self) -> Result, Err>; async fn append(&mut self, events: &Vec) -> Result, Err>; } -#[async_trait] +#[async_trait(?Send)] pub trait VersionedEventRepository where - E: Event + Sync + Send + Debug, + E: Event, { type Version: Eq; @@ -36,13 +36,13 @@ where // https://stackoverflow.com/questions/69560112/how-to-use-rust-async-trait-generic-to-a-lifetime-parameter // https://github.com/dtolnay/async-trait/issues/8 // https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e977da3ddc0c21639b3116e123a94b6f -#[async_trait] +#[async_trait(?Send)] pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where - E: Event + Sync + Send + Debug, - Err: Debug + Send + Sync, + E: Event + Debug, + Err: Debug, { - type StreamId: Send + Sync; + type StreamId; async fn load( &self, diff --git a/src/repository/in_memory/simple.rs b/src/repository/in_memory/simple.rs index 5dca007..db47ce9 100644 --- a/src/repository/in_memory/simple.rs +++ b/src/repository/in_memory/simple.rs @@ -15,14 +15,14 @@ use super::InMemoryEventRepositoryState; #[derive(Default)] pub struct InMemoryEventRepository where - E: Event + Clone + Send + Sync, + E: Event + Clone, { state: Arc>>, } impl InMemoryEventRepository where - E: Event + Clone + Send + Sync, + E: Event + Clone, { pub fn new() -> Self { Self { @@ -31,10 +31,10 @@ where } } -#[async_trait] +#[async_trait(?Send)] impl EventRepository for InMemoryEventRepository where - E: Event + Clone + Send + Sync, + E: Event + Clone, { async fn load(&self) -> Result, ()> { let lock = self.state.lock().unwrap(); @@ -65,7 +65,7 @@ pub struct InMemoryStateRepository { impl InMemoryStateRepository where - State: Default + Send + Sync + Debug + Clone, + State: Default + Debug + Clone, { pub fn new() -> Self { Self::default() @@ -83,10 +83,10 @@ where } } -#[async_trait] +#[async_trait(?Send)] impl StateRepository for InMemoryStateRepository where - State: Default + Send + Sync + Debug + Clone, + State: Default + Debug + Clone, { async fn reify(&self) -> Result { Ok(self.state.clone()) diff --git a/src/repository/in_memory/versioned_with_streams/mod.rs b/src/repository/in_memory/versioned_with_streams/mod.rs index a6b078c..bf37437 100644 --- a/src/repository/in_memory/versioned_with_streams/mod.rs +++ b/src/repository/in_memory/versioned_with_streams/mod.rs @@ -22,7 +22,7 @@ pub mod error; #[derive(Clone)] pub struct InMemoryEventRepository where - E: Event + Sync + Send + Debug, + E: Event + Debug, { stream_name: String, state: HashMap>>>, @@ -30,7 +30,7 @@ where impl InMemoryEventRepository where - E: Event + Sync + Send + Debug, + E: Event + Debug, { pub fn new(stream_name: &str) -> Self { Self { @@ -74,10 +74,10 @@ where } } -#[async_trait] +#[async_trait(?Send)] impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> for InMemoryEventRepository where - E: Event + Sync + Send + Clone + Debug, + E: Event + Clone + Debug, { type StreamId = String; diff --git a/src/repository/state.rs b/src/repository/state.rs index b164f7b..c158df1 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -2,13 +2,13 @@ use async_trait::async_trait; use super::event::VersionedRepositoryError; -#[async_trait] +#[async_trait(?Send)] pub trait StateRepository { async fn reify(&self) -> Result; async fn save(&mut self, state: &State) -> Result; } -#[async_trait] +#[async_trait(?Send)] pub trait VersionedStateRepository<'a, State, Err> { type Version; diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index f58005a..d40fd88 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -10,22 +10,20 @@ use crate::{ use async_trait::async_trait; use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; -#[async_trait] -pub trait StateFromEventRepository -where - ::Evt: Send + Sync + Debug, - ::State: Send + Sync + Debug, -{ +#[async_trait(?Send)] +pub trait StateFromEventRepository { type Ev: Evolver + Send + Sync; async fn load<'a, Err>( initial: ::State, - event_repository: &(impl VersionedEventRepositoryWithStreams<'a, ::Evt, Err> - + Send - + Sync), + event_repository: &(impl VersionedEventRepositoryWithStreams< + 'a, + ::Evt, + Err, + >), ) -> Result<::State, VersionedRepositoryError> where - Err: Debug + Send + Sync, + Err: Debug, { Ok(event_repository .load(None) @@ -59,18 +57,14 @@ where } } -#[async_trait] +#[async_trait(?Send)] pub trait LoadDecideAppend where - ::State: Send + Sync + Debug, - ::Ctx: Send + Sync + Debug, - ::Cmd: Send + Sync + Debug, - ::Evt: Clone + Send + Sync + Debug, - ::Err: Send + Sync + Debug, + ::Evt: Clone, { - type Decide: DeciderWithContext + Send + Sync; + type Decide: DeciderWithContext; - fn to_lda_error( + fn to_lda_error( err: VersionedRepositoryError, ) -> LoadDecideAppendError { match err { @@ -86,8 +80,7 @@ where ::Evt, RepoErr, StreamId = StreamId, - > + Send - + Sync), + >), stream_id: &StreamState, ctx: &<::Decide as DeciderWithContext>::Ctx, cmd: &<::Decide as DeciderWithContext>::Cmd, @@ -97,11 +90,8 @@ where LoadDecideAppendError<::Err, RepoErr>, > where - RepoErr: Debug + Send + Sync, - StreamId: Send - + Sync - + Clone - + StreamIdFromEvent<<::Decide as Evolver>::Evt>, + RepoErr: Debug, + StreamId: Clone + StreamIdFromEvent<<::Decide as Evolver>::Evt>, { let (mut decider_evts, mut version) = match stream_id { StreamState::New => (vec![], RepositoryVersion::NoStream), @@ -219,7 +209,7 @@ pub struct CommandResponse( #[async_trait(?Send)] pub trait DecideEvolveWithCommandResponse where - ::State: Clone + ::State: Clone, { type Decide: DeciderWithContext; @@ -246,7 +236,7 @@ pub enum StreamState { } #[derive(Debug)] -pub enum LoadDecideAppendError { +pub enum LoadDecideAppendError { OccMaxRetries, VersionError, DecideErr(DecideErr), From e832b65bbc3c1c1ae980122433a9c9bd21a328e3 Mon Sep 17 00:00:00 2001 From: Mike Shearer Date: Sun, 4 Jun 2023 12:06:32 -0600 Subject: [PATCH 70/70] wip placeholder: annother attempt - further down the rabbit hole --- src/decider.rs | 10 ++++++++++ src/repository/esdb/mod.rs | 12 +++++++++--- src/repository/event.rs | 4 ++-- src/repository/state.rs | 5 ++++- src/strategies/mod.rs | 16 +++++++++------- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/decider.rs b/src/decider.rs index 2bdafc8..03aca7d 100644 --- a/src/decider.rs +++ b/src/decider.rs @@ -1,5 +1,15 @@ use std::fmt::Debug; +struct Placeholder { + value: String +} + +impl Placeholder { + async fn foo(bar: String) -> String { + bar + } +} + pub trait Event { type EntityId; diff --git a/src/repository/esdb/mod.rs b/src/repository/esdb/mod.rs index 2ab4ba0..2bc3182 100644 --- a/src/repository/esdb/mod.rs +++ b/src/repository/esdb/mod.rs @@ -68,7 +68,7 @@ impl ESDBEventRepository { } } -#[async_trait] +#[async_trait(?Send)] impl<'a, E> VersionedEventRepositoryWithStreams<'a, E, Error> for ESDBEventRepository where E: Event + Sync + Send + Serialize + DeserializeOwned + Clone + Debug, @@ -78,7 +78,10 @@ where async fn load( &self, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> + where + E: Send + { self.load_from_version(&RepositoryVersion::Any, id).await } @@ -86,7 +89,10 @@ where &self, version: &RepositoryVersion, id: Option<&Self::StreamId>, - ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> { + ) -> Result<(Vec, RepositoryVersion), VersionedRepositoryError> + where + E: Send + { let mut stream = self .client .read_stream( diff --git a/src/repository/event.rs b/src/repository/event.rs index 4be7c7f..d79e4aa 100644 --- a/src/repository/event.rs +++ b/src/repository/event.rs @@ -40,7 +40,7 @@ where pub trait VersionedEventRepositoryWithStreams<'a, E, Err> where E: Event + Debug, - Err: Debug, + Err: Debug + Send, { type StreamId; @@ -67,7 +67,7 @@ where } #[derive(Debug, Error)] -pub enum VersionedRepositoryError { +pub enum VersionedRepositoryError { #[error("Version conflict {0:?}")] VersionConflict(VersionDiff), #[error("Repository Error {0}")] diff --git a/src/repository/state.rs b/src/repository/state.rs index c158df1..d63c04e 100644 --- a/src/repository/state.rs +++ b/src/repository/state.rs @@ -9,7 +9,10 @@ pub trait StateRepository { } #[async_trait(?Send)] -pub trait VersionedStateRepository<'a, State, Err> { +pub trait VersionedStateRepository<'a, State, Err> +where + Err: Send +{ type Version; async fn reify(&self) -> Result<(State, Self::Version), Err>; diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index d40fd88..2da96a2 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -11,10 +11,11 @@ use async_trait::async_trait; use repository::event::{StreamIdFromEvent, VersionedEventRepositoryWithStreams}; #[async_trait(?Send)] -pub trait StateFromEventRepository { +pub trait StateFromEventRepository +{ type Ev: Evolver + Send + Sync; - async fn load<'a, Err>( + async fn load<'a, Err: Send>( initial: ::State, event_repository: &(impl VersionedEventRepositoryWithStreams< 'a, @@ -60,11 +61,12 @@ pub trait StateFromEventRepository { #[async_trait(?Send)] pub trait LoadDecideAppend where - ::Evt: Clone, + ::Evt: Clone + Send, + ::Err: Send { type Decide: DeciderWithContext; - fn to_lda_error( + fn to_lda_error( err: VersionedRepositoryError, ) -> LoadDecideAppendError { match err { @@ -73,7 +75,7 @@ where } } - async fn execute<'a, RepoErr, StreamId>( + async fn execute<'a, RepoErr: Send, StreamId>( initial: ::State, event_repository: &mut (impl VersionedEventRepositoryWithStreams< 'a, @@ -153,7 +155,7 @@ where { type Decide: DeciderWithContext; - async fn execute_reify_decide<'a, RepoErr>( + async fn execute_reify_decide<'a, RepoErr: Send>( state_repository: &mut (impl VersionedStateRepository< 'a, ::State, @@ -236,7 +238,7 @@ pub enum StreamState { } #[derive(Debug)] -pub enum LoadDecideAppendError { +pub enum LoadDecideAppendError { OccMaxRetries, VersionError, DecideErr(DecideErr),