use apollo_redaction::Redacted;
use crate::ValidationErrors;
use crate::YamlLocationData;
use crate::errors::ValidationError;
use jsonschema::paths::Location;
use jsonschema::paths::LocationSegment;
use miette::Diagnostic;
use std::fmt::Display;
use std::sync::Arc;
pub struct ErrorCollector<'e> {
location_data: &'e YamlLocationData,
errors: &'e mut Vec<Box<dyn Diagnostic + Send + Sync + 'static>>,
path: Location,
}
impl<'e> ErrorCollector<'e> {
pub(crate) fn new(
location_data: &'e YamlLocationData,
errors: &'e mut Vec<Box<dyn Diagnostic + Send + Sync + 'static>>,
) -> Self {
Self {
location_data,
errors,
path: Location::new(),
}
}
pub fn inner(&mut self) -> ErrorCollector<'_> {
ErrorCollector {
location_data: self.location_data,
errors: self.errors,
path: self.path.clone(),
}
}
pub fn nest<'s, 'a>(
&'s mut self,
segment: impl Into<LocationSegment<'a>>,
) -> ErrorCollector<'s> {
ErrorCollector {
location_data: self.location_data,
errors: self.errors,
path: self.path.join(segment),
}
}
pub fn span(&self) -> miette::SourceSpan {
self.location_data
.resolve_instance_span(&self.path)
.unwrap_or_else(|| miette::SourceSpan::new(0.into(), 0))
}
pub fn report(&mut self, diag: impl Diagnostic + Send + Sync + 'static) {
self.errors.push(Box::new(diag));
}
pub fn report_simple(&mut self, message: impl Display) {
self.report(ValidationError {
label: self.span(),
message: message.to_string(),
});
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
}
pub trait Validate {
fn validate<'a>(&self, _errors: ErrorCollector<'a>) {}
}
impl Validate for bool {}
impl Validate for i8 {}
impl Validate for u8 {}
impl Validate for i16 {}
impl Validate for u16 {}
impl Validate for i32 {}
impl Validate for u32 {}
impl Validate for i64 {}
impl Validate for u64 {}
impl Validate for isize {}
impl Validate for usize {}
impl Validate for std::num::NonZeroI8 {}
impl Validate for std::num::NonZeroU8 {}
impl Validate for std::num::NonZeroI16 {}
impl Validate for std::num::NonZeroU16 {}
impl Validate for std::num::NonZeroI32 {}
impl Validate for std::num::NonZeroU32 {}
impl Validate for std::num::NonZeroI64 {}
impl Validate for std::num::NonZeroU64 {}
impl Validate for std::num::NonZeroIsize {}
impl Validate for std::num::NonZeroUsize {}
impl Validate for str {}
impl Validate for String {}
impl Validate for () {}
impl<T: Validate, R> Validate for Redacted<T, R> {
fn validate<'a>(&self, errors: ErrorCollector<'a>) {
self.unredact().validate(errors);
}
}
pub fn validate<T: Validate>(value: T) -> Result<T, ValidationErrors> {
let location_data = YamlLocationData::empty();
let mut errors = vec![];
value.validate(ErrorCollector::new(&location_data, &mut errors));
if errors.is_empty() {
Ok(value)
} else {
Err(ValidationErrors {
source_code: Arc::from(""),
errors,
})
}
}
#[cfg(test)]
mod tests {
use super::ErrorCollector;
use super::Validate;
use super::validate;
#[test]
fn programmatic_validation() {
validate("".to_string()).expect("should pass for a simple value");
validate(0).expect("should pass for a primitive value");
#[derive(Debug)]
struct Config {
#[expect(unused)]
a: usize,
#[expect(unused)]
b: usize,
}
impl Validate for Config {
fn validate(&self, mut errors: ErrorCollector<'_>) {
errors.nest("a").report_simple("always wrong");
errors.nest("b").report_simple("never correct");
}
}
let err = validate(Config { a: 0, b: 1 }).expect_err("should return errors");
assert_eq!(err.errors.len(), 2);
}
}