Compare commits
No commits in common. "6f8eed44123c63a366c5a0afda28690e7e67a7c0" and "ae6868975f76bc6eb497ee6fdf5d17c3d2fca16c" have entirely different histories.
6f8eed4412
...
ae6868975f
@ -1,6 +1,7 @@
|
|||||||
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.0
|
deposit,1,3,2.742
|
||||||
withdrawal,1,4,1.5
|
withdrawal,1,4,1.5
|
||||||
withdrawal,2,5,3.0
|
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,29 +1,43 @@
|
|||||||
use clap::{App, Arg};
|
use act::parse::parse;
|
||||||
use std::io::{BufReader, BufRead, stdin};
|
use act::process::process;
|
||||||
use std::fs;
|
|
||||||
use act::parse;
|
|
||||||
use act::stores::ActStore;
|
|
||||||
use act::stores::MemActStore;
|
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;
|
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 account state")
|
.about("Merges transactions into final account state")
|
||||||
.author("Lewis Diamond")
|
.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();
|
.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 {
|
||||||
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;
|
pub mod stores;
|
||||||
use async_stream::stream;
|
pub mod types;
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
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 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() -> MemActStore {
|
pub fn new() -> Self {
|
||||||
MemActStore(HashMap::new())
|
MemActStore(HashMap::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_or_sub_balance(&mut self, client: &u16, amnt: isize, sub: bool) -> isize {
|
fn action_act(&mut self, client: u16, action: Action) -> u64 {
|
||||||
let act = self
|
let act = self.0.entry(client).or_insert_with(|| Account::new(client));
|
||||||
.0
|
match action {
|
||||||
.entry(*client)
|
Action::Withdraw(amnt) => act.withdraw(amnt),
|
||||||
.or_insert_with(|| Account::new(*client));
|
Action::Deposit(amnt) => act.deposit(amnt),
|
||||||
if sub {
|
Action::Hold(amnt) => act.hold(amnt),
|
||||||
act.balance -= amnt
|
Action::Unhold(amnt) => act.unhold(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 add_to_balance(&mut self, client: &u16, amnt: isize) -> isize {
|
fn deposit(&mut self, client: u16, amnt: u64) -> u64 {
|
||||||
self.add_or_sub_balance(client, amnt, false)
|
self.action_act(client, Action::Deposit(amnt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sub_from_balance(&mut self, client: &u16, amnt: isize) -> isize {
|
fn withdraw(&mut self, client: u16, amnt: u64) -> u64 {
|
||||||
self.add_or_sub_balance(client, amnt, true)
|
self.action_act(client, Action::Withdraw(amnt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hold_amount(&mut self, client: u16, amnt: isize) {
|
fn hold(&mut self, client: u16, amnt: u64) -> u64 {
|
||||||
todo!()
|
self.action_act(client, Action::Hold(amnt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lock_account(&mut self, client: u16) {
|
fn unhold(&mut self, client: u16, amnt: u64) -> u64 {
|
||||||
todo!()
|
self.action_act(client, Action::Unhold(amnt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unlock_account(&mut self, client: u16) {
|
fn lock_account(&mut self, client: u16) -> bool {
|
||||||
todo!()
|
if let Some(act) = self.0.get_mut(&client) {
|
||||||
|
act.lock()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_account(&self, client: &u16) -> Option<&Account> {
|
fn unlock_account(&mut self, client: u16) -> bool {
|
||||||
self.0.get(client)
|
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() {
|
fn test_add_balance() {
|
||||||
let mut store = MemActStore::new();
|
let mut store = MemActStore::new();
|
||||||
let client_id = 200;
|
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) {
|
if let Some(act) = store.get_account(client_id) {
|
||||||
assert_eq!(200, act.id);
|
assert_eq!(200, act.id());
|
||||||
assert_eq!(200, act.balance);
|
assert_eq!(200, act.available());
|
||||||
} else {
|
} else {
|
||||||
panic!("Could not get account from store");
|
panic!("Could not get account from store");
|
||||||
}
|
}
|
||||||
let balance = store.add_to_balance(&client_id, 50);
|
let balance = store.deposit(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.balance);
|
assert_eq!(250, act.available());
|
||||||
assert_eq!(250, balance);
|
assert_eq!(250, balance);
|
||||||
} else {
|
} else {
|
||||||
panic!("Could not get account from store");
|
panic!("Could not get account from store");
|
||||||
@ -79,18 +112,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.add_to_balance(&client_id, 200);
|
store.deposit(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.balance);
|
assert_eq!(200, act.available());
|
||||||
} 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);
|
let balance = store.withdraw(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.balance);
|
assert_eq!(150, act.available());
|
||||||
assert_eq!(150, balance);
|
assert_eq!(150, balance);
|
||||||
} else {
|
} else {
|
||||||
panic!("Could not get account from store");
|
panic!("Could not get account from store");
|
||||||
@ -98,22 +131,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lock_negative_balance() {
|
fn test_negative_balance() {
|
||||||
let mut store = MemActStore::new();
|
let mut store = MemActStore::new();
|
||||||
let client_id = 1;
|
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) {
|
if let Some(act) = store.get_account(client_id) {
|
||||||
assert_eq!(1, act.id);
|
assert_eq!(1, act.id());
|
||||||
assert_eq!(-1, act.balance);
|
assert_eq!(0, act.available());
|
||||||
} 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) {
|
|
||||||
assert_eq!(1, act.id);
|
#[test]
|
||||||
assert_eq!(150, act.balance);
|
fn test_hold() {
|
||||||
assert_eq!(150, balance);
|
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 {
|
} else {
|
||||||
panic!("Could not get account from store");
|
panic!("Could not get account from store");
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,11 @@ 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 get_account(&self, client: &u16) -> Option<&Account>;
|
fn deposit(&mut self, client: u16, amnt: u64) -> u64;
|
||||||
fn add_to_balance(&mut self, client: &u16, amnt: isize) -> isize;
|
fn withdraw(&mut self, client: u16, amnt: u64) -> u64;
|
||||||
fn sub_from_balance(&mut self, client: &u16, amnt: isize) -> isize;
|
fn hold(&mut self, client: u16, amnt: u64) -> u64;
|
||||||
fn hold_amount(&mut self, client:u16, amnt: isize);
|
fn unhold(&mut self, client: u16, amnt: u64) -> u64;
|
||||||
fn lock_account(&mut self, client: u16);
|
fn lock_account(&mut self, client: u16) -> bool;
|
||||||
fn unlock_account(&mut self, client: u16);
|
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 struct Account {
|
||||||
pub id: u16,
|
id: u16,
|
||||||
pub balance: isize,
|
total: u64,
|
||||||
pub held: usize,
|
held: u64,
|
||||||
pub locked: bool,
|
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,
|
||||||
balance: 0,
|
total: 0,
|
||||||
held: 0,
|
held: 0,
|
||||||
locked: false,
|
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 {
|
Account {
|
||||||
id: client_id,
|
id: client_id,
|
||||||
balance: seed_balance,
|
total: 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ 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)]
|
||||||
@ -14,33 +17,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: usize,
|
pub tx: u32,
|
||||||
/// 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: 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
|
where
|
||||||
D: Deserializer<'de>,
|
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 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(0usize), |v| v.parse::<usize>())
|
.map_or(Ok(0), |v| match v {
|
||||||
|
"" => Ok(0),
|
||||||
|
_ => v.parse::<u64>(),
|
||||||
|
})
|
||||||
.map_err(de::Error::custom)?
|
.map_err(de::Error::custom)?
|
||||||
.checked_mul(10usize.pow(PRECISION))
|
.checked_mul(10u64.pow(PRECISION))
|
||||||
.ok_or_else(|| de::Error::custom("Value too large"))?;
|
.ok_or_else(|| de::Error::custom("Value too large"))?;
|
||||||
//TODO improve this to avoid `format!`
|
//TODO format! here isn't great!
|
||||||
let dec = splitted
|
let dec = splitted
|
||||||
.next()
|
.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)?;
|
.map_err(de::Error::custom)?;
|
||||||
|
|
||||||
Ok(units + dec)
|
Ok(units + dec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user