Compare commits
No commits in common. "6f8eed44123c63a366c5a0afda28690e7e67a7c0" and "ae6868975f76bc6eb497ee6fdf5d17c3d2fca16c" have entirely different histories.
6f8eed4412
...
ae6868975f
@ -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,
|
||||
|
|
8
input2.csv
Normal file
8
input2.csv
Normal 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,
|
|
8
input3.csv
Normal file
8
input3.csv
Normal 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,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();
|
||||
}
|
||||
}
|
||||
|
127
src/lib.rs
127
src/lib.rs
@ -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
240
src/parse.rs
Normal 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
150
src/process.rs
Normal 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());
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user