check_config/
cli.rs

1use std::process::ExitCode;
2use std::{collections::HashMap, io::Write};
3
4use clap::Parser;
5
6use crate::checkers::base::Checker;
7use crate::uri::ReadablePath;
8
9use super::checkers::read_checks_from_path;
10
11#[derive(Copy, Clone, Debug, PartialEq)]
12pub enum ExitStatus {
13    /// Reading checks was successful and there are no checks to fix
14    Success,
15    /// Reading checks was successful and there are checks to fix
16    Failure,
17    /// Reading checks was failed or executing fixes was failed
18    Error,
19}
20
21impl From<ExitStatus> for ExitCode {
22    fn from(status: ExitStatus) -> Self {
23        match status {
24            ExitStatus::Success => ExitCode::from(0),
25            ExitStatus::Failure => ExitCode::from(1),
26            ExitStatus::Error => ExitCode::from(2),
27        }
28    }
29}
30
31/// Config Checker will check and optional fix your config files based on checkers defined in a toml file.
32/// It can check toml, yaml, json and plain text files.
33#[derive(Parser)]
34#[command(author, version, about, long_about = None)]
35struct Cli {
36    /// Path or URL to the root checkers file in toml format
37    /// Defaults (in order of precedence):
38    /// - check-config.toml
39    /// - pyproject.toml with a tool.check-config key
40    #[arg(short, long, env = "CHECK_CONFIG_PATH", verbatim_doc_comment)]
41    path: Option<String>,
42
43    /// Try to fix the config
44    #[arg(long, default_value = "false")]
45    fix: bool,
46
47    /// List all checks. Checks are not executed.
48    #[arg(short, long, default_value = "false")]
49    list_checkers: bool,
50
51    /// Use the env variables as variables for template
52    #[arg(long = "env", default_value = "false")]
53    use_env_variables: bool,
54
55    /// Execute the checkers with one of the specified tags
56    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ANY_TAGS")]
57    any_tags: Vec<String>,
58
59    /// Execute the checkers with all of the specified tags
60    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ALL_TAGS")]
61    all_tags: Vec<String>,
62
63    /// Do not execute the checkers with any of the specified tags.
64    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_SKIP_TAGS")]
65    skip_tags: Vec<String>,
66
67    /// Create missing directories
68    #[arg(short, long, default_value = "false", env = "CHECK_CONFIG_CREATE_DIRS")]
69    create_missing_directories: bool,
70
71    // -v s
72    // -vv show all
73    #[clap(flatten)]
74    verbose: clap_verbosity_flag::Verbosity,
75}
76
77pub(crate) fn filter_checks(
78    checker_tags: &[String],
79    any_tags: &[String],
80    all_tags: &[String],
81    skip_tags: &[String],
82) -> bool {
83    // At least one must match
84    if !any_tags.is_empty() && !any_tags.iter().any(|t| checker_tags.contains(t)) {
85        return false;
86    }
87
88    // All must match
89    if !all_tags.is_empty() && !all_tags.iter().all(|t| checker_tags.contains(t)) {
90        return false;
91    }
92
93    // None must match
94    if !skip_tags.is_empty() && skip_tags.iter().any(|t| checker_tags.contains(t)) {
95        return false;
96    }
97
98    true
99}
100
101pub fn cli() -> ExitCode {
102    let cli = Cli::parse();
103    env_logger::Builder::new()
104        .filter_level(cli.verbose.log_level_filter())
105        .format(|buf, record| writeln!(buf, "{}", record.args()))
106        .init();
107
108    log::info!("Starting check-config");
109
110    let mut variables: HashMap<String, String> = if cli.use_env_variables {
111        std::env::vars().collect()
112    } else {
113        HashMap::new()
114    };
115
116    let path_str = if let Some(path) = cli.path {
117        path
118    } else {
119        "check-config.toml".to_string()
120    };
121    let path = match ReadablePath::from_string(path_str.as_str(), None) {
122        Ok(path) => path,
123        Err(_) => {
124            log::error!(
125                "Unable to load checkers. Path ({path_str}) specified is not a valid path.",
126            );
127            return ExitCode::from(ExitStatus::Error);
128        }
129    };
130
131    let mut checks = read_checks_from_path(&path, &mut variables);
132
133    log::info!("Fix: {}", &cli.fix);
134
135    if cli.list_checkers {
136        log::error!("List of checks (type, location of definition, file to check, tags)");
137        checks.iter().for_each(|check| {
138            let enabled = filter_checks(
139                &check.generic_checker().tags,
140                &cli.any_tags,
141                &cli.all_tags,
142                &cli.skip_tags,
143            );
144
145            check.list_checker(enabled);
146        });
147        return ExitCode::from(ExitStatus::Success);
148    }
149
150    // log::info!(
151    //     "☰ Restricting checkers which have tags which all are part of: {:?}",
152    //     restricted_tags,
153    // );
154
155    checks.retain(|check| {
156        filter_checks(
157            &check.generic_checker().tags,
158            &cli.any_tags,
159            &cli.all_tags,
160            &cli.skip_tags,
161        )
162    });
163
164    ExitCode::from(run_checks(&checks, cli.fix))
165}
166
167pub(crate) fn run_checks(checks: &Vec<Box<dyn Checker>>, fix: bool) -> ExitStatus {
168    let mut fix_needed_count = 0;
169    let mut fix_executed_count = 0;
170    let mut no_fix_needed_count = 0;
171    let mut error_count = 0;
172
173    for check in checks {
174        let fix = !check.generic_checker().check_only && fix;
175        let result = check.check(fix);
176        match result {
177            crate::checkers::base::CheckResult::NoFixNeeded => no_fix_needed_count += 1,
178            crate::checkers::base::CheckResult::FixExecuted(_) => fix_executed_count += 1,
179            crate::checkers::base::CheckResult::FixNeeded(_) => fix_needed_count += 1,
180            crate::checkers::base::CheckResult::Error(_) => error_count += 1,
181        };
182    }
183
184    log::warn!("⬜ {checks} checks found", checks = checks.len());
185    if fix {
186        log::warn!("✅ {fix_executed_count} checks fixed");
187        log::warn!("✅ {no_fix_needed_count} checks did not need a fix");
188    }
189
190    match fix_needed_count {
191        0 => log::error!("🥇 No violations found."),
192        1 => log::error!("🪛 There is 1 violation to fix.",),
193        _ => log::error!("🪛 There are {fix_needed_count} violations to fix.",),
194    }
195
196    match error_count {
197        0 => (),
198
199        1 => log::error!("🚨 There was 1 error executing a fix.",),
200        _ => log::error!("🚨 There are {error_count} errors executing a fix.",),
201    }
202    if error_count > 0 {
203        ExitStatus::Error
204    } else if fix_needed_count > 0 {
205        ExitStatus::Failure
206    } else {
207        ExitStatus::Success
208    }
209}