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)]
10pub struct ExpenseTrackerArgs {
13 #[clap(subcommand)]
14 pub command: Operation,
15 #[arg(short='p', long, value_parser = construct_file_path)]
17 pub records_path: Option<PathBuf>,
19}
20
21#[derive(Subcommand, Debug)]
22pub enum Operation {
23 Add(AddArgs),
25 Filter(FilterArgs),
27 List,
29 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 #[arg(short, long, value_parser = validate_amount)]
48 pub amount: f64,
49 #[arg(short, long)]
51 pub category: String,
52 #[arg(short, long)]
54 pub description: Option<String>,
55 #[arg(short, long)]
57 pub tag: Option<Vec<String>>,
58}
59
60#[derive(Args, Debug)]
61#[group(required = true)]
62pub struct FilterArgs {
63 #[arg(short, long)]
65 pub amount: Option<f64>,
66 #[arg(short, long)]
68 pub category: Option<String>,
69 #[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 #[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}