expense_tracker/
cli.rs

1use std::error::Error;
2use std::path::PathBuf;
3
4use clap::{self, Args, Parser, Subcommand};
5
6use crate::expense::Expense;
7use crate::path::{construct_file_path, validate_file_path};
8
9#[derive(Parser, Debug)]
10// #[clap(author, version, about)]
11// #[command(about = "Does awesome things", long_about = None)]
12pub struct ExpenseTrackerArgs {
13    #[clap(subcommand)]
14    pub command: Operation,
15    /// Custom path to psv file where records should be saved.
16    #[arg(short='p', long, value_parser = construct_file_path)]
17    // , value_parser = validate_file_path
18    pub records_path: Option<PathBuf>,
19}
20
21#[derive(Subcommand, Debug)]
22pub enum Operation {
23    /// add expense record
24    Add(AddArgs),
25    /// filter expense records
26    Filter(FilterArgs),
27    /// list expense records
28    List,
29    /// Expense total
30    Total,
31}
32
33fn validate_amount(arg: &str) -> Result<f64, &'static str> {
34    let amount = arg.parse::<f64>();
35    match amount {
36        Ok(amount) => match amount > 0.0 {
37            true => Ok(amount),
38            false => Err("amount should be greater than 0.0"),
39        },
40        Err(_err) => Err("Invalid value for amount!"),
41    }
42}
43
44#[derive(Args, Debug)]
45pub struct AddArgs {
46    /// expense amount
47    #[arg(short, long, value_parser = validate_amount)]
48    pub amount: f64,
49    /// expense category
50    #[arg(short, long)]
51    pub category: String,
52    /// Description
53    #[arg(short, long)]
54    pub description: Option<String>,
55    /// tags
56    #[arg(short, long)]
57    pub tag: Option<Vec<String>>,
58}
59
60#[derive(Args, Debug)]
61#[group(required = true)]
62pub struct FilterArgs {
63    /// expense amount
64    #[arg(short, long)]
65    pub amount: Option<f64>,
66    /// expense category
67    #[arg(short, long)]
68    pub category: Option<String>,
69    /// tags
70    #[arg(short, long)]
71    pub tag: Option<Vec<String>>,
72}
73
74pub fn parse_sub_commands(args: ExpenseTrackerArgs) -> Result<(), Box<dyn Error>> {
75    match args.command {
76        Operation::Add(add_args) => {
77            let new_expense = Expense::new(
78                add_args.amount,
79                add_args.description,
80                add_args.category,
81                add_args.tag,
82            );
83            new_expense.write_expense_to_psv(args.records_path)?;
84        }
85        Operation::Filter(filter_args) => {
86            validate_file_path(&args.records_path)?;
87            Expense::filter_expenses(
88                filter_args.category,
89                filter_args.tag,
90                filter_args.amount,
91                args.records_path,
92            )?;
93        }
94        Operation::List => {
95            validate_file_path(&args.records_path)?;
96            Expense::list_expenses(args.records_path)?;
97        }
98        Operation::Total => {
99            validate_file_path(&args.records_path)?;
100            let total = Expense::expense_total(args.records_path)?;
101            println!("Total: {}", total);
102        }
103    }
104    Ok(())
105}
106
107#[cfg(test)]
108mod test {
109
110    use super::*;
111    use rstest::rstest;
112
113    // Testing validate_amount
114    #[rstest]
115    #[case("0.186", 0.186)]
116    #[case("10", 10.0)]
117    #[case("12.5", 12.5)]
118    fn test_validate_amount_valid(#[case] amount_inp: &str, #[case] amount_out: f64) {
119        assert_eq!(validate_amount(amount_inp), Ok(amount_out))
120    }
121
122    #[rstest]
123    #[case("0", "amount should be greater than 0.0")]
124    #[case("10doller", "Invalid value for amount!")]
125    fn test_validate_amount_invalid(#[case] amount_inp: &str, #[case] err_str: &str) {
126        assert_eq!(validate_amount(amount_inp), Err(err_str))
127    }
128}