Compare commits
No commits in common. "ae6868975f76bc6eb497ee6fdf5d17c3d2fca16c" and "6f8eed44123c63a366c5a0afda28690e7e67a7c0" have entirely different histories.
ae6868975f
...
6f8eed4412
@ -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,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,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,25 +1,18 @@
|
|||||||
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") {
|
||||||
@ -27,17 +20,10 @@ async fn main() {
|
|||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
127
src/lib.rs
127
src/lib.rs
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
240
src/parse.rs
240
src/parse.rs
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
150
src/process.rs
150
src/process.rs
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user