elseware/src/ship/items/transaction.rs
andy 1af69800ed
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
add new arg for new_user_character and fix tests. add keyboard config tests. fix and clean up warnings in test files
2022-02-19 20:29:46 +00:00

338 lines
12 KiB
Rust

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<EG: EntityGateway>: 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<Box<dyn ItemAction<EG>>>,
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<dyn ItemAction<EG>>) {
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<E: std::fmt::Debug, U>(mut self, action: fn(&mut ItemTransactionActions<EG>, &T) -> Result<U, E>) -> FinalizedItemTransaction<U, E, EG> {
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<E: std::fmt::Debug> {
#[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<T, E: std::fmt::Debug, EG: EntityGateway> {
value: Result<T, E>,
action_queue: Vec<Box<dyn ItemAction<EG>>>,
}
impl<T, E: std::fmt::Debug, EG: EntityGateway> FinalizedItemTransaction<T, E, EG> {
pub async fn commit(self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<T, TransactionError<E>> {
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<UserAccountEntity, GatewayError> {
self.d1_set = user.username;
Ok(UserAccountEntity::default())
}
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> 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<EG: EntityGateway> ItemAction<EG> 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<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> 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<EG: EntityGateway> ItemAction<EG> 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<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> 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<EG: EntityGateway> ItemAction<EG> 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)))));
}
}