From 457a82d4e68fbd560b82a8bba42a020414bca1cf Mon Sep 17 00:00:00 2001 From: Matan Zruya Date: Sun, 2 Apr 2023 21:49:12 -0400 Subject: [PATCH] validate project before construction --- src/main.rs | 5 +- src/project.rs | 11 ++++- src/project/validator.rs | 99 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/project/validator.rs diff --git a/src/main.rs b/src/main.rs index 4e7e971..22da37a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,7 @@ impl Args { #[derive(Debug)] pub enum Error { + FailedBuildingProject, Io, ValidationFailed, } @@ -75,6 +76,7 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::FailedBuildingProject => fmt.write_str("Error::FailedBuildingProject"), Error::Io => fmt.write_str("Error::Io"), Error::ValidationFailed => fmt.write_str("Error::ValidationFailed"), } @@ -103,7 +105,8 @@ fn cli() -> Result<(), Error> { let config = serde_yaml::from_reader(config_file).into_context(Error::Io)?; - let ownership = Ownership::build(Project::build(&project_root, &codeowners_file_path, &config).change_context(Error::Io)?); + let ownership = + Ownership::build(Project::build(&project_root, &codeowners_file_path, &config).change_context(Error::FailedBuildingProject)?); match args.command { Command::Validate => ownership.validate().into_context(Error::ValidationFailed)?, diff --git a/src/project.rs b/src/project.rs index a70d324..9979f60 100644 --- a/src/project.rs +++ b/src/project.rs @@ -6,7 +6,7 @@ use std::{ path::{Path, PathBuf}, }; -use error_stack::{Context, Result}; +use error_stack::{Context, Result, ResultExt}; use jwalk::WalkDir; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; @@ -15,6 +15,9 @@ use tracing::{debug, instrument}; use crate::{config::Config, error_stack_ext::IntoContext}; use glob_match::glob_match; +mod validator; + +pub use validator::Errors as ValidatorErrors; pub struct Project { pub base_path: PathBuf, @@ -115,6 +118,7 @@ mod deserializers { pub enum Error { Io, SerdeYaml, + ProjectInvalid, } impl fmt::Display for Error { @@ -122,6 +126,7 @@ impl fmt::Display for Error { match self { Error::Io => fmt.write_str("Error::Io"), Error::SerdeYaml => fmt.write_str("Error::SerdeYaml"), + Error::ProjectInvalid => fmt.write_str("Error::ProjectInvalid"), } } } @@ -213,6 +218,10 @@ impl Project { let owned_files = owned_files(owned_file_paths); + validator::Validator::new(&owned_files, &teams, &packages) + .validate() + .change_context(Error::ProjectInvalid)?; + Ok(Project { base_path: base_path.to_owned(), files: owned_files, diff --git a/src/project/validator.rs b/src/project/validator.rs new file mode 100644 index 0000000..a2650fc --- /dev/null +++ b/src/project/validator.rs @@ -0,0 +1,99 @@ +use core::fmt; +use error_stack::{Report, Result}; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use std::{collections::HashSet, path::PathBuf}; +use tracing::debug; + +use error_stack::Context; + +use super::{Package, ProjectFile, Team}; + +pub(crate) struct Validator<'a> { + files: &'a [ProjectFile], + teams: &'a [Team], + packages: &'a [Package], +} + +impl<'a> Validator<'a> { + pub fn new(files: &'a [ProjectFile], teams: &'a [Team], packages: &'a [Package]) -> Self { + Self { files, teams, packages } + } + + pub fn validate(&self) -> Result<(), Errors> { + debug!("validating project"); + let mut errors: Vec = Vec::new(); + + let team_names: HashSet<&String> = self.teams.iter().map(|team| &team.name).collect(); + + errors.append(&mut self.invalid_team_annotation(&team_names)); + errors.append(&mut self.invalid_package_ownership(&team_names)); + + if errors.is_empty() { + return Ok(()); + } + + let mut report = Report::new(Errors(errors.clone())); + + for error in errors { + report = report.attach_printable(format!("{}", error)); + } + + Err(report) + } + + fn invalid_team_annotation(&self, team_names: &HashSet<&String>) -> Vec { + self.files + .par_iter() + .flat_map(|file| { + if let Some(owner) = &file.owner { + if !team_names.contains(owner) { + return Some(Error::InvalidTeam(owner.clone(), file.path.clone())); + } + } + + None + }) + .collect() + } + + fn invalid_package_ownership(&self, team_names: &HashSet<&String>) -> Vec { + self.packages + .iter() + .flat_map(|package| { + if !team_names.contains(&package.owner) { + Some(Error::InvalidTeam(package.owner.clone(), package.path.clone())) + } else { + None + } + }) + .collect() + } +} + +#[derive(Debug, Clone)] +pub enum Error { + InvalidTeam(String, PathBuf), +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidTeam(team, path) => { + fmt.write_str(&format!("- {} is referencing an invalid team - '{}'", path.to_string_lossy(), team)) + } + } + } +} + +impl Context for Error {} + +#[derive(Debug, Clone)] +pub struct Errors(Vec); + +impl fmt::Display for Errors { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("Error") + } +} + +impl Context for Errors {}