ledger: process deposits and withdrawals
This commit is contained in:
parent
a00567cadc
commit
6df9c9d7a9
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -123,6 +123,22 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dissimilar"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5"
|
||||
|
||||
[[package]]
|
||||
name = "expect-test"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d4661aca38d826eb7c72fe128e4238220616de4c0cc00db7bfc38e2e1364dd3"
|
||||
dependencies = [
|
||||
"dissimilar",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
@ -293,6 +309,7 @@ name = "processor"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"csv",
|
||||
"expect-test",
|
||||
"fpdec",
|
||||
"serde",
|
||||
"serde_with",
|
||||
|
|
|
@ -11,3 +11,6 @@ fpdec = "0.5"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_with = "2.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.4"
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
//! Error types for this crate.
|
||||
use thiserror::Error;
|
||||
|
||||
/// Any kind of error that can happen when processing a [crate::Transaction] in a [crate::Ledger].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
|
||||
pub enum LedgerError {
|
||||
#[error("not enough funds available to run transaction")]
|
||||
NotEnoughFunds,
|
||||
}
|
||||
|
||||
/// Any kind of error that can happen when deserializing a [crate::Transaction] value.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
|
||||
pub enum ParseError {
|
||||
|
|
126
src/ledger.rs
126
src/ledger.rs
|
@ -1,6 +1,6 @@
|
|||
//! A ledger implementation to track all transactions.
|
||||
|
||||
use crate::{ClientId, TxAmount, TxId};
|
||||
use crate::{ClientId, Deposit, LedgerError, Transaction, TxAmount, TxId, Withdrawal};
|
||||
|
||||
/// A ledger of accounts, which processes transactions one at a time.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
|
@ -16,6 +16,8 @@ pub struct AccountInfo {
|
|||
locked: bool,
|
||||
}
|
||||
|
||||
type LedgerResult<T> = Result<T, LedgerError>;
|
||||
|
||||
impl Ledger {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
|
@ -37,6 +39,27 @@ impl Ledger {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self, tx: Transaction) -> LedgerResult<()> {
|
||||
match tx {
|
||||
Transaction::Deposit(Deposit { client, tx, amount }) => self.delta(client, tx, amount),
|
||||
Transaction::Withdrawal(Withdrawal { client, tx, amount }) => {
|
||||
self.delta(client, tx, -amount)
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn delta(&mut self, client: ClientId, tx: TxId, delta: TxAmount) -> LedgerResult<()> {
|
||||
let account = self.accounts.entry(client).or_default();
|
||||
let new_balance = account.available_funds() + delta;
|
||||
if new_balance < TxAmount::ZERO {
|
||||
return Err(LedgerError::NotEnoughFunds);
|
||||
}
|
||||
account.available_funds = new_balance;
|
||||
self.reversible_transactions.insert(tx, (client, delta));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountInfo {
|
||||
|
@ -60,3 +83,104 @@ impl AccountInfo {
|
|||
self.available_funds + self.held_funds
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use expect_test::{expect, Expect};
|
||||
|
||||
macro_rules! inline_csv {
|
||||
($line:literal) => {
|
||||
$line
|
||||
};
|
||||
($line:literal, $($lines:literal),+ $(,)?) => {
|
||||
concat!($line, "\n", inline_csv!($($lines),+))
|
||||
};
|
||||
}
|
||||
|
||||
fn process_transactions(input: &str) -> Result<Ledger, LedgerError> {
|
||||
let mut ledger = Ledger::new();
|
||||
for tx in Transaction::configured_csv_reader_builder()
|
||||
.from_reader(input.as_bytes())
|
||||
.into_deserialize()
|
||||
{
|
||||
ledger.process(tx.unwrap())?
|
||||
}
|
||||
Ok(ledger)
|
||||
}
|
||||
|
||||
fn check_ledger(ledger: &Ledger, expect: Expect) {
|
||||
let mut writer = csv::Writer::from_writer(vec![]);
|
||||
ledger.dump_csv(&mut writer).unwrap();
|
||||
let actual = String::from_utf8(writer.into_inner().unwrap()).unwrap();
|
||||
expect.assert_eq(&actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deposit_single_account() {
|
||||
let ledger = process_transactions(inline_csv!(
|
||||
"type, client, tx, amount",
|
||||
"deposit, 1, 1, 1.0",
|
||||
"deposit, 1, 2, 2.0",
|
||||
))
|
||||
.unwrap();
|
||||
check_ledger(
|
||||
&ledger,
|
||||
expect![[r#"
|
||||
client,available,held,total,locked
|
||||
1,3.0,0,3.0,false
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deposit_multiple_accounts() {
|
||||
let ledger = process_transactions(inline_csv!(
|
||||
"type, client, tx, amount",
|
||||
"deposit, 1, 1, 1.0",
|
||||
"deposit, 2, 2, 1.0",
|
||||
"deposit, 1, 3, 2.0",
|
||||
))
|
||||
.unwrap();
|
||||
check_ledger(
|
||||
&ledger,
|
||||
expect![[r#"
|
||||
client,available,held,total,locked
|
||||
1,3.0,0,3.0,false
|
||||
2,1.0,0,1.0,false
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deposit_and_withdrawal() {
|
||||
let ledger = process_transactions(inline_csv!(
|
||||
"type, client, tx, amount",
|
||||
"deposit, 1, 1, 1.0",
|
||||
"deposit, 2, 2, 1.0",
|
||||
"deposit, 1, 3, 2.0",
|
||||
"withdrawal, 1, 4, 1.5",
|
||||
"withdrawal, 2, 5, 1.0",
|
||||
))
|
||||
.unwrap();
|
||||
check_ledger(
|
||||
&ledger,
|
||||
expect![[r#"
|
||||
client,available,held,total,locked
|
||||
1,1.5,0,1.5,false
|
||||
2,0.0,0,0.0,false
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deposit_and_withdrawal_not_enough_funds() {
|
||||
let error = process_transactions(inline_csv!(
|
||||
"type, client, tx, amount",
|
||||
"deposit, 2, 2, 1.0",
|
||||
"withdrawal, 2, 5, 3.0",
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error, LedgerError::NotEnoughFunds);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue