todo_ci/
cli.rs

1use std::path::PathBuf;
2
3use chrono::FixedOffset;
4use clap::{builder::TypedValueParser, Parser, ValueEnum, error::ErrorKind};
5use grep::{
6    matcher::{Captures, Matcher},
7    regex::RegexMatcher,
8};
9
10/// todo-ci: A simple ci tool to check overdue todos
11#[derive(Parser, Debug)]
12#[command(author, version, about, long_about = None)]
13pub struct Args {
14    /// For disabling ignored files by default (.gitignore, hidden files, etc.)
15    #[arg(short = 'n', long = "no-ignore")]
16    pub no_ignore: bool,
17
18    /// For disabling returning system error code (1) if there are overdue todos
19    #[arg(short = 'e', long = "no-error")]
20    pub no_error: bool,
21
22    /// Display mode:
23    ///{n}
24    ///- concise: total number of valid + overdue todos {n}
25    ///- overdue-only: total number of valid + overdue todos + details of overdue todos {n}
26    ///- default: total number of valid + overdue todos + details of all todos {n}
27    #[arg(
28        value_enum,
29        rename_all = "kebab_case",
30        short = 'd',
31        long = "display-mode",
32        default_value = "default"
33    )]
34    pub display_mode: DisplayMode,
35
36    /// Root directory to check `todos` for
37    #[arg(value_parser, default_value = "./")]
38    pub root_directory: PathBuf,
39
40    /// Pattern to check `todos` for (i.e. `*.rs` , `main.*`, etc.)
41    #[arg(short = 'p', long = "pattern", value_parser, default_value = "*")]
42    pub ignore_pattern: String,
43    /// Timezone to use for date checking
44    #[arg(short = 't', long = "timezone-offset", value_parser = FixedOffsetParser, default_value = "+00:00", allow_hyphen_values = true)]
45    pub timezone_offset: FixedOffset,
46}
47
48#[derive(ValueEnum, Debug, Clone)] 
49pub enum DisplayMode {
50    Concise,
51    OverdueOnly,
52    Default,
53    // Verbose,
54}
55
56#[derive(Clone)]
57struct FixedOffsetParser;
58
59impl TypedValueParser for FixedOffsetParser {
60    type Value = FixedOffset;
61
62    fn parse_ref(
63        &self,
64        _cmd: &clap::Command,
65        _arg: Option<&clap::Arg>,
66        value: &std::ffi::OsStr,
67    ) -> Result<FixedOffset, clap::Error> {
68        let offset_string = value.to_str().expect("Should be string!");
69        const OFFSET_PATTERN: &str = r"(-|\+)(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])";
70
71        let matcher = RegexMatcher::new(OFFSET_PATTERN).expect("Regex should be valid");
72        let mut captures = matcher.new_captures().expect("Regex should be valid");
73        matcher
74            .captures(offset_string.as_bytes(), &mut captures)
75            .expect("Regex should be valid");
76
77        // Regex group match validation
78        if matcher.capture_count() != 4 {
79            Err(clap::Error::raw(
80                ErrorKind::ValueValidation,
81                "UTC offset does not follow the format [+|-][HH]:[SS]",
82            ))
83        } else {
84            // Unwraps here are ok - we validated the dates are integers already in the regex
85            let offset_seconds: i32 = (3600
86                * offset_string[captures.get(2).unwrap()]
87                    .parse::<i32>()
88                    .unwrap())
89                + (60
90                    * offset_string[captures.get(3).unwrap()]
91                        .parse::<i32>()
92                        .unwrap());
93
94            if &offset_string[captures.get(1).unwrap()] == "+" {
95                Ok(FixedOffset::east(offset_seconds))
96            } else {
97                Ok(FixedOffset::west(offset_seconds))
98            }
99        }
100    }
101}