prdoclib/commands/
check.rs

1//! Implementation of the check command. This command attempts to load a PRDoc file and checks
2//! whether it adheres to the schema or not.
3
4use crate::{
5	common::PRNumber,
6	config::PRDocConfig,
7	doc_filename::DocFileName,
8	docfile::DocFile,
9	error::{self, PRdocLibError},
10	prdoc_source::PRDocSource,
11	schema::Schema,
12	utils::{get_numbers_from_file, get_project_root},
13};
14use std::{
15	collections::HashSet,
16	path::{Path, PathBuf},
17};
18
19/// Implementation of the main [check](/prdoc::opts::CheckOpts) command of the cli.
20pub struct CheckCmd {
21	pub(crate) schema: Schema,
22}
23
24/// PRDoc are checked via a PR number or a file.
25/// - When passing a file path, it may not result in a `PRNumber`.
26/// - When passing a PRNumber, it also may not result in a file
27pub type CheckResult = (PRDocSource, bool);
28
29impl CheckCmd {
30	/// Create a new instance of the check command
31	pub fn new(schema: Schema) -> Self {
32		Self { schema }
33	}
34
35	pub(crate) fn check_numbers(
36		&self,
37		numbers: Vec<PRNumber>,
38		dir: &PathBuf,
39	) -> error::Result<HashSet<CheckResult>> {
40		log::debug!("Checking PRs: {:?}", numbers);
41
42		let res = numbers
43			.iter()
44			.map(|&number| {
45				log::debug!("Checking PR #{}", number);
46
47				let file_maybe = DocFileName::find(number, None, dir);
48
49				match file_maybe {
50					Ok(file) => {
51						log::debug!("Attempting to load file: {}", file.display());
52						let yaml = self.schema.load(&file);
53
54						match yaml {
55							Ok(_value) => {
56								log::debug!("Loading was OK");
57								(number.into(), true)
58							},
59							Err(PRdocLibError::ValidationErrors(validation)) => {
60								log::info!("errors: {:#?}", validation.errors);
61								log::info!("missing: {:#?}", validation.missing);
62								(number.into(), false)
63							},
64							Err(e) => {
65								log::error!("Loading the schema failed:");
66								log::error!("{}", e.to_string());
67								(number.into(), false)
68							},
69						}
70					},
71					Err(e) => {
72						log::error!("{}", e.to_string());
73						(number.into(), false)
74					},
75				}
76			})
77			.collect();
78
79		Ok(res)
80	}
81
82	/// Check a PRDoc based on its number in a given folder.
83	pub(crate) fn _check_number(
84		&self,
85		number: PRNumber,
86		dir: &PathBuf,
87	) -> error::Result<CheckResult> {
88		let file = DocFileName::find(number, None, dir)?;
89		Ok((file.clone().into(), self.check_file(&file).1))
90	}
91
92	/// Check a specific file given its full path.
93	/// All the other check_xxx functions are based on this one.
94	pub(crate) fn check_file(&self, file: &PathBuf) -> CheckResult {
95		log::debug!("Checking file {}", file.display());
96
97		let value = self.schema.load(&file);
98		let filename_maybe = DocFileName::try_from(file);
99		if let Ok(_value) = value {
100			if let Ok(filename) = filename_maybe {
101				(filename.into(), true)
102			} else {
103				(file.into(), false)
104			}
105		} else if let Ok(f) = filename_maybe {
106			(f.into(), false)
107		} else {
108			(file.into(), false)
109		}
110	}
111
112	/// Check all files in a given folder. The dot files (ie filenames starting with a dot) are
113	/// ignored This functions allows checking all files or only the valid ones thanks to the
114	/// `valid_only` argument.
115	pub(crate) fn check_files_in_folder(
116		self,
117		dir: &PathBuf,
118		valid_only: bool,
119	) -> error::Result<HashSet<CheckResult>> {
120		log::debug!("Checking all files in folder {}", dir.display());
121
122		let schema = self.schema.clone();
123		let files = DocFile::find(schema, dir, valid_only)?
124			.filter(|f| !f.file_name().unwrap_or_default().to_string_lossy().starts_with('.'));
125		let hs: HashSet<CheckResult> = files.map(|f| self.check_file(&f)).collect();
126		Ok(hs)
127	}
128
129	/// Check a list of PRDoc files based on:
130	///  - a `file` containing the list of PR numbers
131	///  - a base `dir` where to look for those PRDoc files
132	pub(crate) fn check_list(
133		&self,
134		file: &PathBuf,
135		dir: &PathBuf,
136	) -> error::Result<HashSet<CheckResult>> {
137		let extract_numbers = get_numbers_from_file(file)?;
138
139		let numbers: Vec<PRNumber> =
140			extract_numbers.iter().filter_map(|(_, _, n)| n.to_owned()).collect();
141
142		self.check_numbers(numbers, dir)
143	}
144
145	/// Return true if all checks were OK, false otherwise.
146	pub fn global_result(hs: HashSet<CheckResult>) -> bool {
147		for item in hs.iter() {
148			if !item.1 {
149				return false;
150			}
151		}
152
153		true
154	}
155
156	/// Run the check: considering an input directory and either a file, some numbers, of a list
157	/// file, run thru the list and check the validity of the PRDoc files.
158	/// We return a Vec instead of a HashSet because a check based on a file may not always lead
159	/// to a PR number, making the HashSet made of a bunch of (None, bool).
160	pub fn run(
161		config: &PRDocConfig,
162		schema: Option<PathBuf>,
163		dir: &PathBuf,
164		file: Option<PathBuf>,
165		numbers: Option<Vec<PRNumber>>,
166		list: Option<PathBuf>,
167	) -> crate::error::Result<HashSet<CheckResult>> {
168		log::info!("Checking directory {}", dir.display());
169		log::debug!("From dir: {}", dir.canonicalize().unwrap().display());
170
171		let repo_root = get_project_root()?;
172		log::debug!("From repo root: {}", repo_root.canonicalize().unwrap().display());
173
174		let schema_path = if let Some(schema_path) = schema {
175			schema_path
176		} else if config.schema_path().is_absolute() {
177			config.schema_path()
178		} else {
179			repo_root.join(config.schema_path())
180		};
181
182		log::info!("Using schema: {}", schema_path.canonicalize().unwrap().display());
183		let schema = Schema::new(schema_path);
184
185		let check_cmd = CheckCmd::new(schema);
186
187		match (file, numbers, list) {
188			(Some(file), None, None) => {
189				let file =
190					if file.is_relative() { Path::new(&dir).join(&file) } else { file.clone() };
191
192				let mut hs = HashSet::new();
193				let _ = hs.insert(check_cmd.check_file(&file));
194				Ok(hs)
195			},
196
197			(None, Some(numbers), None) => check_cmd.check_numbers(numbers, dir),
198			(None, None, Some(list)) => check_cmd.check_list(&list, dir),
199			(None, None, None) => check_cmd.check_files_in_folder(dir, false),
200
201			_ => unreachable!(),
202		}
203	}
204}