Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement modifiers #24

Merged
merged 12 commits into from
Nov 29, 2020
4 changes: 1 addition & 3 deletions yaml-validator-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ fn actual_main(opt: Opt) -> Result<(), Error> {
Ok(())
}

fn main() -> Result<(), Error> {
fn main() {
let opt = Opt::from_args();

match actual_main(opt) {
Expand All @@ -130,8 +130,6 @@ fn main() -> Result<(), Error> {
std::process::exit(1);
}
}

Ok(())
}

#[cfg(test)]
Expand Down
16 changes: 16 additions & 0 deletions yaml-validator/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ impl<'a> Into<SchemaError<'a>> for SchemaErrorKind<'a> {
}
}

pub fn condense_errors<'a, T>(
iter: &mut dyn Iterator<Item = Result<T, SchemaError<'a>>>,
) -> Result<(), SchemaError<'a>> {
let mut errors: Vec<SchemaError> = iter.filter_map(Result::err).collect();

if !errors.is_empty() {
if errors.len() == 1 {
Err(errors.pop().unwrap())
} else {
Err(SchemaErrorKind::Multiple { errors }.into())
}
} else {
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::types::*;
Expand Down
65 changes: 51 additions & 14 deletions yaml-validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ pub use yaml_rust;
use yaml_rust::Yaml;

mod error;
mod modifiers;
mod types;
mod utils;
use modifiers::*;
use types::*;

use error::{add_path_name, optional};
use error::{add_path_name, condense_errors, optional};
pub use error::{SchemaError, SchemaErrorKind};

use utils::YamlUtils;
Expand Down Expand Up @@ -68,15 +70,7 @@ impl<'schema> TryFrom<&'schema Vec<Yaml>> for Context<'schema> {
.map(Schema::try_from)
.partition(Result::is_ok);

if !errs.is_empty() {
let mut errors: Vec<SchemaError<'schema>> =
errs.into_iter().map(Result::unwrap_err).collect();
if errors.len() == 1 {
return Err(errors.pop().unwrap());
} else {
return Err(SchemaErrorKind::Multiple { errors }.into());
}
}
condense_errors(&mut errs.into_iter())?;

Ok(Context {
schemas: schemas
Expand All @@ -97,6 +91,10 @@ enum PropertyType<'schema> {
Integer(SchemaInteger),
Real(SchemaReal),
Reference(SchemaReference<'schema>),
Not(SchemaNot<'schema>),
OneOf(SchemaOneOf<'schema>),
AllOf(SchemaAllOf<'schema>),
AnyOf(SchemaAnyOf<'schema>),
}

impl<'schema> TryFrom<&'schema Yaml> for PropertyType<'schema> {
Expand All @@ -110,15 +108,50 @@ impl<'schema> TryFrom<&'schema Yaml> for PropertyType<'schema> {
.into());
}

let reference = yaml
if let Some(uri) = yaml
.lookup("$ref", "string", Yaml::as_str)
.map(Option::from)
.or_else(optional(None))?;

if let Some(uri) = reference {
.or_else(optional(None))?
{
return Ok(PropertyType::Reference(SchemaReference { uri }));
}

if yaml
.lookup("not", "hash", Option::from)
.map(Option::from)
.or_else(optional(None))?
.is_some()
{
return Ok(PropertyType::Not(SchemaNot::try_from(yaml)?));
}

if yaml
.lookup("oneOf", "hash", Option::from)
.map(Option::from)
.or_else(optional(None))?
.is_some()
{
return Ok(PropertyType::OneOf(SchemaOneOf::try_from(yaml)?));
}

if yaml
.lookup("allOf", "hash", Option::from)
.map(Option::from)
.or_else(optional(None))?
.is_some()
{
return Ok(PropertyType::AllOf(SchemaAllOf::try_from(yaml)?));
}

if yaml
.lookup("anyOf", "hash", Option::from)
.map(Option::from)
.or_else(optional(None))?
.is_some()
{
return Ok(PropertyType::AnyOf(SchemaAnyOf::try_from(yaml)?));
}

let typename = yaml.lookup("type", "string", Yaml::as_str)?;

match typename {
Expand Down Expand Up @@ -147,6 +180,10 @@ impl<'yaml, 'schema: 'yaml> Validate<'yaml, 'schema> for PropertyType<'schema> {
PropertyType::Array(p) => p.validate(ctx, yaml),
PropertyType::Hash(p) => p.validate(ctx, yaml),
PropertyType::Reference(p) => p.validate(ctx, yaml),
PropertyType::Not(p) => p.validate(ctx, yaml),
PropertyType::OneOf(p) => p.validate(ctx, yaml),
PropertyType::AllOf(p) => p.validate(ctx, yaml),
PropertyType::AnyOf(p) => p.validate(ctx, yaml),
}
}
}
Expand Down
124 changes: 124 additions & 0 deletions yaml-validator/src/modifiers/all_of.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use crate::error::{add_path_name, condense_errors, SchemaError, SchemaErrorKind};
use crate::utils::YamlUtils;
use crate::{Context, PropertyType, Validate};
use std::convert::TryFrom;
use yaml_rust::Yaml;

#[derive(Debug)]
pub(crate) struct SchemaAllOf<'schema> {
items: Vec<PropertyType<'schema>>,
}

impl<'schema> TryFrom<&'schema Yaml> for SchemaAllOf<'schema> {
type Error = SchemaError<'schema>;

fn try_from(yaml: &'schema Yaml) -> Result<Self, Self::Error> {
yaml.strict_contents(&["allOf"], &[])?;
let (items, errs): (Vec<_>, Vec<_>) = yaml
.lookup("allOf", "array", Yaml::as_vec)?
.iter()
.map(|property| PropertyType::try_from(property).map_err(add_path_name("items")))
.partition(Result::is_ok);

condense_errors(&mut errs.into_iter())?;

if items.is_empty() {
return Err(SchemaErrorKind::MalformedField {
error: "allOf modifier requires an array of schemas to validate against".to_owned(),
}
.with_path_name("allOf"));
}

Ok(SchemaAllOf {
items: items.into_iter().map(Result::unwrap).collect(),
})
}
}

impl<'yaml, 'schema: 'yaml> Validate<'yaml, 'schema> for SchemaAllOf<'schema> {
fn validate(
&self,
ctx: &'schema Context<'schema>,
yaml: &'yaml Yaml,
) -> Result<(), SchemaError<'yaml>> {
let errs: Vec<_> = self
.items
.iter()
.map(|schema| schema.validate(ctx, yaml).map_err(add_path_name("allOf")))
.filter(Result::is_err)
.collect();

condense_errors(&mut errs.into_iter())?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::utils::load_simple;

#[test]
fn one_of_from_yaml() {
SchemaAllOf::try_from(&load_simple(
r#"
allOf:
- type: integer
- type: string
"#,
))
.unwrap();

assert_eq!(
SchemaAllOf::try_from(&load_simple(
r#"
allOff:
- type: integer
"#,
))
.unwrap_err(),
SchemaErrorKind::Multiple {
errors: vec![
SchemaErrorKind::FieldMissing { field: "allOf" }.into(),
SchemaErrorKind::ExtraField { field: "allOff" }.into(),
]
}
.into()
)
}

#[test]
fn validate_unit_case() {
let yaml = load_simple(
r#"
allOf:
- type: integer
"#,
);
let schema = SchemaAllOf::try_from(&yaml).unwrap();

schema
.validate(&Context::default(), &load_simple("10"))
.unwrap();
}

#[test]
fn validate_multiple_subvalidators() {
let yaml = load_simple(
r#"
allOf:
- type: string
minLength: 10
- type: string
maxLength: 10
"#,
);

let schema = SchemaAllOf::try_from(&yaml).unwrap();

// Validate against a 10-character long string, causing overlap!
schema
.validate(&Context::default(), &load_simple("hello you!"))
.unwrap();
}
}
Loading