Skip to content

Commit

Permalink
fix: Improve validation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
gmpinder committed Dec 17, 2024
1 parent 000fc9a commit ac10b61
Show file tree
Hide file tree
Showing 73 changed files with 2,871 additions and 446 deletions.
336 changes: 165 additions & 171 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ tempfile.workspace = true
tokio = { workspace = true, optional = true }
bon.workspace = true
users.workspace = true
thiserror = "2.0.7"

[features]
# Top level features
Expand Down
133 changes: 64 additions & 69 deletions src/commands/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ use std::{

use blue_build_process_management::ASYNC_RUNTIME;
use blue_build_recipe::{FromFileList, ModuleExt, Recipe, StagesExt};
use blue_build_utils::traits::IntoCollector;
use bon::Builder;
use clap::Args;
use colored::Colorize;
use log::{debug, info, trace};
use miette::{bail, miette, Context, IntoDiagnostic, Report};
use miette::{bail, miette, Report};
use rayon::prelude::*;
use schema_validator::{
SchemaValidator, MODULE_STAGE_LIST_V1_SCHEMA_URL, MODULE_V1_SCHEMA_URL, RECIPE_V1_SCHEMA_URL,
STAGE_V1_SCHEMA_URL,
SchemaValidateError, SchemaValidator, MODULE_STAGE_LIST_V1_SCHEMA_URL, MODULE_V1_SCHEMA_URL,
RECIPE_V1_SCHEMA_URL, STAGE_V1_SCHEMA_URL,
};
use serde::de::DeserializeOwned;
use serde_json::Value;
Expand Down Expand Up @@ -97,11 +98,21 @@ impl BlueBuildCommand for ValidateCommand {
impl ValidateCommand {
async fn setup_validators(&mut self) -> Result<(), Report> {
let (rv, sv, mv, mslv) = tokio::try_join!(
SchemaValidator::builder().url(RECIPE_V1_SCHEMA_URL).build(),
SchemaValidator::builder().url(STAGE_V1_SCHEMA_URL).build(),
SchemaValidator::builder().url(MODULE_V1_SCHEMA_URL).build(),
SchemaValidator::builder()
.url(RECIPE_V1_SCHEMA_URL)
.all_errors(self.all_errors)
.build(),
SchemaValidator::builder()
.url(STAGE_V1_SCHEMA_URL)
.all_errors(self.all_errors)
.build(),
SchemaValidator::builder()
.url(MODULE_V1_SCHEMA_URL)
.all_errors(self.all_errors)
.build(),
SchemaValidator::builder()
.url(MODULE_STAGE_LIST_V1_SCHEMA_URL)
.all_errors(self.all_errors)
.build(),
)?;
self.recipe_validator = Some(rv);
Expand All @@ -116,16 +127,16 @@ impl ValidateCommand {
path: &Path,
traversed_files: &[&Path],
single_validator: &SchemaValidator,
) -> Vec<Report>
) -> Vec<SchemaValidateError>
where
DF: DeserializeOwned + FromFileList,
{
let path_display = path.display().to_string().bold().italic();

if traversed_files.contains(&path) {
return vec![miette!(
"{} File {path_display} has already been parsed:\n{traversed_files:?}",
"Circular dependency detected!".bright_red(),
return vec![SchemaValidateError::CircularDependency(
path.to_path_buf(),
traversed_files.collect_into_vec(),
)];
}
let traversed_files = {
Expand All @@ -141,100 +152,90 @@ impl ValidateCommand {
};

match serde_yaml::from_str::<Value>(&file_str)
.into_diagnostic()
.with_context(|| format!("Failed to deserialize file {path_display}"))
.map_err(|e| SchemaValidateError::SerdeYamlError(e, path.to_path_buf()))
{
Ok(instance) => {
trace!("{path_display}:\n{instance}");

if instance.get(DF::LIST_KEY).is_some() {
debug!("{path_display} is a list file");
let err = match self
let err = self
.module_stage_list_validator
.as_ref()
.unwrap()
.process_validation(path, file_str.clone(), self.all_errors)
{
Err(e) => return vec![e],
Ok(e) => e,
};
.process_validation(path, file_str.clone())
.err();

err.map_or_else(
|| {
serde_yaml::from_str::<DF>(&file_str)
.into_diagnostic()
.map_or_else(
|e| vec![e],
|file| {
let mut errs = file
.get_from_file_paths()
serde_yaml::from_str::<DF>(&file_str).map_or_else(
|e| {
vec![SchemaValidateError::SerdeYamlError(e, path.to_path_buf())]
},
|file| {
let mut errs = file
.get_from_file_paths()
.par_iter()
.map(|file_path| {
self.validate_file::<DF>(
file_path,
&traversed_files,
single_validator,
)
})
.flatten()
.collect::<Vec<_>>();
errs.extend(
file.get_module_from_file_paths()
.par_iter()
.map(|file_path| {
self.validate_file::<DF>(
self.validate_file::<ModuleExt>(
file_path,
&traversed_files,
single_validator,
&[],
self.module_validator.as_ref().unwrap(),
)
})
.flatten()
.collect::<Vec<_>>();
errs.extend(
file.get_module_from_file_paths()
.par_iter()
.map(|file_path| {
self.validate_file::<ModuleExt>(
file_path,
&[],
self.module_validator.as_ref().unwrap(),
)
})
.flatten()
.collect::<Vec<_>>(),
);
errs
},
)
.collect::<Vec<_>>(),
);
errs
},
)
},
|err| vec![err],
)
} else {
debug!("{path_display} is a single file file");
single_validator
.process_validation(path, file_str, self.all_errors)
.map_or_else(|e| vec![e], |e| e.map_or_else(Vec::new, |e| vec![e]))
.process_validation(path, file_str)
.map_or_else(|e| vec![e], |()| Vec::new())
}
}
Err(e) => vec![e],
}
}

fn validate_recipe(&self) -> Result<(), Vec<Report>> {
fn validate_recipe(&self) -> Result<(), Vec<SchemaValidateError>> {
let recipe_path_display = self.recipe.display().to_string().bold().italic();
debug!("Validating recipe {recipe_path_display}");

let recipe_str = Arc::new(read_file(&self.recipe).map_err(err_vec)?);
let recipe: Value = serde_yaml::from_str(&recipe_str)
.into_diagnostic()
.with_context(|| format!("Failed to deserialize recipe {recipe_path_display}"))
.map_err(err_vec)?;
.map_err(|e| vec![SchemaValidateError::SerdeYamlError(e, self.recipe.clone())])?;
trace!("{recipe_path_display}:\n{recipe}");

let schema_validator = self.recipe_validator.as_ref().unwrap();
let err = schema_validator
.process_validation(&self.recipe, recipe_str.clone(), self.all_errors)
.map_err(err_vec)?;
.process_validation(&self.recipe, recipe_str.clone())
.err();

if let Some(err) = err {
Err(vec![err])
} else {
let recipe: Recipe = serde_yaml::from_str(&recipe_str)
.into_diagnostic()
.with_context(|| {
format!("Unable to convert Value to Recipe for {recipe_path_display}")
})
.map_err(err_vec)?;
.map_err(|e| vec![SchemaValidateError::SerdeYamlError(e, self.recipe.clone())])?;

let mut errors: Vec<Report> = Vec::new();
let mut errors: Vec<SchemaValidateError> = Vec::new();
if let Some(stages) = &recipe.stages_ext {
debug!("Validating stages for recipe {recipe_path_display}");

Expand Down Expand Up @@ -287,25 +288,19 @@ impl ValidateCommand {
}
}

fn err_vec(err: Report) -> Vec<Report> {
fn err_vec(err: SchemaValidateError) -> Vec<SchemaValidateError> {
vec![err]
}

fn read_file(path: &Path) -> Result<String, Report> {
fn read_file(path: &Path) -> Result<String, SchemaValidateError> {
let mut recipe = String::new();
BufReader::new(
OpenOptions::new()
.read(true)
.open(path)
.into_diagnostic()
.with_context(|| {
format!(
"Unable to open {}",
path.display().to_string().italic().bold()
)
})?,
.map_err(|e| SchemaValidateError::OpenFile(e, path.to_path_buf()))?,
)
.read_to_string(&mut recipe)
.into_diagnostic()?;
.map_err(|e| SchemaValidateError::ReadFile(e, path.to_path_buf()))?;
Ok(recipe)
}
49 changes: 26 additions & 23 deletions src/commands/validate/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,34 +87,37 @@ impl TryFrom<String> for Location {
}
}

pub struct LocationSegmentIterator<'a> {
iter: std::vec::IntoIter<LocationSegment<'a>>,
}

impl<'a> Iterator for LocationSegmentIterator<'a> {
impl<'a> IntoIterator for &'a Location {
type Item = LocationSegment<'a>;
type IntoIter = std::vec::IntoIter<LocationSegment<'a>>;

fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
fn into_iter(self) -> Self::IntoIter {
self.as_str()
.split('/')
.filter(|p| !p.is_empty())
.map(|p| {
p.parse::<usize>()
.map_or_else(|_| LocationSegment::Property(p), LocationSegment::Index)
})
.collect::<Vec<_>>()
.into_iter()
}
}

impl<'a> IntoIterator for &'a Location {
type Item = LocationSegment<'a>;
type IntoIter = LocationSegmentIterator<'a>;

fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
iter: self
.as_str()
.split('/')
.filter(|p| !p.is_empty())
.map(|p| {
p.parse::<usize>()
.map_or_else(|_| LocationSegment::Property(p), LocationSegment::Index)
})
.collect::<Vec<_>>()
.into_iter(),
impl<'a> FromIterator<LocationSegment<'a>> for Location {
fn from_iter<T: IntoIterator<Item = LocationSegment<'a>>>(iter: T) -> Self {
fn inner<'a, 'b, 'c, I>(path_iter: &mut I, location: &'b LazyLocation<'b, 'a>) -> Location
where
I: Iterator<Item = LocationSegment<'c>>,
{
let Some(path) = path_iter.next() else {
return JsonLocation::from(location).into();
};
let location = location.push(path);
inner(path_iter, &location)
}

let loc = LazyLocation::default();
inner(&mut iter.into_iter(), &loc)
}
}
Loading

0 comments on commit ac10b61

Please sign in to comment.