use crate::entity::gateway::EntityGateway; use thiserror::Error; use crate::ship::items::manager::{ItemManager, ItemManagerError}; use crate::entity::gateway::GatewayError; #[derive(Error, Debug)] pub enum TransactionCommitError { #[error("transaction commit gateway error {0}")] Gateway(#[from] GatewayError), #[error("transaction commit itemmanager error {0}")] ItemManager(#[from] ItemManagerError), } #[async_trait::async_trait] pub trait ItemAction: std::marker::Send + std::marker::Sync + std::fmt::Debug { async fn commit(&self, manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError>; } pub struct ItemTransactionActions<'a, EG: EntityGateway> { action_queue: Vec>>, pub manager: &'a ItemManager, } impl<'a, EG: EntityGateway> ItemTransactionActions<'a, EG> { fn new(manager: &'a ItemManager) -> ItemTransactionActions<'a, EG> { ItemTransactionActions { action_queue: Vec::new(), manager } } pub fn action(&mut self, action: Box>) { self.action_queue.push(action) } } pub struct ItemTransaction<'a, T, EG: EntityGateway> { data: T, actions: ItemTransactionActions<'a, EG>, } impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> { pub fn new(manager: &'a ItemManager, arg: T) -> ItemTransaction<'a, T, EG> { ItemTransaction { data: arg, actions: ItemTransactionActions::new(manager), } } pub fn act(mut self, action: fn(&mut ItemTransactionActions, &T) -> Result) -> FinalizedItemTransaction { match action(&mut self.actions, &self.data) { Ok(k) => { FinalizedItemTransaction { value: Ok(k), action_queue: self.actions.action_queue, } }, Err(err) => { FinalizedItemTransaction { value: Err(err), action_queue: Vec::new(), } } } } } #[derive(Error, Debug)] pub enum TransactionError { #[error("transaction action error {0:?}")] Action(E), #[error("transaction commit error {0}")] Commit(#[from] TransactionCommitError), } // this only exists to drop the ItemManager borrow of ItemTransaction so a mutable ItemTransaction can be passed in later pub struct FinalizedItemTransaction { value: Result, action_queue: Vec>>, } impl FinalizedItemTransaction { pub async fn commit(self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result> { match self.value { Ok(value) => { for action in self.action_queue.into_iter() { // TODO: better handle rolling back if this ever errors out action.commit(item_manager, entity_gateway).await.map_err(|err| TransactionError::Commit(err))?; } Ok(value) }, Err(err) => Err(TransactionError::Action(err)), } } } #[cfg(test)] mod test { use super::*; use crate::entity::account::{UserAccountId, NewUserAccountEntity, UserAccountEntity}; use crate::entity::character::{NewCharacterEntity, CharacterEntity}; use crate::entity::gateway::GatewayError; use thiserror::Error; #[async_std::test] async fn test_item_transaction() { #[derive(Debug)] struct DummyAction1 { name: String, } #[derive(Debug)] struct DummyAction2 { value: u32, } #[derive(Error, Debug)] #[error("")] enum DummyError { Error } #[derive(Default, Clone)] struct DummyGateway { d1_set: String, d2_inc: u32, } #[async_trait::async_trait] impl EntityGateway for DummyGateway { async fn create_user(&mut self, user: NewUserAccountEntity) -> Result { self.d1_set = user.username; Ok(UserAccountEntity::default()) } async fn create_character(&mut self, char: NewCharacterEntity) -> Result { self.d2_inc += char.slot; Ok(CharacterEntity::default()) } } #[async_trait::async_trait] impl ItemAction for DummyAction1 { async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { item_manager.id_counter = 55555; entity_gateway.create_user(NewUserAccountEntity { username: self.name.clone(), ..NewUserAccountEntity::default() }) .await?; Ok(()) } } #[async_trait::async_trait] impl ItemAction for DummyAction2 { async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { item_manager.id_counter += self.value; entity_gateway.create_character(NewCharacterEntity { slot: self.value, ..NewCharacterEntity::new(UserAccountId(0), 1) // TODO: handle different keyboard_config_presets }) .await?; Ok(()) } } let mut item_manager = ItemManager::default(); let mut entity_gateway = DummyGateway::default(); let result = ItemTransaction::new(&item_manager, 12) .act(|it, k| { it.action(Box::new(DummyAction1 {name: "asdf".into()})); it.action(Box::new(DummyAction2 {value: 11})); it.action(Box::new(DummyAction2 {value: *k})); if *k == 99 { return Err(DummyError::Error) } Ok(String::from("hello")) }) .commit(&mut item_manager, &mut entity_gateway) .await; assert!(entity_gateway.d1_set == "asdf"); assert!(entity_gateway.d2_inc == 23); assert!(item_manager.id_counter == 55578); assert!(result.unwrap() == "hello"); } #[async_std::test] async fn test_item_transaction_with_action_error() { #[derive(Debug)] struct DummyAction1 { } #[derive(Debug)] struct DummyAction2 { } #[derive(Error, Debug, PartialEq, Eq)] #[error("")] enum DummyError { Error } #[derive(Default, Clone)] struct DummyGateway { _d1_set: String, d2_inc: u32, } #[async_trait::async_trait] impl EntityGateway for DummyGateway { async fn create_character(&mut self, char: NewCharacterEntity) -> Result { self.d2_inc += char.slot; Ok(CharacterEntity::default()) } } #[async_trait::async_trait] impl ItemAction for DummyAction1 { async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { entity_gateway.create_character(NewCharacterEntity { slot: 1, ..NewCharacterEntity::new(UserAccountId(0), 1) // TODO: handle different keyboard_config_presets }) .await?; Ok(()) } } #[async_trait::async_trait] impl ItemAction for DummyAction2 { async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { entity_gateway.create_character(NewCharacterEntity { slot: 1, ..NewCharacterEntity::new(UserAccountId(0), 1) // TODO: handle different keyboard_config_presets }) .await?; Ok(()) } } let mut item_manager = ItemManager::default(); let mut entity_gateway = DummyGateway::default(); let result = ItemTransaction::new(&item_manager, 12) .act(|it, _| -> Result<(), _> { it.action(Box::new(DummyAction1 {})); it.action(Box::new(DummyAction2 {})); it.action(Box::new(DummyAction2 {})); Err(DummyError::Error) }) .commit(&mut item_manager, &mut entity_gateway) .await; assert!(entity_gateway.d2_inc == 0); assert!(matches!(result, Err(TransactionError::Action(DummyError::Error)))); } #[async_std::test] async fn test_item_transaction_with_commit_error() { #[derive(Debug)] struct DummyAction1 { } #[derive(Debug)] struct DummyAction2 { } #[derive(Error, Debug, PartialEq, Eq)] #[error("")] enum DummyError { } #[derive(Default, Clone)] struct DummyGateway { _d1_set: String, d2_inc: u32, } #[async_trait::async_trait] impl EntityGateway for DummyGateway { async fn create_character(&mut self, char: NewCharacterEntity) -> Result { self.d2_inc += char.slot; Ok(CharacterEntity::default()) } } #[async_trait::async_trait] impl ItemAction for DummyAction1 { async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { entity_gateway.create_character(NewCharacterEntity { slot: 1, ..NewCharacterEntity::new(UserAccountId(0), 1) // TODO: handle different keyboard_config_presets }) .await?; Err(GatewayError::Error.into()) } } #[async_trait::async_trait] impl ItemAction for DummyAction2 { async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { entity_gateway.create_character(NewCharacterEntity { slot: 1, ..NewCharacterEntity::new(UserAccountId(0), 1) // TODO: handle different keyboard_config_presets }) .await?; Ok(()) } } let mut item_manager = ItemManager::default(); let mut entity_gateway = DummyGateway::default(); let result = ItemTransaction::new(&item_manager, 12) .act(|it, _| -> Result<_, DummyError> { it.action(Box::new(DummyAction1 {})); it.action(Box::new(DummyAction2 {})); it.action(Box::new(DummyAction2 {})); Ok(()) }) .commit(&mut item_manager, &mut entity_gateway) .await; // in an ideal world this would be 0 as rollbacks would occur assert!(entity_gateway.d2_inc == 1); assert!(matches!(result, Err(TransactionError::Commit(TransactionCommitError::Gateway(GatewayError::Error))))); } }