diff --git a/Cargo.lock b/Cargo.lock index 3ecb1ec..df38e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ name = "act" version = "0.1.0" dependencies = [ "async-stream", + "clap", "csv", "serde", "strum", @@ -16,6 +17,15 @@ dependencies = [ "tokio-test", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "async-stream" version = "0.3.0" @@ -37,6 +47,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -79,6 +100,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "csv" version = "1.1.6" @@ -349,6 +385,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strum" version = "0.20.0" @@ -381,6 +423,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "tokio" version = "1.3.0" @@ -442,12 +493,24 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ae7028a..18ed3e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ tokio-stream = "0.1" async-stream = "0.3" csv = "1.1.5" strum_macros = "0.20.1" +clap = "2.33.3" [dev-dependencies] tokio-test = "0.4.0" diff --git a/input.csv b/input.csv new file mode 100644 index 0000000..57b50e8 --- /dev/null +++ b/input.csv @@ -0,0 +1,6 @@ +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 diff --git a/src/bin/act.rs b/src/bin/act.rs new file mode 100644 index 0000000..aa55b26 --- /dev/null +++ b/src/bin/act.rs @@ -0,0 +1,29 @@ +use clap::{App, Arg}; +use std::io::{BufReader, BufRead, stdin}; +use std::fs; +use act::parse; +use act::stores::ActStore; +use act::stores::MemActStore; +use tokio_stream::StreamExt; + +#[tokio::main] +async fn main() { + let matches = App::new("act") + .version("0.1") + .about("Merges transactions into account state") + .author("Lewis Diamond") + .arg(Arg::with_name("input").required(false).index(1).help("Input file, stdin if omitted or -")) + .get_matches(); + + let input: Box = match matches.value_of("input") { + Some("-")|Some("")|None => Box::new(BufReader::new(stdin())), + Some(f) => Box::new(BufReader::new(fs::File::open(f).unwrap())), + }; + + + let s = parse(input); + tokio::pin!(s); + while let Some(v) = s.next().await { + println!("{:?}", v); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1322eef..0510b4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod types; +pub mod stores; use async_stream::stream; use std::io::Read; use tokio_stream::Stream; @@ -100,7 +101,7 @@ withdrawal,2,5,10.0110101"; } #[test] - fn invalid_amoutns_are_filtered() { + fn invalid_amounts_are_filtered() { block_on(async { let data = "\ type,client,tx,amount @@ -117,7 +118,6 @@ withdrawal,1,7,-18446744073709551616"; Transaction {tx_type: TransactionType::Withdrawal, amount: 0, client: 1, tx: 4}, ]; let txs = parse(data.as_bytes()).collect::>().await; - println!("PARSE: {}", "0.1".parse::().unwrap()); assert_eq!(expected, txs); }); diff --git a/src/stores/mem.rs b/src/stores/mem.rs new file mode 100644 index 0000000..0803757 --- /dev/null +++ b/src/stores/mem.rs @@ -0,0 +1,121 @@ +use super::ActStore; +use crate::types::Account; +use std::collections::HashMap; + +pub struct MemActStore(HashMap); + +impl MemActStore { + pub fn new() -> MemActStore { + MemActStore(HashMap::new()) + } + + fn add_or_sub_balance(&mut self, client: &u16, amnt: usize, sub: bool) -> usize { + let act = self + .0 + .entry(*client) + .or_insert_with(|| Account::new(*client)); + if sub { + act.balance -= amnt + } else { + act.balance += amnt + }; + act.balance + } +} + +impl ActStore for MemActStore { + fn add_to_balance(&mut self, client: &u16, amnt: usize) -> usize { + self.add_or_sub_balance(client, amnt, false) + } + + fn sub_from_balance(&mut self, client: &u16, amnt: usize) -> usize { + self.add_or_sub_balance(client, amnt, true) + } + + fn hold_amount(&mut self, client: u16, amnt: usize) { + todo!() + } + + fn lock_account(&mut self, client: u16) { + todo!() + } + + fn unlock_account(&mut self, client: u16) { + todo!() + } + + fn get_account(&self, client: &u16) -> Option<&Account> { + self.0.get(client) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_balance() { + let mut store = MemActStore::new(); + let client_id = 200; + store.add_to_balance(&client_id, 200); + + if let Some(act) = store.get_account(&client_id) { + assert_eq!(200, act.id); + assert_eq!(200, act.balance); + } 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); + assert_eq!(250, balance); + } else { + panic!("Could not get account from store"); + } + } + + #[test] + fn test_sub_balance() { + let mut store = MemActStore::new(); + let client_id = 1; + store.add_to_balance(&client_id, 200); + + if let Some(act) = store.get_account(&client_id) { + assert_eq!(1, act.id); + assert_eq!(200, act.balance); + } 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); + } else { + panic!("Could not get account from store"); + } + } + + #[test] + fn test_lock_negative_balance() { + let mut store = MemActStore::new(); + let client_id = 1; + store.sub_from_balance(&client_id, 1); + + if let Some(act) = store.get_account(&client_id) { + assert_eq!(1, act.id); + assert_eq!(-1, act.balance); + } 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); + } else { + panic!("Could not get account from store"); + } + } +} diff --git a/src/stores/mod.rs b/src/stores/mod.rs new file mode 100644 index 0000000..bf7a316 --- /dev/null +++ b/src/stores/mod.rs @@ -0,0 +1,15 @@ +pub mod mem; +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: usize) -> usize; + fn sub_from_balance(&mut self, client: &u16, amnt: usize) -> usize; + fn hold_amount(&mut self, client:u16, amnt: usize); + fn lock_account(&mut self, client: u16); + fn unlock_account(&mut self, client: u16); + +} diff --git a/src/types/account.rs b/src/types/account.rs new file mode 100644 index 0000000..d387542 --- /dev/null +++ b/src/types/account.rs @@ -0,0 +1,29 @@ +use serde::Serialize; + +#[derive(Debug, Serialize, PartialEq)] +pub struct Account { + pub id: u16, + pub balance: isize, + pub held: usize, + pub locked: bool, +} + +impl Account { + pub fn new(client_id: u16) -> Account { + Account { + id: client_id, + balance: 0, + held: 0, + locked: false, + } + } + + pub fn with_balance(client_id: u16, seed_balance: isize) -> Account { + Account { + id: client_id, + balance: seed_balance, + held: 0, + locked: false, + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 908f08f..205ba43 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,43 +1,5 @@ -use serde::de; -use serde::{Deserialize, Deserializer}; -const PRECISION: u32 = 4; - -#[derive(Eq, PartialEq, Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum TransactionType { - Deposit, - Withdrawal, -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Transaction { - #[serde(rename = "type")] - pub tx_type: TransactionType, - pub client: u16, - pub tx: usize, - /// Amount of the smallest unit, e.g. 0.0001 as per the specification - #[serde(deserialize_with = "de_amount")] - pub amount: usize, -} - -fn de_amount<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - //TODO validate for input such as `.100` so it doesn't give 100 - let deserialized = String::deserialize(deserializer)?; - let mut splitted = deserialized.split('.'); - let units = splitted - .next() - .map_or(Ok(0usize), |v| v.parse::()) - .map_err(de::Error::custom)? - .checked_mul(10usize.pow(PRECISION)) - .ok_or_else(|| de::Error::custom("Value too large"))?; - //TODO improve this to avoid `format!` - let dec = splitted - .next() - .map_or(Ok(0usize), |v| format!("{:0<4.4}", v).parse::()) - .map_err(de::Error::custom)?; - - Ok(units + dec) -} +pub mod transaction; +pub use transaction::Transaction; +pub use transaction::TransactionType; +pub mod account; +pub use account::Account; diff --git a/src/types/transaction.rs b/src/types/transaction.rs new file mode 100644 index 0000000..c6949f8 --- /dev/null +++ b/src/types/transaction.rs @@ -0,0 +1,46 @@ +use serde::de; +use serde::{Deserialize, Deserializer}; +const PRECISION: u32 = 4; + +#[derive(Eq, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TransactionType { + Deposit, + Withdrawal, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Transaction { + #[serde(rename = "type")] + pub tx_type: TransactionType, + pub client: u16, + pub tx: usize, + /// Amount of the smallest unit, e.g. 0.0001 as per the specification + #[serde(deserialize_with = "de_amount")] + pub amount: usize, +} + +fn de_amount<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + //TODO validate for input such as `.100` so it doesn't give 100 + let deserialized = String::deserialize(deserializer)?; + let mut splitted = deserialized.split('.'); + let units = splitted + .next() + .map_or(Ok(0usize), |v| v.parse::()) + .map_err(de::Error::custom)? + .checked_mul(10usize.pow(PRECISION)) + .ok_or_else(|| de::Error::custom("Value too large"))?; + //TODO improve this to avoid `format!` + let dec = splitted + .next() + .map_or(Ok(0usize), |v| format!("{:0<4.4}", v).parse::()) + .map_err(de::Error::custom)?; + + Ok(units + dec) +} + + +