diff --git a/Cargo.lock b/Cargo.lock index 3e6a44b..4c0d8c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,133 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fpdec" version = "0.5.4" @@ -28,6 +155,130 @@ dependencies = [ "quote", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + [[package]] name = "proc-macro2" version = "1.0.43" @@ -41,7 +292,10 @@ dependencies = [ name = "processor" version = "0.1.0" dependencies = [ + "csv", "fpdec", + "serde", + "serde_with", ] [[package]] @@ -53,8 +307,184 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa 1.0.3", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89df7a26519371a3cce44fbb914c2819c84d9b897890987fa3ab096491cc0ea8" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de337f322382fcdfbb21a014f7c224ee041a23785651db67b9827403178f698f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" +dependencies = [ + "itoa 1.0.3", + "libc", + "num_threads", + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 3bd25fa..9f5c338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +csv = "1.1" fpdec = "0.5" +serde = { version = "1.0", features = ["derive"] } +serde_with = "2.0" diff --git a/src/core.rs b/src/core.rs index 7e239cf..8cb649f 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,16 +1,27 @@ //! Core types used in the processing of payments. +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + /// Clients are anonymous, identified by globally unique ids. "16-bit ought to be enough for /// anyone". -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, +)] +#[serde(transparent)] pub struct ClientId(pub u16); /// Transactions are identified by a globally unique id. 32 bit is sufficient for our puposes. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, +)] +#[serde(transparent)] pub struct TxId(pub u32); /// Amounts are represented as exact decimals, up to four places past the decimal. /// For ease of implementation, make use of [fpdec::Decimal] instead of implementing a custom /// fixed-point number. -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxAmount(pub fpdec::Decimal); +#[serde_as] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[serde(transparent)] +pub struct TxAmount(#[serde_as(as = "DisplayFromStr")] pub fpdec::Decimal); diff --git a/src/transaction.rs b/src/transaction.rs index a9f8110..6ada41c 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,8 +1,11 @@ //! Define all supported transactions. use crate::core::{ClientId, TxAmount, TxId}; +use serde::Deserialize; + /// A generic [Transaction]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(try_from = "TransactionRecord")] pub enum Transaction { Deposit(Deposit), Withdrawal(Withdrawal), @@ -11,6 +14,61 @@ pub enum Transaction { Chargeback(Chargeback), } +impl Transaction { + /// Build a [csv::ReaderBuilder] configured to read a CSV formatted [Transaction] stream. + pub fn configured_csv_reader_builder() -> csv::ReaderBuilder { + let mut builder = csv::ReaderBuilder::new(); + builder + // Expect header input + .has_headers(true) + // Allow whitespace + .trim(csv::Trim::All) + // Allow trailing fields to be omitted + .flexible(true); + builder + } +} + +// A type used to deserialize [Transaction] from an input CSV stream. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +struct TransactionRecord<'a> { + #[serde(rename = "type")] + type_: &'a str, + client: ClientId, + tx: TxId, + amount: Option, +} + +impl TryFrom> for Transaction { + // FIXME: use an actual error type. + type Error = String; + + fn try_from(value: TransactionRecord<'_>) -> Result { + let TransactionRecord { + type_, + client, + tx, + amount, + } = value; + + let transaction = match type_ { + "deposit" => { + let amount = amount.ok_or("Missing amount for transaction")?; + Transaction::Deposit(Deposit { client, tx, amount }) + } + "withdrawal" => { + let amount = amount.ok_or("Missing amount for transaction")?; + Transaction::Withdrawal(Withdrawal { client, tx, amount }) + } + "dispute" => Transaction::Dispute(Dispute { client, tx }), + "resolve" => Transaction::Resolve(Resolve { client, tx }), + "chargeback" => Transaction::Chargeback(Chargeback { client, tx }), + _ => return Err(format!("Unkown transaction type '{}'", type_)), + }; + Ok(transaction) + } +} + /// Deposit funds into an account, i.e: increase its balance by the amount given. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Deposit { @@ -55,3 +113,117 @@ pub struct Chargeback { pub client: ClientId, pub tx: TxId, } + +#[cfg(test)] +mod test { + use super::*; + use fpdec::{Dec, Decimal}; + + fn parse_transaction(input: &str) -> Transaction { + let rdr = Transaction::configured_csv_reader_builder().from_reader(input.as_bytes()); + rdr.into_deserialize().next().unwrap().unwrap() + } + + #[test] + fn deserialize_deposit() { + let data = "type,client,tx,amount\ndeposit,1,2,3.0"; + assert_eq!( + parse_transaction(data), + Transaction::Deposit(Deposit { + client: ClientId(1), + tx: TxId(2), + amount: TxAmount(Dec!(3.0)) + }), + ); + } + + #[test] + fn deserialize_withdrawal() { + let data = "type,client,tx,amount\nwithdrawal,1,2,3.0"; + assert_eq!( + parse_transaction(data), + Transaction::Withdrawal(Withdrawal { + client: ClientId(1), + tx: TxId(2), + amount: TxAmount(Dec!(3.0)) + }), + ); + } + + #[test] + fn deserialize_dispute() { + let data = "type,client,tx,amount\ndispute,1,2"; + assert_eq!( + parse_transaction(data), + Transaction::Dispute(Dispute { + client: ClientId(1), + tx: TxId(2), + }), + ); + } + + #[test] + fn deserialize_resolve() { + let data = "type,client,tx,amount\nresolve,1,2"; + assert_eq!( + parse_transaction(data), + Transaction::Resolve(Resolve { + client: ClientId(1), + tx: TxId(2), + }), + ); + } + + #[test] + fn deserialize_chargeback() { + let data = "type,client,tx,amount\nchargeback,1,2"; + assert_eq!( + parse_transaction(data), + Transaction::Chargeback(Chargeback { + client: ClientId(1), + tx: TxId(2), + }), + ); + } + + #[test] + fn deserialize_transactions() { + let data = concat!( + "type,client,tx,amount\n", + "deposit, 1, 2, 12.0000\n", + "withdrawal , 3,4, 42.27 \n", + "dispute, 5 , 6, \n", + " resolve,7,8,\n", + "chargeback,9,10", + ); + let rdr = Transaction::configured_csv_reader_builder().from_reader(data.as_bytes()); + let transactions: Result, _> = rdr.into_deserialize().collect(); + assert_eq!( + transactions.unwrap(), + vec![ + Transaction::Deposit(Deposit { + client: ClientId(1), + tx: TxId(2), + amount: TxAmount(Dec!(12.0000)), + }), + Transaction::Withdrawal(Withdrawal { + client: ClientId(3), + tx: TxId(4), + amount: TxAmount(Dec!(42.27)), + }), + Transaction::Dispute(Dispute { + client: ClientId(5), + tx: TxId(6), + }), + Transaction::Resolve(Resolve { + client: ClientId(7), + tx: TxId(8), + }), + Transaction::Chargeback(Chargeback { + client: ClientId(9), + tx: TxId(10), + }), + ] + ); + } +}