/Users/oliverbrotchie/Documents/GitHub/csv_ledger/src/ledger.rs
Line | Count | Source |
1 | | //! # Ledger |
2 | | //! This module contains `Ledger`, the state store used for this CLI. |
3 | | //! |
4 | | //! `Ledger` stores running totals of client accounts, consumes csv files and |
5 | | //! outputs associated to account statements to string. |
6 | | //! |
7 | | //! **Basic example:** |
8 | | //! ```rust |
9 | | //! use crate::ledger::ledger |
10 | | //! |
11 | | //! fn main() { |
12 | | //! // Read in a new file |
13 | | //! let reader = BufReader::new(File::open("./foo.csv").unwrap()); |
14 | | //! |
15 | | //! // Create a new ledger and read in the csv file line by line |
16 | | //! let ledger = Ledger::default(); |
17 | | //! ledger.consume_csv(reader); |
18 | | //! |
19 | | //! // Print out the result |
20 | | //! println!("{}", ledger); |
21 | | //! } |
22 | | //! ``` |
23 | | |
24 | | use crate::{ |
25 | | parse::{parse_header, parse_transaction, Transaction}, |
26 | | LedgerErr, |
27 | | }; |
28 | | use std::{ |
29 | | collections::{BTreeMap, HashMap}, |
30 | | fmt::{self, Display}, |
31 | | io::{BufRead, BufReader, Read}, |
32 | | }; |
33 | | |
34 | 13 | #[derive(Default, Debug1 )] |
35 | | pub struct Ledger { |
36 | | /// The list of client accounts. |
37 | | pub clients: HashMap<u16, ClientData>, |
38 | | /// The list of transactions. Note: This is a nieve implementation of transaction storage, |
39 | | /// requiring all transactions to be stored in memory. Due to there being no maximum limmit to |
40 | | /// how old a transaction can be for a `hold` to be applied, all transactions must be addressable. |
41 | | pub transactions: BTreeMap<u32, i64>, |
42 | | } |
43 | | |
44 | | /// An individual client account. |
45 | 1 | #[derive(Debug)] |
46 | | pub struct ClientData { |
47 | | held: BTreeMap<u32, i64>, |
48 | | available: i64, |
49 | | total: i64, |
50 | | locked: bool, |
51 | | } |
52 | | |
53 | | impl Ledger { |
54 | | #[inline] |
55 | | /// Consume a `BufReader` that contains a csv file of transactions. |
56 | | pub fn consume_csv<T>(&mut self, mut reader: BufReader<T>) -> Result<(), LedgerErr> |
57 | | where |
58 | | T: Read, |
59 | | { |
60 | 12 | validate_header(&mut reader)?4 ; |
61 | | |
62 | 18 | for (index, line) in reader.lines().enumerate()8 { |
63 | 18 | let res17 = line.map_err(LedgerErr::Reading)?1 ; // map_err is used to provide better debug info |
64 | 17 | if !res.trim().is_empty() { |
65 | 13 | match parse_transaction(&res) |
66 | 13 | .map_err(|err| LedgerErr::from_parse(err, index + 2)1 )?1 Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReaderTwo>::{closure#0}Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<std::fs::File>::{closure#0}Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReader>::{closure#0}Unexecuted instantiation: <csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&[u8; 1usize]>>::{closure#0}<csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&str>>::{closure#0}Line | Count | Source | 66 | 1 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? |
|
67 | | { |
68 | 1 | Transaction::Withdrawal(id, tx, amount) => { |
69 | 1 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals |
70 | | } |
71 | 7 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), |
72 | 2 | Transaction::Dispute(id, tx) => self.hold(id, tx), |
73 | 1 | Transaction::Resolve(id, tx) => self.resolve(id, tx), |
74 | 1 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), |
75 | | } |
76 | 4 | } |
77 | | } |
78 | | |
79 | 6 | Ok(()) |
80 | 12 | } <csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&[u8; 1usize]>> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?; | 61 | | | 62 | 0 | for (index, line) in reader.lines().enumerate() { | 63 | 0 | let res = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<std::fs::File> Line | Count | Source | 60 | 6 | validate_header(&mut reader)?1 ; | 61 | | | 62 | 5 | for (index, line) in reader.lines().enumerate() { | 63 | 5 | let res = line.map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 64 | 5 | if !res.trim().is_empty() { | 65 | 5 | match parse_transaction(&res) | 66 | 5 | .map_err(|err| LedgerErr::from_parse(err, index + 2))?0 | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 5 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 5 | Ok(()) | 80 | 6 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReader> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?; | 61 | | | 62 | 0 | for (index, line) in reader.lines().enumerate() { | 63 | 0 | let res = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<std::io::cursor::Cursor<&str>> Line | Count | Source | 60 | 3 | validate_header(&mut reader)?1 ; | 61 | | | 62 | 12 | for (index, line) in reader.lines().enumerate()2 { | 63 | 12 | let res = line.map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 64 | 12 | if !res.trim().is_empty() { | 65 | 8 | match parse_transaction(&res) | 66 | 8 | .map_err(|err| LedgerErr::from_parse(err, index + 2))?1 | 67 | | { | 68 | 1 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 1 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 2 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 2 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 1 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 1 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 4 | } | 77 | | } | 78 | | | 79 | 1 | Ok(()) | 80 | 3 | } |
<csv_ledger::ledger::Ledger>::consume_csv::<csv_ledger::ledger::ledger::TestReaderTwo> Line | Count | Source | 60 | 1 | validate_header(&mut reader)?0 ; | 61 | | | 62 | 1 | for (index, line) in reader.lines().enumerate() { | 63 | 1 | let res0 = line.map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 64 | 0 | if !res.trim().is_empty() { | 65 | 0 | match parse_transaction(&res) | 66 | 0 | .map_err(|err| LedgerErr::from_parse(err, index + 2))? | 67 | | { | 68 | 0 | Transaction::Withdrawal(id, tx, amount) => { | 69 | 0 | self.insert_transaction(id, tx, -amount) // Negative amounts for withdrawals | 70 | | } | 71 | 0 | Transaction::Deposit(id, tx, amount) => self.insert_transaction(id, tx, amount), | 72 | 0 | Transaction::Dispute(id, tx) => self.hold(id, tx), | 73 | 0 | Transaction::Resolve(id, tx) => self.resolve(id, tx), | 74 | 0 | Transaction::Chargeback(id, tx) => self.chageback(id, tx), | 75 | | } | 76 | 0 | } | 77 | | } | 78 | | | 79 | 0 | Ok(()) | 80 | 1 | } |
|
81 | | |
82 | | /// Insert a new transaction |
83 | | /// |
84 | | /// Example: |
85 | | /// ```rust |
86 | | /// const ledger = Ledger::default(); |
87 | | /// |
88 | | /// // Deposit |
89 | | /// ledger.insert_transaction(1,1,10.0); |
90 | | /// |
91 | | /// // Withdrawal |
92 | | /// ledger.insert_transaction(1,2,-10.0); |
93 | | /// ``` |
94 | | pub fn insert_transaction(&mut self, client_id: u16, transaction_id: u32, amount: i64) { |
95 | 15 | if let Some(client3 ) = self.clients.get_mut(&client_id) { |
96 | 3 | if !client.locked { |
97 | 2 | client.total += amount; |
98 | 2 | client.available += amount; |
99 | 2 | self.transactions.insert(transaction_id, amount); |
100 | 2 | }1 |
101 | 12 | } else { |
102 | 12 | self.clients.insert(client_id, ClientData::new(amount)); |
103 | 12 | self.transactions.insert(transaction_id, amount); |
104 | 12 | } |
105 | 15 | } |
106 | | |
107 | | /// Opens a dispute on a transaction. |
108 | | pub fn hold(&mut self, client_id: u16, transaction_id: u32) { |
109 | | // Discard any incorrect inputs |
110 | 7 | if let (Some(amount), Some(client5 )) = ( |
111 | 7 | self.transactions.get(&transaction_id), |
112 | 7 | self.clients.get_mut(&client_id), |
113 | 7 | ) { |
114 | 5 | client.available -= amount; |
115 | 5 | client.held.insert(transaction_id, *amount); |
116 | 5 | }2 |
117 | 7 | } |
118 | | |
119 | | /// Resolves a disputed transaction - adds disputed transaction's value back to the available funds. |
120 | | pub fn resolve(&mut self, client_id: u16, transaction_id: u32) { |
121 | | // Discard any incorrect inputs |
122 | 4 | if let Some(client3 ) = self.clients.get_mut(&client_id) && |
123 | 3 | let Some(amount2 ) = client.held.remove(&transaction_id) |
124 | 2 | { |
125 | 2 | client.available += amount; |
126 | 2 | } |
127 | 4 | } |
128 | | |
129 | | /// Peform a chargeback on a disputed transaction - |
130 | | pub fn chageback(&mut self, client_id: u16, transaction_id: u32) { |
131 | | // Discard any incorrect inputs |
132 | 4 | if let Some(client3 ) = self.clients.get_mut(&client_id) && |
133 | 3 | let Some(amount2 ) = client.held.remove(&transaction_id) |
134 | 2 | { |
135 | 2 | client.total -= amount; |
136 | 2 | client.locked = true; |
137 | 2 | } |
138 | 4 | } |
139 | | } |
140 | | |
141 | | impl Display for Ledger { |
142 | 7 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
143 | 7 | write!( |
144 | 7 | f, |
145 | 7 | "client, available, held, total, locked{}", |
146 | 7 | self.clients |
147 | 7 | .iter() |
148 | 8 | .fold(String::new(), |acc, (key, value)| format!( |
149 | 8 | "{acc}\n{key}, {value}" |
150 | 8 | )) |
151 | 7 | ) |
152 | 7 | } |
153 | | } |
154 | | |
155 | | /// Validate the header of the csv file. |
156 | 17 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> |
157 | 17 | where |
158 | 17 | T: Read, |
159 | 17 | { |
160 | 17 | let mut buf = String::new(); |
161 | 17 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?2 ; // map_err is used to provide better debug info |
162 | 15 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1)6 )?6 ; csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&str>>::{closure#0}Line | Count | Source | 162 | 4 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReader>::{closure#0}csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&[u8; 1usize]>>::{closure#0}Line | Count | Source | 162 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReaderTwo>::{closure#0}csv_ledger::ledger::validate_header::<std::fs::File>::{closure#0}Line | Count | Source | 162 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; |
Unexecuted instantiation: csv_ledger::ledger::validate_header::<csv_ledger::ledger::validate_header::TestReader>::{closure#0} |
163 | 9 | Ok(()) |
164 | 17 | } csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReader> Line | Count | Source | 156 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 1 | where | 158 | 1 | T: Read, | 159 | 1 | { | 160 | 1 | let mut buf = String::new(); | 161 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 162 | 0 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 163 | 0 | Ok(()) | 164 | 1 | } |
csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&str>> Line | Count | Source | 156 | 7 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 7 | where | 158 | 7 | T: Read, | 159 | 7 | { | 160 | 7 | let mut buf = String::new(); | 161 | 7 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 162 | 7 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?4 ; | 163 | 3 | Ok(()) | 164 | 7 | } |
csv_ledger::ledger::validate_header::<csv_ledger::ledger::validate_header::TestReader> Line | Count | Source | 156 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 1 | where | 158 | 1 | T: Read, | 159 | 1 | { | 160 | 1 | let mut buf = String::new(); | 161 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?; // map_err is used to provide better debug info | 162 | 0 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 163 | 0 | Ok(()) | 164 | 1 | } |
csv_ledger::ledger::validate_header::<std::fs::File> Line | Count | Source | 156 | 6 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 6 | where | 158 | 6 | T: Read, | 159 | 6 | { | 160 | 6 | let mut buf = String::new(); | 161 | 6 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 162 | 6 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?1 ; | 163 | 5 | Ok(()) | 164 | 6 | } |
csv_ledger::ledger::validate_header::<std::io::cursor::Cursor<&[u8; 1usize]>> Line | Count | Source | 156 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 1 | where | 158 | 1 | T: Read, | 159 | 1 | { | 160 | 1 | let mut buf = String::new(); | 161 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 162 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?; | 163 | 0 | Ok(()) | 164 | 1 | } |
csv_ledger::ledger::validate_header::<csv_ledger::ledger::ledger::TestReaderTwo> Line | Count | Source | 156 | 1 | fn validate_header<T>(reader: &mut BufReader<T>) -> Result<(), LedgerErr> | 157 | 1 | where | 158 | 1 | T: Read, | 159 | 1 | { | 160 | 1 | let mut buf = String::new(); | 161 | 1 | reader.read_line(&mut buf).map_err(LedgerErr::Reading)?0 ; // map_err is used to provide better debug info | 162 | 1 | parse_header(&buf).map_err(|err| LedgerErr::Parse(err.to_string(), 1))?0 ; | 163 | 1 | Ok(()) | 164 | 1 | } |
|
165 | | |
166 | | impl ClientData { |
167 | 14 | fn new(amount: i64) -> Self { |
168 | 14 | ClientData { |
169 | 14 | held: BTreeMap::new(), |
170 | 14 | available: amount, |
171 | 14 | total: amount, |
172 | 14 | locked: false, |
173 | 14 | } |
174 | 14 | } |
175 | | } |
176 | | |
177 | | impl Display for ClientData { |
178 | 8 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
179 | 8 | write!( |
180 | 8 | f, |
181 | 8 | "{}, {}, {}, {}", |
182 | 8 | dp_string(self.available), |
183 | 8 | dp_string(self.held.values().sum()), |
184 | 8 | dp_string(self.total), |
185 | 8 | self.locked |
186 | 8 | ) |
187 | 8 | } |
188 | | } |
189 | | |
190 | | /// Convert a i64 to a string with four decimal places (eg val / 100) |
191 | 30 | fn dp_string(amount: i64) -> String { |
192 | 30 | format!("{}.{:04}", amount / 10000, amount % 10000) |
193 | 30 | } |
194 | | |
195 | | #[cfg(test)] |
196 | | mod dp_string { |
197 | | use super::dp_string; |
198 | 1 | #[test] |
199 | 1 | fn test_dp_string() { |
200 | 1 | assert_eq!(dp_string(0), "0.0000"); |
201 | 1 | assert_eq!(dp_string(1), "0.0001"); |
202 | 1 | assert_eq!(dp_string(10), "0.0010"); |
203 | 1 | assert_eq!(dp_string(100), "0.0100"); |
204 | 1 | assert_eq!(dp_string(1000), "0.1000"); |
205 | 1 | assert_eq!(dp_string(10000), "1.0000"); |
206 | 1 | } |
207 | | } |
208 | | |
209 | | #[cfg(test)] |
210 | | mod validate_header { |
211 | | use super::validate_header; |
212 | | use std::io::{BufReader, Cursor, Error, ErrorKind, Read}; |
213 | | |
214 | | struct TestReader {} |
215 | | |
216 | | impl Read for TestReader { |
217 | 1 | fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> { |
218 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
219 | 1 | } |
220 | | } |
221 | | |
222 | 1 | #[test] |
223 | 1 | fn ok() { |
224 | 1 | validate_header(&mut BufReader::new(Cursor::new("type, client, tx, amount"))).unwrap(); |
225 | 1 | } |
226 | | |
227 | 1 | #[test] |
228 | 1 | fn err_runthrough() { |
229 | 1 | validate_header(&mut BufReader::new(TestReader {})).unwrap_err(); |
230 | 1 | validate_header(&mut BufReader::new(Cursor::new(""))).unwrap_err(); |
231 | 1 | validate_header(&mut BufReader::new(Cursor::new("\n"))).unwrap_err(); |
232 | 1 | validate_header(&mut BufReader::new(Cursor::new("type,"))).unwrap_err(); |
233 | 1 | } |
234 | | } |
235 | | |
236 | | #[cfg(test)] |
237 | | mod client_data { |
238 | | use super::ClientData; |
239 | | |
240 | 1 | #[test] |
241 | 1 | fn debug() { |
242 | 1 | let data = ClientData::new(10); |
243 | 1 | |
244 | 1 | assert_eq!( |
245 | 1 | format!("{:?}", data), |
246 | 1 | "ClientData { held: {}, available: 10, total: 10, locked: false }" |
247 | 1 | ); |
248 | 1 | } |
249 | | } |
250 | | |
251 | | #[cfg(test)] |
252 | | mod ledger { |
253 | | use super::{ClientData, Ledger}; |
254 | | use std::collections::BTreeMap; |
255 | | use std::io::{BufReader, Cursor, Error, ErrorKind, Read}; |
256 | | |
257 | | struct TestReader {} |
258 | | |
259 | | impl Read for TestReader { |
260 | 1 | fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> { |
261 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
262 | 1 | } |
263 | | } |
264 | | |
265 | | struct TestReaderTwo<'a> { |
266 | | inner: Cursor<&'a str>, |
267 | | state: bool, |
268 | | } |
269 | | |
270 | | // Fail after second read |
271 | | impl Read for TestReaderTwo<'_> { |
272 | 2 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { |
273 | 2 | if self.state { |
274 | 1 | Err(Error::new(ErrorKind::InvalidData, "Something went wrong.")) |
275 | | } else { |
276 | 1 | self.state = true; |
277 | 1 | Ok(self.inner.read(buf).unwrap()) |
278 | | } |
279 | 2 | } |
280 | | } |
281 | | |
282 | 1 | #[test] |
283 | 1 | fn ok_consume() { |
284 | 1 | let mut ledger = Ledger::default(); |
285 | 1 | |
286 | 1 | ledger |
287 | 1 | .consume_csv(BufReader::new(Cursor::new( |
288 | 1 | "type, client, tx, amount |
289 | 1 | |
290 | 1 | deposit, 1, 1, 20.0 |
291 | 1 | withdrawal,1,2,10.0 |
292 | 1 | dispute,1,2, |
293 | 1 | resolve,1,2, |
294 | 1 | |
295 | 1 | deposit,2,3,113.1112 |
296 | 1 | dispute,2,3, |
297 | 1 | chargeback,2,3, |
298 | 1 | |
299 | 1 | ", |
300 | 1 | ))) |
301 | 1 | .unwrap(); |
302 | 1 | |
303 | 1 | let result = ledger.to_string(); |
304 | 1 | let mut lines = result.lines(); |
305 | 1 | |
306 | 1 | assert_eq!( |
307 | 1 | lines.next().unwrap(), |
308 | 1 | "client, available, held, total, locked" |
309 | 1 | ); |
310 | | |
311 | 1 | let accounts = vec![ |
312 | 1 | "1, 10.0000, 0.0000, 10.0000, false", |
313 | 1 | "2, 0.0000, 0.0000, 0.0000, true", |
314 | 1 | ]; |
315 | 1 | |
316 | 1 | assert!(accounts.contains(&lines.next().unwrap())); |
317 | 1 | assert!(accounts.contains(&lines.next().unwrap())); |
318 | 1 | assert!(lines.next().is_none()) |
319 | 1 | } |
320 | | |
321 | 1 | #[test] |
322 | 1 | fn err_consume_runthrough() { |
323 | 1 | let mut ledger = Ledger::default(); |
324 | 1 | |
325 | 1 | ledger |
326 | 1 | .consume_csv(BufReader::new(Cursor::new(""))) |
327 | 1 | .unwrap_err(); |
328 | 1 | |
329 | 1 | ledger |
330 | 1 | .consume_csv(BufReader::new(Cursor::new(&[0x0]))) |
331 | 1 | .unwrap_err(); |
332 | 1 | |
333 | 1 | ledger |
334 | 1 | .consume_csv(BufReader::new(TestReader {})) |
335 | 1 | .unwrap_err(); |
336 | 1 | |
337 | 1 | ledger |
338 | 1 | .consume_csv(BufReader::new(TestReaderTwo { |
339 | 1 | inner: Cursor::new("type, client, tx, amount\n"), |
340 | 1 | state: false, |
341 | 1 | })) |
342 | 1 | .unwrap_err(); |
343 | 1 | |
344 | 1 | ledger |
345 | 1 | .consume_csv(BufReader::new(Cursor::new("type, client, tx, amount\n123"))) |
346 | 1 | .unwrap_err(); |
347 | 1 | } |
348 | | |
349 | 1 | #[test] |
350 | 1 | fn insert_transaction() { |
351 | 1 | let mut client_2 = ClientData::new(0); |
352 | 1 | client_2.locked = true; |
353 | 1 | |
354 | 1 | let mut ledger = Ledger { |
355 | 1 | clients: [(2_u16, client_2)].into_iter().collect(), |
356 | 1 | transactions: BTreeMap::new(), |
357 | 1 | }; |
358 | 1 | |
359 | 1 | ledger.insert_transaction(1, 1, 1); |
360 | 1 | ledger.insert_transaction(1, 2, 1); |
361 | 1 | |
362 | 1 | // Locked |
363 | 1 | ledger.insert_transaction(2, 3, 1); |
364 | 1 | |
365 | 1 | let client_1 = ledger.clients.get(&1).unwrap(); |
366 | 1 | let client_2 = ledger.clients.get(&2).unwrap(); |
367 | 1 | assert_eq!(client_1.available, 2); |
368 | 1 | assert_eq!(client_2.available, 0); |
369 | 1 | assert_eq!(client_1.total, 2); |
370 | 1 | assert_eq!(client_2.total, 0); |
371 | 1 | } |
372 | | |
373 | 1 | #[test] |
374 | 1 | fn dispute() { |
375 | 1 | let mut ledger = Ledger::default(); |
376 | 1 | |
377 | 1 | ledger.insert_transaction(1, 1, 1); |
378 | 1 | ledger.hold(1, 1); |
379 | 1 | ledger.hold(2, 1); |
380 | 1 | ledger.hold(1, 2); |
381 | 1 | |
382 | 1 | let c = ledger.clients.get(&1).unwrap(); |
383 | 1 | |
384 | 1 | assert_eq!(ledger.clients.len(), 1); |
385 | 1 | assert_eq!(c.held.get(&1).unwrap(), &1_i64); |
386 | 1 | assert_eq!(c.available, 0_i64); |
387 | 1 | } |
388 | | |
389 | 1 | #[test] |
390 | 1 | fn resolve() { |
391 | 1 | let mut ledger = Ledger::default(); |
392 | 1 | |
393 | 1 | ledger.insert_transaction(1, 1, 1); |
394 | 1 | ledger.hold(1, 1); |
395 | 1 | ledger.resolve(1, 1); |
396 | 1 | ledger.resolve(2, 1); |
397 | 1 | ledger.resolve(1, 2); |
398 | 1 | |
399 | 1 | let c = ledger.clients.get(&1).unwrap(); |
400 | 1 | assert_eq!(c.held.len(), 0); |
401 | 1 | assert_eq!(c.available, 1_i64); |
402 | 1 | } |
403 | | |
404 | 1 | #[test] |
405 | 1 | fn chargeback() { |
406 | 1 | let mut ledger = Ledger::default(); |
407 | 1 | |
408 | 1 | ledger.insert_transaction(1, 1, 1); |
409 | 1 | ledger.hold(1, 1); |
410 | 1 | ledger.chageback(1, 1); |
411 | 1 | ledger.chageback(2, 1); |
412 | 1 | ledger.chageback(1, 2); |
413 | 1 | |
414 | 1 | let c = ledger.clients.get(&1).unwrap(); |
415 | 1 | assert_eq!(c.held.len(), 0); |
416 | 1 | assert_eq!(c.total, 0_i64); |
417 | 1 | assert_eq!(c.locked, true); |
418 | 1 | } |
419 | | |
420 | 1 | #[test] |
421 | 1 | fn debug() { |
422 | 1 | assert_eq!( |
423 | 1 | format!("{:?}", Ledger::default()), |
424 | 1 | "Ledger { clients: {}, transactions: {} }" |
425 | 1 | ) |
426 | 1 | } |
427 | | |
428 | 1 | #[test] |
429 | 1 | fn display() { |
430 | 1 | let mut ledger = Ledger::default(); |
431 | 1 | ledger.insert_transaction(1, 1, 1); |
432 | 1 | assert_eq!( |
433 | 1 | format!("{}", ledger), |
434 | 1 | "client, available, held, total, locked\n1, 0.0001, 0.0000, 0.0001, false" |
435 | 1 | ); |
436 | 1 | } |
437 | | } |