Compare commits

...

No commits in common. "ae6868975f76bc6eb497ee6fdf5d17c3d2fca16c" and "6f8eed44123c63a366c5a0afda28690e7e67a7c0" have entirely different histories.

11 changed files with 212 additions and 659 deletions

View File

@ -1,7 +1,6 @@
type,client,tx,amount type,client,tx,amount
deposit,1,1,1.0 deposit,1,1,1.0
deposit,2,2,2.0 deposit,2,2,2.0
deposit,1,3,2.742 deposit,1,3,2.0
withdrawal,1,4,1.5 withdrawal,1,4,1.5
withdrawal,2,5,3.0 withdrawal,2,5,3.0
dispute,2,2,

1 type client tx amount
2 deposit 1 1 1.0
3 deposit 2 2 2.0
4 deposit 1 3 2.742 2.0
5 withdrawal 1 4 1.5
6 withdrawal 2 5 3.0
dispute 2 2

View File

@ -1,8 +0,0 @@
type,client,tx,amount
deposit,1,1,1.0
deposit,2,2,2.0
deposit,1,3,2.742
withdrawal,1,4,1.5
withdrawal,2,5,3.0
dispute,2,2,
resolve,2,2,
1 type client tx amount
2 deposit 1 1 1.0
3 deposit 2 2 2.0
4 deposit 1 3 2.742
5 withdrawal 1 4 1.5
6 withdrawal 2 5 3.0
7 dispute 2 2
8 resolve 2 2

View File

@ -1,8 +0,0 @@
type,client,tx,amount
deposit,1,1,1.0
deposit,2,2,2.0
deposit,1,3,2.742
withdrawal,1,4,1.5
withdrawal,2,5,3.0
dispute,2,2,
chargeback,2,2,
1 type client tx amount
2 deposit 1 1 1.0
3 deposit 2 2 2.0
4 deposit 1 3 2.742
5 withdrawal 1 4 1.5
6 withdrawal 2 5 3.0
7 dispute 2 2
8 chargeback 2 2

View File

@ -1,43 +1,29 @@
use act::parse::parse;
use act::process::process;
use act::stores::MemActStore;
use act::types::Transaction;
use clap::{App, Arg}; use clap::{App, Arg};
use std::collections::HashMap; use std::io::{BufReader, BufRead, stdin};
use std::fs; use std::fs;
use std::io::{stdin, BufRead, BufReader}; use act::parse;
use act::stores::ActStore;
use act::stores::MemActStore;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let matches = App::new("act") let matches = App::new("act")
.version("0.1") .version("0.1")
.about("Merges transactions into final account state") .about("Merges transactions into account state")
.author("Lewis Diamond") .author("Lewis Diamond")
.arg( .arg(Arg::with_name("input").required(false).index(1).help("Input file, stdin if omitted or -"))
Arg::with_name("input")
.required(false)
.index(1)
.help("Input file, stdin if omitted or -"),
)
.get_matches(); .get_matches();
let input: Box<dyn BufRead> = match matches.value_of("input") { let input: Box<dyn BufRead> = match matches.value_of("input") {
Some("-") | Some("") | None => Box::new(BufReader::new(stdin())), Some("-")|Some("")|None => Box::new(BufReader::new(stdin())),
Some(f) => Box::new(BufReader::new(fs::File::open(f).unwrap())), Some(f) => Box::new(BufReader::new(fs::File::open(f).unwrap())),
}; };
let mut act_store = MemActStore::new();
let mut tx_store: HashMap<u32, Transaction> = HashMap::new();
let s = parse(input); let s = parse(input);
tokio::pin!(s); tokio::pin!(s);
while let Some(v) = s.next().await { while let Some(v) = s.next().await {
process(v, &mut act_store, &mut tx_store); println!("{:?}", v);
}
let mut writer = csv::WriterBuilder::new().from_writer(std::io::stdout());
for act in act_store.into_iter() {
writer.serialize(act.1).unwrap();
} }
} }

View File

@ -1,4 +1,125 @@
pub mod parse;
pub mod process;
pub mod stores;
pub mod types; pub mod types;
pub mod stores;
use async_stream::stream;
use std::io::Read;
use tokio_stream::Stream;
use types::Transaction;
pub fn parse<R: Read>(input: R) -> impl Stream<Item = Transaction> {
let mut reader = csv::ReaderBuilder::new()
.trim(csv::Trim::All)
.has_headers(true)
.from_reader(input);
stream! {
for tx in reader.deserialize() {
match tx {
Ok(tx) => yield tx,
//Depending on the infrastructure, a specific output format
//might be used to add monitoring/alerting
Err(e) => eprintln!("Error reading CSV: {}", e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_stream::StreamExt;
use tokio_test::block_on;
use types::Transaction;
use types::TransactionType;
#[test]
fn valid_csv_is_parsed() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,1.0
deposit,2,2,2.0
deposit,1,3,2.0
withdrawal,1,4,1.5
withdrawal,2,5,3.0";
let expected = vec![
Transaction {tx_type: TransactionType::Deposit, amount: 10000, client: 1, tx: 1},
Transaction {tx_type: TransactionType::Deposit, amount: 20000, client: 2, tx: 2},
Transaction {tx_type: TransactionType::Deposit, amount: 20000, client: 1, tx: 3},
Transaction {tx_type: TransactionType::Withdrawal, amount: 15000, client: 1, tx: 4},
Transaction {tx_type: TransactionType::Withdrawal, amount: 30000, client: 2, tx: 5},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn valid_csv_with_whitespaces_is_parsed() {
block_on(async {
let data = "\
type, client, tx, amount
deposit, 1, 1, 1.0
deposit, 2, 2, 2.0
deposit, 1, 3, 2.0
withdrawal, 1, 4, 1.5
withdrawal, 2, 5, 3.0";
let expected = vec![
Transaction {tx_type: TransactionType::Deposit, amount: 10000, client: 1, tx: 1},
Transaction {tx_type: TransactionType::Deposit, amount: 20000, client: 2, tx: 2},
Transaction {tx_type: TransactionType::Deposit, amount: 20000, client: 1, tx: 3},
Transaction {tx_type: TransactionType::Withdrawal, amount: 15000, client: 1, tx: 4},
Transaction {tx_type: TransactionType::Withdrawal, amount: 30000, client: 2, tx: 5},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn amounts_are_parsed_correctly() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,1.0001
deposit,2,2,2.0010
deposit,1,3,10.01
withdrawal,1,4,01.10
withdrawal,2,5,10.0110101";
let expected = vec![
Transaction {tx_type: TransactionType::Deposit, amount: 10001, client: 1, tx: 1},
Transaction {tx_type: TransactionType::Deposit, amount: 20010, client: 2, tx: 2},
Transaction {tx_type: TransactionType::Deposit, amount: 100100, client: 1, tx: 3},
Transaction {tx_type: TransactionType::Withdrawal, amount: 11000, client: 1, tx: 4},
Transaction {tx_type: TransactionType::Withdrawal, amount: 100110, client: 2, tx: 5},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn invalid_amounts_are_filtered() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,99999999999999999
deposit,2,2,18446744073709551615
deposit,1,3,18446744073709551616
withdrawal,1,4,0
withdrawal,2,5,-1
withdrawal,1,6,-99999999999999999
withdrawal,1,6,-18446744073709551615
withdrawal,1,7,-18446744073709551616";
let expected = vec![
Transaction {tx_type: TransactionType::Withdrawal, amount: 0, client: 1, tx: 4},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
}

View File

@ -1,240 +0,0 @@
use crate::types::Transaction;
use async_stream::stream;
use std::io::Read;
use tokio_stream::Stream;
pub fn parse<R: Read>(input: R) -> impl Stream<Item = Transaction> {
let mut reader = csv::ReaderBuilder::new()
.trim(csv::Trim::All)
.flexible(true)
.has_headers(true)
.from_reader(input);
stream! {
for tx in reader.deserialize() {
match tx {
Ok(tx) => yield tx,
//Depending on the infrastructure, a specific output format
//might be used to add monitoring/alerting
Err(e) => eprintln!("Error reading CSV: {}", e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Transaction;
use crate::types::TransactionType;
use tokio_stream::StreamExt;
use tokio_test::block_on;
#[test]
fn valid_csv_is_parsed() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,1.0
deposit,2,2,2.0
deposit,1,3,2.0
withdrawal,1,4,1.5
withdrawal,2,5,3.0";
let expected = vec![
Transaction {
tx_type: TransactionType::Deposit,
amount: 10000,
client: 1,
tx: 1,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 2,
tx: 2,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 1,
tx: 3,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 15000,
client: 1,
tx: 4,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 30000,
client: 2,
tx: 5,
},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn valid_csv_with_whitespaces_is_parsed() {
block_on(async {
let data = "\
type, client, tx, amount
deposit, 1, 1, 1.0
deposit, 2, 2, 2.0
deposit, 1, 3, 2.0
withdrawal, 1, 4, 1.5
withdrawal, 2, 5, 3.0";
let expected = vec![
Transaction {
tx_type: TransactionType::Deposit,
amount: 10000,
client: 1,
tx: 1,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 2,
tx: 2,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 1,
tx: 3,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 15000,
client: 1,
tx: 4,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 30000,
client: 2,
tx: 5,
},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn amounts_are_parsed_correctly() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,1.0001
deposit,2,2,2.0010
deposit,1,3,10.01
withdrawal,1,4,01.10
withdrawal,2,5,10.0110101";
let expected = vec![
Transaction {
tx_type: TransactionType::Deposit,
amount: 10001,
client: 1,
tx: 1,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20010,
client: 2,
tx: 2,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 100100,
client: 1,
tx: 3,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 11000,
client: 1,
tx: 4,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 100110,
client: 2,
tx: 5,
},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn invalid_amounts_are_filtered() {
block_on(async {
let data = "\
type,client,tx,amount
deposit,1,1,99999999999999999
deposit,2,2,18446744073709551615
deposit,1,3,18446744073709551616
withdrawal,1,4,0
withdrawal,1,4,
withdrawal,1,4,a
withdrawal,2,5,-1
withdrawal,1,6,-99999999999999999
withdrawal,1,6,-18446744073709551615
withdrawal,1,7,-18446744073709551616";
let expected = vec![
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 0,
client: 1,
tx: 4,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 0,
client: 1,
tx: 4,
},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
#[test]
fn disputes_are_parsed_correctly() {
block_on(async {
let data = "\
type,client,tx,amount
dispute,1,1,
dispute,1,2,";
let expected = vec![
Transaction {
tx_type: TransactionType::Dispute,
amount: 0,
client: 1,
tx: 1,
},
Transaction {
tx_type: TransactionType::Dispute,
amount: 0,
client: 1,
tx: 2,
},
];
let txs = parse(data.as_bytes()).collect::<Vec<Transaction>>().await;
assert_eq!(expected, txs);
});
}
}

View File

@ -1,150 +0,0 @@
use crate::{
stores::ActStore,
types::{Transaction, TransactionType},
};
use std::collections::HashMap;
pub fn process(
t: Transaction,
act_store: &mut dyn ActStore,
tx_store: &mut HashMap<u32, Transaction>,
) {
match t.tx_type {
TransactionType::Deposit => {
act_store.deposit(t.client, t.amount);
tx_store.insert(t.tx, t);
}
TransactionType::Withdrawal => {
act_store.withdraw(t.client, t.amount);
}
TransactionType::Dispute => {
if let Some(orig) = tx_store.get(&t.tx) {
act_store.hold(t.client, orig.amount);
}
}
TransactionType::Resolve => {
if let Some(orig) = tx_store.get(&t.tx) {
act_store.unhold(t.client, orig.amount);
}
}
TransactionType::Chargeback => {
if let Some(orig) = tx_store.get(&t.tx) {
act_store.unhold(t.client, orig.amount);
act_store.withdraw(t.client, orig.amount);
act_store.lock_account(t.client);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stores::MemActStore;
use crate::types::Transaction;
use crate::types::TransactionType;
#[test]
fn valid_tx_and_over_limit_withdraw() {
let mut act_store: Box<dyn ActStore> = Box::new(MemActStore::new());
let mut tx_store: HashMap<u32, Transaction> = HashMap::new();
let txs = vec![
Transaction {
tx_type: TransactionType::Deposit,
amount: 10000,
client: 1,
tx: 1,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 2,
tx: 2,
},
Transaction {
tx_type: TransactionType::Deposit,
amount: 20000,
client: 1,
tx: 3,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 15000,
client: 1,
tx: 4,
},
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 30000,
client: 2,
tx: 5,
},
];
for tx in txs {
process(tx, act_store.as_mut(), &mut tx_store);
}
let act = act_store.get_account(1).unwrap();
assert_eq!(0, act.held());
assert_eq!(15000, act.available());
let act = act_store.get_account(2).unwrap();
assert_eq!(0, act.held());
assert_eq!(20000, act.available());
}
#[test]
fn held_funds() {
let mut act_store: Box<dyn ActStore> = Box::new(MemActStore::new());
let mut tx_store: HashMap<u32, Transaction> = HashMap::new();
process(
Transaction {
tx_type: TransactionType::Deposit,
amount: 10000,
client: 1,
tx: 1,
},
act_store.as_mut(),
&mut tx_store,
);
process(
Transaction {
tx_type: TransactionType::Dispute,
amount: 0,
client: 1,
tx: 1,
},
act_store.as_mut(),
&mut tx_store,
);
let act = act_store.get_account(1).unwrap();
assert_eq!(10000, act.held());
assert_eq!(0, act.available());
process(
Transaction {
tx_type: TransactionType::Withdrawal,
amount: 10000,
client: 1,
tx: 2,
},
act_store.as_mut(),
&mut tx_store,
);
let act = act_store.get_account(1).unwrap();
assert_eq!(10000, act.held());
assert_eq!(0, act.available());
process(
Transaction {
tx_type: TransactionType::Resolve,
amount: 0,
client: 1,
tx: 1,
},
act_store.as_mut(),
&mut tx_store,
);
let act = act_store.get_account(1).unwrap();
assert_eq!(0, act.held());
assert_eq!(10000, act.available());
}
}

View File

@ -1,84 +1,51 @@
use super::ActStore; use super::ActStore;
use crate::types::Account; use crate::types::Account;
use std::collections::hash_map::IntoIter;
use std::collections::HashMap; use std::collections::HashMap;
pub struct MemActStore(HashMap<u16, Account>); pub struct MemActStore(HashMap<u16, Account>);
enum Action {
Withdraw(u64),
Deposit(u64),
Hold(u64),
Unhold(u64),
}
impl MemActStore { impl MemActStore {
pub fn new() -> Self { pub fn new() -> MemActStore {
MemActStore(HashMap::new()) MemActStore(HashMap::new())
} }
fn action_act(&mut self, client: u16, action: Action) -> u64 { fn add_or_sub_balance(&mut self, client: &u16, amnt: isize, sub: bool) -> isize {
let act = self.0.entry(client).or_insert_with(|| Account::new(client)); let act = self
match action { .0
Action::Withdraw(amnt) => act.withdraw(amnt), .entry(*client)
Action::Deposit(amnt) => act.deposit(amnt), .or_insert_with(|| Account::new(*client));
Action::Hold(amnt) => act.hold(amnt), if sub {
Action::Unhold(amnt) => act.unhold(amnt), act.balance -= amnt
} } else {
} act.balance += amnt
} };
act.balance
impl Default for MemActStore {
fn default() -> Self {
Self::new()
}
}
impl IntoIterator for MemActStore {
type Item = (u16, Account);
type IntoIter = IntoIter<u16, Account>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
} }
} }
impl ActStore for MemActStore { impl ActStore for MemActStore {
fn deposit(&mut self, client: u16, amnt: u64) -> u64 { fn add_to_balance(&mut self, client: &u16, amnt: isize) -> isize {
self.action_act(client, Action::Deposit(amnt)) self.add_or_sub_balance(client, amnt, false)
} }
fn withdraw(&mut self, client: u16, amnt: u64) -> u64 { fn sub_from_balance(&mut self, client: &u16, amnt: isize) -> isize {
self.action_act(client, Action::Withdraw(amnt)) self.add_or_sub_balance(client, amnt, true)
} }
fn hold(&mut self, client: u16, amnt: u64) -> u64 { fn hold_amount(&mut self, client: u16, amnt: isize) {
self.action_act(client, Action::Hold(amnt)) todo!()
} }
fn unhold(&mut self, client: u16, amnt: u64) -> u64 { fn lock_account(&mut self, client: u16) {
self.action_act(client, Action::Unhold(amnt)) todo!()
} }
fn lock_account(&mut self, client: u16) -> bool { fn unlock_account(&mut self, client: u16) {
if let Some(act) = self.0.get_mut(&client) { todo!()
act.lock()
} else {
false
}
} }
fn unlock_account(&mut self, client: u16) -> bool { fn get_account(&self, client: &u16) -> Option<&Account> {
if let Some(act) = self.0.get_mut(&client) { self.0.get(client)
act.unlock()
} else {
false
}
}
fn get_account(&self, client: u16) -> Option<&Account> {
self.0.get(&client)
} }
} }
@ -90,18 +57,18 @@ mod tests {
fn test_add_balance() { fn test_add_balance() {
let mut store = MemActStore::new(); let mut store = MemActStore::new();
let client_id = 200; let client_id = 200;
store.deposit(client_id, 200); store.add_to_balance(&client_id, 200);
if let Some(act) = store.get_account(client_id) { if let Some(act) = store.get_account(&client_id) {
assert_eq!(200, act.id()); assert_eq!(200, act.id);
assert_eq!(200, act.available()); assert_eq!(200, act.balance);
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
} }
let balance = store.deposit(client_id, 50); let balance = store.add_to_balance(&client_id, 50);
if let Some(act) = store.get_account(client_id) { if let Some(act) = store.get_account(&client_id) {
assert_eq!(200, act.id()); assert_eq!(200, act.id);
assert_eq!(250, act.available()); assert_eq!(250, act.balance);
assert_eq!(250, balance); assert_eq!(250, balance);
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
@ -112,18 +79,18 @@ mod tests {
fn test_sub_balance() { fn test_sub_balance() {
let mut store = MemActStore::new(); let mut store = MemActStore::new();
let client_id = 1; let client_id = 1;
store.deposit(client_id, 200); store.add_to_balance(&client_id, 200);
if let Some(act) = store.get_account(client_id) { if let Some(act) = store.get_account(&client_id) {
assert_eq!(1, act.id()); assert_eq!(1, act.id);
assert_eq!(200, act.available()); assert_eq!(200, act.balance);
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
} }
let balance = store.withdraw(client_id, 50); let balance = store.sub_from_balance(&client_id, 50);
if let Some(act) = store.get_account(client_id) { if let Some(act) = store.get_account(&client_id) {
assert_eq!(1, act.id()); assert_eq!(1, act.id);
assert_eq!(150, act.available()); assert_eq!(150, act.balance);
assert_eq!(150, balance); assert_eq!(150, balance);
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
@ -131,50 +98,22 @@ mod tests {
} }
#[test] #[test]
fn test_negative_balance() { fn test_lock_negative_balance() {
let mut store = MemActStore::new(); let mut store = MemActStore::new();
let client_id = 1; let client_id = 1;
store.withdraw(client_id, 1); store.sub_from_balance(&client_id, 1);
if let Some(act) = store.get_account(client_id) { if let Some(act) = store.get_account(&client_id) {
assert_eq!(1, act.id()); assert_eq!(1, act.id);
assert_eq!(0, act.available()); assert_eq!(-1, act.balance);
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
} }
} let balance = store.sub_from_balance(&client_id, 50);
if let Some(act) = store.get_account(&client_id) {
#[test] assert_eq!(1, act.id);
fn test_hold() { assert_eq!(150, act.balance);
let mut store = MemActStore::new(); assert_eq!(150, balance);
let client_id = 1;
store.deposit(client_id, 100);
let avail = store.hold(client_id, 10);
assert_eq!(90, avail);
if let Some(act) = store.get_account(client_id) {
assert_eq!(1, act.id());
assert_eq!(90, act.available());
} else {
panic!("Could not get account from store");
}
assert_eq!(0, store.hold(client_id, 100));
assert_eq!(10, store.unhold(client_id, 20));
}
#[test]
fn test_lock() {
let mut store = MemActStore::new();
let client_id = 1;
store.deposit(client_id, 100);
let locked = store.lock_account(client_id);
assert!(locked);
if let Some(act) = store.get_account(client_id) {
assert_eq!(1, act.id());
assert!(act.is_locked());
} else { } else {
panic!("Could not get account from store"); panic!("Could not get account from store");
} }

View File

@ -4,11 +4,12 @@ pub use mem::MemActStore;
use crate::types::Account; use crate::types::Account;
pub trait ActStore { pub trait ActStore {
fn get_account(&self, client: u16) -> Option<&Account>;
fn deposit(&mut self, client: u16, amnt: u64) -> u64; fn get_account(&self, client: &u16) -> Option<&Account>;
fn withdraw(&mut self, client: u16, amnt: u64) -> u64; fn add_to_balance(&mut self, client: &u16, amnt: isize) -> isize;
fn hold(&mut self, client: u16, amnt: u64) -> u64; fn sub_from_balance(&mut self, client: &u16, amnt: isize) -> isize;
fn unhold(&mut self, client: u16, amnt: u64) -> u64; fn hold_amount(&mut self, client:u16, amnt: isize);
fn lock_account(&mut self, client: u16) -> bool; fn lock_account(&mut self, client: u16);
fn unlock_account(&mut self, client: u16) -> bool; fn unlock_account(&mut self, client: u16);
} }

View File

@ -1,113 +1,29 @@
use serde::{Serialize, Serializer}; use serde::Serialize;
const PRECISION: u32 = 4;
#[derive(Debug, PartialEq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Account { pub struct Account {
id: u16, pub id: u16,
total: u64, pub balance: isize,
held: u64, pub held: usize,
locked: bool, pub locked: bool,
} }
impl Account { impl Account {
pub fn new(client_id: u16) -> Account { pub fn new(client_id: u16) -> Account {
Account { Account {
id: client_id, id: client_id,
total: 0, balance: 0,
held: 0, held: 0,
locked: false, locked: false,
} }
} }
pub fn with_balance(client_id: u16, seed_balance: u64) -> Account { pub fn with_balance(client_id: u16, seed_balance: isize) -> Account {
Account { Account {
id: client_id, id: client_id,
total: seed_balance, balance: seed_balance,
held: 0, held: 0,
locked: false, locked: false,
} }
} }
pub fn id(&self) -> u16 {
self.id
}
pub fn available(&self) -> u64 {
self.total.saturating_sub(self.held)
}
pub fn held(&self) -> u64 {
self.held
}
pub fn hold(&mut self, amnt: u64) -> u64 {
if let Some(new_held) = self.held.checked_add(amnt) {
self.held = new_held;
}
self.available()
}
pub fn unhold(&mut self, amnt: u64) -> u64 {
if let Some(new_held) = self.held.checked_sub(amnt) {
self.held = new_held;
}
self.available()
}
pub fn deposit(&mut self, amnt: u64) -> u64 {
if let Some(new_bal) = self.total.checked_add(amnt) {
self.total = new_bal;
};
self.available()
}
pub fn withdraw(&mut self, amnt: u64) -> u64 {
if self.available().checked_sub(amnt).is_some() {
self.total -= amnt;
};
self.available()
}
pub fn lock(&mut self) -> bool {
self.locked = true;
self.locked
}
pub fn unlock(&mut self) -> bool {
self.locked = false;
self.locked
}
pub fn is_locked(&self) -> bool {
self.locked
}
}
#[derive(Debug, Serialize, PartialEq)]
pub struct AccountSer {
id: u16,
total: String,
held: String,
available: String,
locked: bool,
}
impl serde::Serialize for Account {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let total = format!(
"{0:.4}",
(self.total as f64) / (10u64.pow(PRECISION) as f64)
);
let held = format!("{0:.4}", (self.held as f64) / (10u64.pow(PRECISION) as f64));
let available = format!(
"{0:.4}",
(self.available() as f64) / (10u64.pow(PRECISION) as f64)
);
let ser = AccountSer {
id: self.id,
total,
held,
available,
locked: self.locked,
};
ser.serialize(s)
}
} }

View File

@ -7,9 +7,6 @@ const PRECISION: u32 = 4;
pub enum TransactionType { pub enum TransactionType {
Deposit, Deposit,
Withdrawal, Withdrawal,
Dispute,
Resolve,
Chargeback,
} }
#[derive(Debug, Deserialize, PartialEq)] #[derive(Debug, Deserialize, PartialEq)]
@ -17,33 +14,33 @@ pub struct Transaction {
#[serde(rename = "type")] #[serde(rename = "type")]
pub tx_type: TransactionType, pub tx_type: TransactionType,
pub client: u16, pub client: u16,
pub tx: u32, pub tx: usize,
/// Amount of the smallest unit, e.g. 0.0001 as per the specification /// Amount of the smallest unit, e.g. 0.0001 as per the specification
#[serde(deserialize_with = "de_amount")] #[serde(deserialize_with = "de_amount")]
pub amount: u64, pub amount: usize,
} }
fn de_amount<'de, D>(deserializer: D) -> Result<u64, D::Error> fn de_amount<'de, D>(deserializer: D) -> Result<usize, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
//TODO use something like num_bigint instead //TODO validate for input such as `.100` so it doesn't give 100
let deserialized = String::deserialize(deserializer)?; let deserialized = String::deserialize(deserializer)?;
let mut splitted = deserialized.split('.'); let mut splitted = deserialized.split('.');
let units = splitted let units = splitted
.next() .next()
.map_or(Ok(0), |v| match v { .map_or(Ok(0usize), |v| v.parse::<usize>())
"" => Ok(0),
_ => v.parse::<u64>(),
})
.map_err(de::Error::custom)? .map_err(de::Error::custom)?
.checked_mul(10u64.pow(PRECISION)) .checked_mul(10usize.pow(PRECISION))
.ok_or_else(|| de::Error::custom("Value too large"))?; .ok_or_else(|| de::Error::custom("Value too large"))?;
//TODO format! here isn't great! //TODO improve this to avoid `format!`
let dec = splitted let dec = splitted
.next() .next()
.map_or(Ok(0u64), |v| format!("{:0<4.4}", v).parse::<u64>()) .map_or(Ok(0usize), |v| format!("{:0<4.4}", v).parse::<usize>())
.map_err(de::Error::custom)?; .map_err(de::Error::custom)?;
Ok(units + dec) Ok(units + dec)
} }