Coverage Report

Created: 2022-08-31 14:14

/Users/oliverbrotchie/Documents/GitHub/csv_ledger/src/main.rs
Line
Count
Source (jump to first uncovered line)
1
1
#![feature(int_log)]#![feature(int_log)]
2
//! # `csv_ledger`
3
//!  Consumes a CSV containing a list of transactions and produces a set of client account statements.
4
//!
5
//! ## Installation
6
//!
7
//! ```sh
8
//! cargo install csv_ledger
9
//! ```
10
//!
11
//! ## Usage
12
//!
13
//! **Print output to console:**
14
//! ```sh
15
//! csv_ledger foo.csv
16
//! ```
17
//!
18
//! **Save output to file:**
19
//! ```sh
20
//! csv_ledger --output out.csv foo.csv
21
//! ```
22
//!
23
//! **To see help infomation:**
24
//! ```sh
25
//! csv_ledger --help
26
//! ```
27
//!
28
pub mod ledger;
29
pub mod parse;
30
31
use clap::Parser;
32
use core::fmt;
33
use ledger::Ledger;
34
use nom::Err as NomErr;
35
use std::{
36
    env,
37
    fmt::Display,
38
    fs::{self, File},
39
    io::{self, BufReader},
40
    path::PathBuf,
41
    process::ExitCode,
42
};
43
44
1
#[derive(Debug)]
45
pub enum LedgerErr {
46
    Opening(io::Error),
47
    Reading(io::Error),
48
    Saving(io::Error),
49
    Parse(String, usize),
50
}
51
52
impl LedgerErr {
53
4
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
54
4
        LedgerErr::Parse(
55
4
            match err {
56
1
                NomErr::Incomplete(_) => "Input was incomplete",
57
2
                NomErr::Error(_) => "Input was in the wrong format",
58
1
                NomErr::Failure(_) => "Faliure whilst parsing input",
59
            }
60
4
            .to_string(),
61
4
            index,
62
4
        )
63
4
    }
<csv_ledger::LedgerErr>::from_parse::<nom::error::Error<&str>>
Line
Count
Source
53
1
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
54
1
        LedgerErr::Parse(
55
1
            match err {
56
0
                NomErr::Incomplete(_) => "Input was incomplete",
57
1
                NomErr::Error(_) => "Input was in the wrong format",
58
0
                NomErr::Failure(_) => "Faliure whilst parsing input",
59
            }
60
1
            .to_string(),
61
1
            index,
62
1
        )
63
1
    }
<csv_ledger::LedgerErr>::from_parse::<(&str, nom::error::ErrorKind)>
Line
Count
Source
53
2
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
54
2
        LedgerErr::Parse(
55
2
            match err {
56
0
                NomErr::Incomplete(_) => "Input was incomplete",
57
1
                NomErr::Error(_) => "Input was in the wrong format",
58
1
                NomErr::Failure(_) => "Faliure whilst parsing input",
59
            }
60
2
            .to_string(),
61
2
            index,
62
2
        )
63
2
    }
<csv_ledger::LedgerErr>::from_parse::<nom::internal::Needed>
Line
Count
Source
53
1
    fn from_parse<E>(err: NomErr<E>, index: usize) -> LedgerErr {
54
1
        LedgerErr::Parse(
55
1
            match err {
56
1
                NomErr::Incomplete(_) => "Input was incomplete",
57
0
                NomErr::Error(_) => "Input was in the wrong format",
58
0
                NomErr::Failure(_) => "Faliure whilst parsing input",
59
            }
60
1
            .to_string(),
61
1
            index,
62
1
        )
63
1
    }
64
}
65
66
impl Display for LedgerErr {
67
8
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68
8
        let (
msg, e4
) = match self {
69
2
            LedgerErr::Opening(e) => ("opening the csv", e),
70
1
            LedgerErr::Reading(e) => ("reading in the csv", e),
71
1
            LedgerErr::Saving(e) => ("saving the output file", e),
72
4
            LedgerErr::Parse(e, index) => {
73
4
                return write!(
74
4
                    f,
75
4
                    "Ledger Error 🦀 - Issue whilst parsing csv: \"{}\", At line: {index}",
76
4
                    e
77
4
                )
78
            }
79
        };
80
81
4
        write!(f, "Ledger Error 🦀 - Issue whilst {msg}: {}", e)
82
8
    }
83
}
84
85
4
#[derive(Parser, 
Debug1
)]
Unexecuted instantiation: <csv_ledger::Args as clap::derive::CommandFactory>::into_app_for_update
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#3}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches
<csv_ledger::Args as clap::derive::Args>::augment_args
Line
Count
Source
85
4
#[derive(Parser, Debug)]
<csv_ledger::Args as clap::derive::CommandFactory>::into_app
Line
Count
Source
85
2
#[derive(Parser, Debug)]
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#3}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut
86
#[clap(author, version, about)]
87
struct Args {
88
    /// The path to the input CSV File.
89
0
    path: PathBuf,
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#2}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#2}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#0}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#2}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#2}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#0}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#1}
90
91
    #[clap(short = 'o', long = "output")]
92
    /// A path to save the output a a file. By default, the output will be printed to stdout.
93
0
    output: Option<PathBuf>,
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#4}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args_for_update::{closure#1}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::update_from_arg_matches_mut::{closure#4}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#1}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::Args>::augment_args::{closure#1}::{closure#0}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#4}
Unexecuted instantiation: <csv_ledger::Args as clap::derive::FromArgMatches>::from_arg_matches_mut::{closure#4}::{closure#0}
94
}
95
96
impl Args {
97
    /// Parse cli args or read mocked test enviroment variables.
98
5
    fn parse_input() -> Result<Args, clap::Error> {
99
5
        if cfg!(feature = "test_args") && env::var("CSV_LEDGER_TEST_ARGS").is_ok() {
100
4
            match env::var("CSV_LEDGER_PATH") {
101
3
                Ok(p) => Ok(Args {
102
3
                    path: p.into(),
103
3
                    output: env::var("CSV_LEDGER_OUTPUT").ok().map(|s| 
s.into()1
),
104
3
                }),
105
1
                Err(_) => Err(clap::Error::with_description(
106
1
                    "CSV_LEDGER_PATH environment variable not set.".to_string(),
107
1
                    clap::ErrorKind::MissingRequiredArgument,
108
1
                )),
109
            }
110
        } else {
111
1
            Args::try_parse()
112
        }
113
5
    }
114
}
115
116
5
fn main() -> ExitCode {
117
5
    let 
args3
= match Args::parse_input() {
118
3
        Ok(args) => args,
119
2
        Err(err) => {
120
2
            eprintln!("{err}");
121
2
            return ExitCode::FAILURE;
122
        }
123
    };
124
125
3
    if let Err(
err1
) = perform_parse_and_output(args.path, args.output) {
126
1
        eprintln!("{err}");
127
1
        return ExitCode::FAILURE;
128
2
    }
129
2
130
2
    ExitCode::SUCCESS
131
5
}
132
133
#[inline]
134
8
pub fn perform_parse_and_output(path: PathBuf, output: Option<PathBuf>) -> Result<(), LedgerErr> {
135
    // Open the csv file
136
8
    let 
file6
= File::open(path).map_err(LedgerErr::Opening)
?2
;
137
138
    // Create a new ledger and consume the csv file
139
6
    let mut ledger = Ledger::default();
140
6
    ledger.consume_csv(BufReader::new(file))
?1
;
141
142
    // Output the result
143
5
    if let Some(
output_path3
) = output {
144
3
        fs::write(output_path, ledger.to_string()).map_err(LedgerErr::Saving)
?1
;
145
2
    } else {
146
2
        println!("{}", ledger);
147
2
    }
148
149
4
    Ok(())
150
8
}
151
152
#[cfg(test)]
153
mod perform_parse_and_output {
154
    use std::{fs, path::Path};
155
    use tempfile::tempdir;
156
157
1
    #[test]
158
1
    fn ok_stdout() {
159
1
        let dir = tempdir().expect("Failed to create temporary directory");
160
1
        let path = dir.path().join("test.csv");
161
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
162
1
163
1
        fs::write(&path, input).expect("Failed to create temporary file");
164
1
165
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
166
1
        assert!(result.is_ok());
167
1
    }
168
169
1
    #[test]
170
1
    fn ok_output_file() {
171
1
        let dir = tempdir().expect("Failed to create temporary directory");
172
1
        let path = dir.path().join("test.csv");
173
1
        let output = dir.path().join("test_output.csv");
174
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
175
1
176
1
        fs::write(&path, input).expect("Unable to write file");
177
1
178
1
        let result =
179
1
            super::perform_parse_and_output(path.clone().into(), Some(output.clone().into()));
180
1
181
1
        result.unwrap();
182
1
        assert!(Path::new(&output).is_file());
183
1
    }
184
185
1
    #[test]
186
1
    fn err_read_file() {
187
1
        let dir = tempdir().expect("Failed to create temporary directory");
188
1
        let path = dir.path().join("/foo/test.csv");
189
1
190
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
191
1
        assert!(result.is_err());
192
1
    }
193
194
1
    #[test]
195
1
    fn err_consume() {
196
1
        let dir = tempdir().expect("Failed to create temporary directory");
197
1
        let path = dir.path().join("test.csv");
198
1
        let input = "";
199
1
200
1
        fs::write(&path, input).expect("Failed to create temporary file");
201
1
202
1
        let result = super::perform_parse_and_output(path.clone().into(), None);
203
1
        assert!(result.is_err());
204
1
    }
205
206
1
    #[test]
207
1
    fn err_output_file() {
208
1
        let dir = tempdir().expect("Failed to create temporary directory");
209
1
        let path = dir.path().join("test.csv");
210
1
        let output = dir.path().join("/example/test_output.csv");
211
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
212
1
213
1
        fs::write(&path, input).expect("Unable to write file");
214
1
215
1
        let result =
216
1
            super::perform_parse_and_output(path.clone().into(), Some(output.clone().into()));
217
1
        assert!(result.is_err());
218
1
    }
219
}
220
221
#[cfg(test)]
222
mod args {
223
    use super::Args;
224
    use clap::Parser;
225
226
1
    #[test]
227
1
    fn debug() {
228
1
        let args = Args {
229
1
            path: "./tests/test.csv".into(),
230
1
            output: Some("./tests/test_output.csv".into()),
231
1
        };
232
1
233
1
        assert_eq!(
234
1
            format!("{:?}", args),
235
1
            "Args { path: \"./tests/test.csv\", output: Some(\"./tests/test_output.csv\") }"
236
1
        );
237
1
    }
238
239
1
    #[test]
240
1
    fn parse_err() {
241
1
        Args::try_parse_from(["foo.csv"]).unwrap_err();
242
1
    }
243
}
244
245
#[cfg(test)]
246
mod ledger_err {
247
    use crate::LedgerErr;
248
    use nom::{error::ErrorKind, Err as NomErr, Needed};
249
250
1
    #[test]
251
1
    fn from_parse() {
252
1
        assert_eq!(
253
1
            LedgerErr::from_parse(NomErr::Incomplete::<Needed>(Needed::Unknown), 1).to_string(),
254
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Input was incomplete\", At line: 1",
255
1
        );
256
257
1
        assert_eq!(
258
1
            LedgerErr::from_parse(NomErr::Failure(("ERROR", ErrorKind::Fail)), 1).to_string(),
259
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Faliure whilst parsing input\", At line: 1",
260
1
        );
261
262
1
        assert_eq!(
263
1
            LedgerErr::from_parse(NomErr::Error(("ERROR", ErrorKind::Fail)), 1).to_string(),
264
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"Input was in the wrong format\", At line: 1",
265
1
        );
266
1
    }
267
268
1
    #[test]
269
1
    fn debug() {
270
1
        let err = super::LedgerErr::Opening(std::io::Error::new(
271
1
            std::io::ErrorKind::NotFound,
272
1
            "File not found",
273
1
        ));
274
1
        assert_eq!(
275
1
            format!("{:?}", err),
276
1
            "Opening(Custom { kind: NotFound, error: \"File not found\" })",
277
1
        );
278
1
    }
279
280
1
    #[test]
281
1
    fn display() {
282
1
        assert_eq!(
283
1
            format!(
284
1
                "{}",
285
1
                super::LedgerErr::Opening(std::io::Error::new(
286
1
                    std::io::ErrorKind::NotFound,
287
1
                    "File not found",
288
1
                ))
289
1
            ),
290
1
            "Ledger Error 🦀 - Issue whilst opening the csv: File not found",
291
1
        );
292
293
1
        assert_eq!(
294
1
            format!(
295
1
                "{}",
296
1
                super::LedgerErr::Reading(std::io::Error::new(
297
1
                    std::io::ErrorKind::NotFound,
298
1
                    "File not found",
299
1
                ))
300
1
            ),
301
1
            "Ledger Error 🦀 - Issue whilst reading in the csv: File not found",
302
1
        );
303
304
1
        assert_eq!(
305
1
            format!(
306
1
                "{}",
307
1
                super::LedgerErr::Saving(std::io::Error::new(
308
1
                    std::io::ErrorKind::NotFound,
309
1
                    "File not found",
310
1
                ))
311
1
            ),
312
1
            "Ledger Error 🦀 - Issue whilst saving the output file: File not found",
313
1
        );
314
315
1
        assert_eq!(
316
1
            format!("{}", super::LedgerErr::Parse("ERROR".into(), 1)),
317
1
            "Ledger Error 🦀 - Issue whilst parsing csv: \"ERROR\", At line: 1"
318
1
        );
319
1
    }
320
}
321
322
#[cfg(all(test, feature = "test_args"))]
323
mod main {
324
    use crate::main;
325
    use std::{env, fs};
326
    use tempfile::tempdir;
327
328
5
    fn reset_args() {
329
5
        env::remove_var("CSV_LEDGER_TEST_ARGS");
330
5
        env::remove_var("CSV_LEDGER_OUTPUT");
331
5
        env::remove_var("CSV_LEDGER_PATH");
332
5
    }
333
334
1
    #[test]
335
1
    fn ok_stdout() {
336
1
        reset_args();
337
1
        let dir = tempdir().expect("Failed to create temporary directory");
338
1
        let path = dir.path().join("test.csv");
339
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
340
1
341
1
        fs::write(&path, input).expect("Unable to write file");
342
1
343
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
344
1
        env::set_var("CSV_LEDGER_PATH", path);
345
1
        main();
346
1
    }
347
348
1
    #[test]
349
1
    fn ok_file() {
350
1
        reset_args();
351
1
352
1
        let dir = tempdir().expect("Failed to create temporary directory");
353
1
        let path = dir.path().join("test.csv");
354
1
        let output = dir.path().join("test_output.csv");
355
1
        let input = "type, client, tx, amount\ndeposit, 1, 1, 1.0";
356
1
357
1
        fs::write(&path, input).expect("Unable to write file");
358
1
359
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
360
1
        env::set_var("CSV_LEDGER_PATH", path);
361
1
        env::set_var("CSV_LEDGER_OUTPUT", output);
362
1
        main();
363
1
    }
364
365
1
    #[test]
366
1
    fn err_invalid_path() {
367
1
        reset_args();
368
1
        let dir = tempdir().expect("Failed to create temporary directory");
369
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
370
1
        env::set_var("CSV_LEDGER_PATH", dir.path().join("foo.csv"));
371
1
        main();
372
1
    }
373
374
1
    #[test]
375
1
    fn err_missing_path() {
376
1
        reset_args();
377
1
        env::set_var("CSV_LEDGER_TEST_ARGS", "true");
378
1
        main();
379
1
    }
380
381
1
    #[test]
382
1
    fn err_default_args() {
383
1
        reset_args();
384
1
        main();
385
1
    }
386
}