Compare commits

...

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

11 changed files with 660 additions and 213 deletions

View File

@ -1,6 +1,7 @@
type,client,tx,amount
deposit,1,1,1.0
deposit,2,2,2.0
deposit,1,3,2.0
deposit,1,3,2.742
withdrawal,1,4,1.5
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.0 2.742
5 withdrawal 1 4 1.5
6 withdrawal 2 5 3.0
7 dispute 2 2

8
input2.csv Normal file
View File

@ -0,0 +1,8 @@
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

8
input3.csv Normal file
View File

@ -0,0 +1,8 @@
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,18 +1,25 @@
use clap::{App, Arg};
use std::io::{BufReader, BufRead, stdin};
use std::fs;
use act::parse;
use act::stores::ActStore;
use act::parse::parse;
use act::process::process;
use act::stores::MemActStore;
use act::types::Transaction;
use clap::{App, Arg};
use std::collections::HashMap;
use std::fs;
use std::io::{stdin, BufRead, BufReader};
use tokio_stream::StreamExt;
#[tokio::main]
async fn main() {
let matches = App::new("act")
.version("0.1")
.about("Merges transactions into account state")
.about("Merges transactions into final account state")
.author("Lewis Diamond")
.arg(Arg::with_name("input").required(false).index(1).help("Input file, stdin if omitted or -"))
.arg(
Arg::with_name("input")
.required(false)
.index(1)
.help("Input file, stdin if omitted or -"),
)
.get_matches();
let input: Box<dyn BufRead> = match matches.value_of("input") {
@ -20,10 +27,17 @@ async fn main() {
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);
tokio::pin!(s);
while let Some(v) = s.next().await {
println!("{:?}", v);
process(v, &mut act_store, &mut tx_store);
}
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,125 +1,4 @@
pub mod types;
pub mod parse;
pub mod process;
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);
});
}
}
pub mod types;

240
src/parse.rs Normal file
View File

@ -0,0 +1,240 @@
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);
});
}
}

150
src/process.rs Normal file
View File

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

View File

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

View File

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