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 Success,
15 Failure,
17 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#[derive(Parser)]
34#[command(author, version, about, long_about = None)]
35struct Cli {
36 #[arg(short, long, env = "CHECK_CONFIG_PATH", verbatim_doc_comment)]
41 path: Option<String>,
42
43 #[arg(long, default_value = "false")]
45 fix: bool,
46
47 #[arg(short, long, default_value = "false")]
49 list_checkers: bool,
50
51 #[arg(long = "env", default_value = "false")]
53 use_env_variables: bool,
54
55 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ANY_TAGS")]
57 any_tags: Vec<String>,
58
59 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_ALL_TAGS")]
61 all_tags: Vec<String>,
62
63 #[arg(long, value_delimiter = ',', env = "CHECK_CONFIG_SKIP_TAGS")]
65 skip_tags: Vec<String>,
66
67 #[arg(short, long, default_value = "false", env = "CHECK_CONFIG_CREATE_DIRS")]
69 create_missing_directories: bool,
70
71 #[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 if !any_tags.is_empty() && !any_tags.iter().any(|t| checker_tags.contains(t)) {
85 return false;
86 }
87
88 if !all_tags.is_empty() && !all_tags.iter().all(|t| checker_tags.contains(t)) {
90 return false;
91 }
92
93 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 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}