check_config/
cli.rs

1use std::io::Write;
2use std::process::ExitCode;
3
4use clap::Parser;
5
6use crate::checkers::{
7    base::{Action, Check},
8    RelativeUrl,
9};
10
11use super::checkers::read_checks_from_path;
12
13#[derive(Copy, Clone)]
14pub enum ExitStatus {
15    /// Linting was successful and there were no linting errors.
16    Success,
17    /// Linting was successful but there were linting errors.
18    Failure,
19    /// Linting failed.
20    Error,
21}
22
23impl From<ExitStatus> for ExitCode {
24    fn from(status: ExitStatus) -> Self {
25        match status {
26            ExitStatus::Success => ExitCode::from(0),
27            ExitStatus::Failure => ExitCode::from(1),
28            ExitStatus::Error => ExitCode::from(2),
29        }
30    }
31}
32
33/// Config Checker will check and optional fix your config files based on checkers defined in a toml file.
34/// It can check toml, yaml, json and plain text files.
35#[derive(Parser)]
36#[command(author, version, about, long_about = None)]
37struct Cli {
38    /// Path or URL to the root checkers file in toml format
39    /// Defaults (in order of precedence):
40    /// - check-config.toml
41    /// - pyproject.toml with a tool.check-config key
42    #[arg(short, long, env = "CHECK_CONFIG_PATH", verbatim_doc_comment)]
43    path: Option<String>,
44
45    /// Try to fix the config
46    #[arg(long, default_value = "false")]
47    fix: bool,
48
49    /// List all checks. Checks are not executed.
50    #[arg(short, long, default_value = "false")]
51    list_checkers: bool,
52
53    /// Execute the checkers with one of the specified tags
54    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ANY_TAGS")]
55    any_tags: Vec<String>,
56
57    /// Execute the checkers with all of the specified tags
58    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ALL_TAGS")]
59    all_tags: Vec<String>,
60
61    /// Do not execute the checkers with any of the specified tags.
62    #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_SKIP_TAGS")]
63    skip_tags: Vec<String>,
64
65    /// Create missing directories
66    #[arg(short, long, default_value = "false", env = "CHECK_CONFIG_CREATE_DIRS")]
67    create_missing_directories: bool,
68
69    // -v s
70    // -vv show all
71    #[clap(flatten)]
72    verbose: clap_verbosity_flag::Verbosity,
73}
74
75pub(crate) fn filter_checks(
76    checker_tags: &[String],
77    any_tags: &[String],
78    all_tags: &[String],
79    skip_tags: &[String],
80) -> bool {
81    // At least one must match
82    if !any_tags.is_empty() && !any_tags.iter().any(|t| checker_tags.contains(t)) {
83        return false;
84    }
85
86    // All must match
87    if !all_tags.is_empty() && !all_tags.iter().all(|t| checker_tags.contains(t)) {
88        return false;
89    }
90
91    // None must match
92    if !skip_tags.is_empty() && skip_tags.iter().any(|t| checker_tags.contains(t)) {
93        return false;
94    }
95
96    true
97}
98
99pub(crate) fn parse_path_str_to_uri(path: &str) -> Option<url::Url> {
100    if path.starts_with("/") {
101        super::uri::parse_uri(format!("file://{path}").as_str(), None).ok()
102    } else {
103        let cwd = super::uri::parse_uri(
104            &format!(
105                "file://{}/",
106                std::env::current_dir().unwrap().as_path().to_str().unwrap()
107            ),
108            None,
109        )
110        .unwrap();
111        super::uri::parse_uri(path, Some(&cwd)).ok()
112    }
113}
114pub fn cli() -> ExitCode {
115    let cli = Cli::parse();
116    env_logger::Builder::new()
117        .filter_level(cli.verbose.log_level_filter())
118        .format(|buf, record| writeln!(buf, "{}", record.args()))
119        .init();
120
121    log::info!("Starting check-config");
122
123    let mut checks = match cli.path {
124        Some(path_str) => match parse_path_str_to_uri(path_str.as_str()) {
125            Some(uri) => {
126                log::info!("Using checkers from {}", &uri.short_url_str());
127                read_checks_from_path(&uri, vec![])
128            }
129            None => {
130                log::error!(
131                    "Unable to load checkers. Path ({path_str}) specified is not a valid path.",
132                );
133                return ExitCode::from(ExitStatus::Error);
134            }
135        },
136        None => {
137            log::warn!("⚠️ No path specified. Trying check-config.toml");
138            let uri = parse_path_str_to_uri("check-config.toml").expect("valid path");
139            match std::path::Path::new(uri.path()).exists() {
140                true => {
141                    log::info!("Using checkers from {}", &uri);
142                    read_checks_from_path(&uri, vec![])
143                }
144                false => {
145                    log::warn!("check-config.toml does not exists.");
146                    log::warn!("Trying pyproject.toml.");
147                    let uri = parse_path_str_to_uri("pyproject.toml").expect("valid path");
148                    match std::path::Path::new(uri.path()).exists() {
149                        true => {
150                            log::info!("Using checkers from {}", &uri);
151                            read_checks_from_path(&uri, vec!["tool", "check-config"])
152                        }
153                        false => {
154                            log::error!("⚠️ No path specified and default paths are not found, so we ran out of options to load the config");
155                            return ExitCode::from(ExitStatus::Error);
156                        }
157                    }
158                }
159            }
160        }
161    };
162
163    log::info!("Fix: {}", &cli.fix);
164
165    if cli.list_checkers {
166        log::error!("List of checks (type, location of definition, file to check, tags)");
167        checks.iter().for_each(|check| {
168            let enabled = filter_checks(
169                &check.generic_check().tags,
170                &cli.any_tags,
171                &cli.all_tags,
172                &cli.skip_tags,
173            );
174
175            log::error!(
176                "{} {} - {} - {} - {:?}",
177                if enabled { "⬜" } else { " ✖️" },
178                check.generic_check().file_with_checks.short_url_str(),
179                check
180                    .generic_check()
181                    .file_to_check
182                    .as_os_str()
183                    .to_string_lossy(),
184                check.check_type(),
185                check.generic_check().tags
186            )
187        });
188        return ExitCode::from(ExitStatus::Success);
189    }
190
191    // log::info!(
192    //     "☰ Restricting checkers which have __tags__ which all are part of: {:?}",
193    //     restricted_tags,
194    // );
195
196    checks.retain(|check| {
197        filter_checks(
198            &check.generic_check().tags,
199            &cli.any_tags,
200            &cli.all_tags,
201            &cli.skip_tags,
202        )
203    });
204
205    dbg!(checks.len());
206
207    let (action_count, success_count) =
208        run_checks(&checks, cli.fix, cli.create_missing_directories);
209
210    log::warn!("{success_count} checks successful.");
211    if action_count > 0 {
212        // note: error level is used to always show this message, also with the lowest verbose level
213        if action_count == 1 {
214            log::error!("🪛  There is 1 violation to fix.",);
215        } else {
216            log::error!("🪛  There are {action_count} violations to fix.",);
217        }
218        ExitCode::from(ExitStatus::Failure)
219    } else {
220        // note: error level is used to always show this message, also with the lowest verbose level
221        log::error!("🥇 No violations found.");
222        ExitCode::from(ExitStatus::Success)
223    }
224}
225
226pub(crate) fn run_checks(
227    checks: &Vec<Box<dyn Check>>,
228    fix: bool,
229    create_missing_directories: bool,
230) -> (i32, i32) {
231    let mut action_count = 0;
232    let mut success_count = 0;
233
234    for check in checks {
235        let result = if fix {
236            check.fix(create_missing_directories)
237        } else {
238            check.check()
239        };
240        match result {
241            Err(_) => {
242                log::error!("⚠  There was an error fixing files.");
243                return (0, 0);
244            }
245            Ok(Action::None) => success_count += 1,
246            Ok(_action) => {
247                action_count += 1;
248            }
249        };
250    }
251
252    (action_count, success_count)
253}