Skip to content

Commit

Permalink
Implement modifiers (#24)
Browse files Browse the repository at this point in the history
* Began implementing 'not' modifier. Still need validation testing

* Add tests for 'not' modifier

* Fix modifier nesting for 'not' inversion

* Implement 'required' modifier for object, but have not made fields optional yet, nor written tests for them

* Remove debug printing statement

* Implement oneOf modifier. Still needs more tests

* Add test case for oneOf modifier for complex(ish) validators like 'string' with min and max length set, where overlap may occur

* Make object members optional by default and add tests for 'required' tag on objects

* Remove unnecessary import of smallvec

* Implement anyOf & allOf

* Numerous linting fixes

* Remove unnecessary return type
  • Loading branch information
MathiasPius committed Nov 29, 2020
1 parent 2a9efc5 commit 7c09d93
Show file tree
Hide file tree
Showing 10 changed files with 818 additions and 61 deletions.
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

0 comments on commit 7c09d93

Please sign in to comment.