Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,13 +68,15 @@ impl Args {

#[derive(Debug)]
pub enum Error {
FailedBuildingProject,
Io,
ValidationFailed,
}

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"),
}
Expand DownExpand Up@@ -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)?,
Expand Down
11 changes: 10 additions & 1 deletion src/project.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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};
Expand All@@ -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,
Expand DownExpand Up@@ -115,13 +118,15 @@ mod deserializers {
pub enum Error {
Io,
SerdeYaml,
ProjectInvalid,
}

impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io => fmt.write_str("Error::Io"),
Error::SerdeYaml => fmt.write_str("Error::SerdeYaml"),
Error::ProjectInvalid => fmt.write_str("Error::ProjectInvalid"),
}
}
}
Expand DownExpand Up@@ -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,
Expand Down
99 changes: 99 additions & 0 deletions src/project/validator.rs
Original file line numberDiff line numberDiff line change
@@ -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<Error> = 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<Error> {
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<Error> {
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<Error>);

impl fmt::Display for Errors {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Error")
}
}

impl Context for Errors {}