yaml_validator/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::collections::BTreeMap;
4use std::convert::TryFrom;
5pub use yaml_rust;
6use yaml_rust::Yaml;
7
8mod breadcrumb;
9mod errors;
10mod modifiers;
11mod types;
12mod utils;
13use modifiers::*;
14use types::*;
15
16pub use errors::schema::{SchemaError, SchemaErrorKind};
17use errors::ValidationError;
18
19use crate::types::bool::SchemaBool;
20use utils::{CondenseErrors, OptionalLookup, YamlUtils};
21
22/// Validation trait implemented by all types, as well as the [Schema](crate::Schema) type
23pub trait Validate<'yaml, 'schema: 'yaml> {
24    fn validate(
25        &self,
26        ctx: &'schema Context<'schema>,
27        yaml: &'yaml Yaml,
28    ) -> Result<(), ValidationError<'yaml>>;
29}
30
31/// Contains a number of schemas that may or may not be dependent on each other.
32#[derive(Debug, Default)]
33pub struct Context<'schema> {
34    schemas: BTreeMap<&'schema str, Schema<'schema>>,
35}
36
37impl<'schema> Context<'schema> {
38    /// Get a reference to a single schema within the context to use for validation.
39    ///
40    /// # Examples
41    ///
42    /// ```rust
43    /// # use yaml_rust::YamlLoader;
44    /// # use std::convert::TryFrom;
45    /// # use yaml_validator::{Validate, Context};
46    /// #
47    /// let schemas = vec![
48    ///     YamlLoader::load_from_str(r#"
49    ///         uri: just-a-number
50    ///         schema:
51    ///             type: integer
52    ///     "#).unwrap().remove(0)
53    /// ];
54    ///
55    /// let context = Context::try_from(&schemas[..]).unwrap();
56    /// let document = YamlLoader::load_from_str("10").unwrap().remove(0);
57    ///
58    /// context.get_schema("just-a-number").unwrap()
59    ///     .validate(&context, &document).unwrap();
60    /// ```
61    pub fn get_schema(&self, uri: &str) -> Option<&Schema<'schema>> {
62        self.schemas.get(uri)
63    }
64}
65
66/// A context can only be created from a vector of Yaml documents, all of which must fit the schema layout.
67impl<'schema> TryFrom<&'schema [Yaml]> for Context<'schema> {
68    type Error = SchemaError<'schema>;
69    fn try_from(documents: &'schema [Yaml]) -> Result<Self, Self::Error> {
70        let schemas = SchemaError::condense_errors(&mut documents.iter().map(Schema::try_from))?;
71
72        Ok(Context {
73            schemas: schemas
74                .into_iter()
75                .map(|schema| (schema.uri, schema))
76                .collect(),
77        })
78    }
79}
80
81#[derive(Debug)]
82enum PropertyType<'schema> {
83    Object(SchemaObject<'schema>),
84    Array(SchemaArray<'schema>),
85    Hash(SchemaHash<'schema>),
86    String(SchemaString),
87    Integer(SchemaInteger),
88    Real(SchemaReal),
89    Bool(SchemaBool),
90    Reference(SchemaReference<'schema>),
91    Not(SchemaNot<'schema>),
92    OneOf(SchemaOneOf<'schema>),
93    AllOf(SchemaAllOf<'schema>),
94    AnyOf(SchemaAnyOf<'schema>),
95}
96
97impl<'schema> TryFrom<&'schema Yaml> for PropertyType<'schema> {
98    type Error = SchemaError<'schema>;
99    fn try_from(yaml: &'schema Yaml) -> Result<Self, Self::Error> {
100        if yaml.as_hash().is_none() {
101            return Err(SchemaErrorKind::WrongType {
102                expected: "hash",
103                actual: yaml.type_to_str(),
104            }
105            .into());
106        }
107
108        if let Some(uri) = yaml
109            .lookup("$ref", "string", Yaml::as_str)
110            .into_optional()
111            .map_err(SchemaError::from)?
112        {
113            return Ok(PropertyType::Reference(SchemaReference { uri }));
114        }
115
116        if yaml
117            .lookup("not", "hash", Option::from)
118            .into_optional()
119            .map_err(SchemaError::from)?
120            .is_some()
121        {
122            return Ok(PropertyType::Not(SchemaNot::try_from(yaml)?));
123        }
124
125        if yaml
126            .lookup("oneOf", "hash", Option::from)
127            .into_optional()
128            .map_err(SchemaError::from)?
129            .is_some()
130        {
131            return Ok(PropertyType::OneOf(SchemaOneOf::try_from(yaml)?));
132        }
133
134        if yaml
135            .lookup("allOf", "hash", Option::from)
136            .into_optional()
137            .map_err(SchemaError::from)?
138            .is_some()
139        {
140            return Ok(PropertyType::AllOf(SchemaAllOf::try_from(yaml)?));
141        }
142
143        if yaml
144            .lookup("anyOf", "hash", Option::from)
145            .into_optional()
146            .map_err(SchemaError::from)?
147            .is_some()
148        {
149            return Ok(PropertyType::AnyOf(SchemaAnyOf::try_from(yaml)?));
150        }
151
152        let typename = yaml.lookup("type", "string", Yaml::as_str)?;
153
154        match typename {
155            "object" => Ok(PropertyType::Object(SchemaObject::try_from(yaml)?)),
156            "string" => Ok(PropertyType::String(SchemaString::try_from(yaml)?)),
157            "integer" => Ok(PropertyType::Integer(SchemaInteger::try_from(yaml)?)),
158            "real" => Ok(PropertyType::Real(SchemaReal::try_from(yaml)?)),
159            "array" => Ok(PropertyType::Array(SchemaArray::try_from(yaml)?)),
160            "hash" => Ok(PropertyType::Hash(SchemaHash::try_from(yaml)?)),
161            "boolean" => Ok(PropertyType::Bool(SchemaBool::try_from(yaml)?)),
162            unknown_type => Err(SchemaErrorKind::UnknownType { unknown_type }.into()),
163        }
164    }
165}
166
167impl<'yaml, 'schema: 'yaml> Validate<'yaml, 'schema> for PropertyType<'schema> {
168    fn validate(
169        &self,
170        ctx: &'schema Context<'schema>,
171        yaml: &'yaml Yaml,
172    ) -> Result<(), ValidationError<'yaml>> {
173        match self {
174            PropertyType::Integer(p) => p.validate(ctx, yaml),
175            PropertyType::Real(p) => p.validate(ctx, yaml),
176            PropertyType::String(p) => p.validate(ctx, yaml),
177            PropertyType::Object(p) => p.validate(ctx, yaml),
178            PropertyType::Array(p) => p.validate(ctx, yaml),
179            PropertyType::Hash(p) => p.validate(ctx, yaml),
180            PropertyType::Reference(p) => p.validate(ctx, yaml),
181            PropertyType::Not(p) => p.validate(ctx, yaml),
182            PropertyType::OneOf(p) => p.validate(ctx, yaml),
183            PropertyType::AllOf(p) => p.validate(ctx, yaml),
184            PropertyType::AnyOf(p) => p.validate(ctx, yaml),
185            PropertyType::Bool(p) => p.validate(ctx, yaml),
186        }
187    }
188}
189
190/// A single schema unit used for validation.
191#[derive(Debug)]
192pub struct Schema<'schema> {
193    uri: &'schema str,
194    schema: PropertyType<'schema>,
195}
196
197impl<'schema> TryFrom<&'schema Yaml> for Schema<'schema> {
198    type Error = SchemaError<'schema>;
199    fn try_from(yaml: &'schema Yaml) -> Result<Self, Self::Error> {
200        yaml.strict_contents(&["uri", "schema"], &[])?;
201
202        let uri = yaml.lookup("uri", "string", Yaml::as_str)?;
203        let schema = PropertyType::try_from(yaml.lookup("schema", "yaml", Option::from)?)
204            .map_err(SchemaError::add_path_name(uri))?;
205
206        Ok(Schema { uri, schema })
207    }
208}
209
210impl<'yaml, 'schema: 'yaml> Validate<'yaml, 'schema> for Schema<'schema> {
211    fn validate(
212        &self,
213        ctx: &'schema Context<'schema>,
214        yaml: &'yaml Yaml,
215    ) -> Result<(), ValidationError<'yaml>> {
216        self.schema.validate(ctx, yaml)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::utils::load_simple;
224    use crate::Context;
225    use yaml_rust::YamlLoader;
226
227    #[test]
228    fn from_yaml() {
229        let yaml = YamlLoader::load_from_str(
230            r#"---
231uri: test
232schema:
233  type: integer
234---
235uri: another
236schema:
237  $ref: test
238"#,
239        )
240        .unwrap();
241
242        let context = Context::try_from(&yaml[..]).unwrap();
243        let schema = context.get_schema("another").unwrap();
244        dbg!(&context);
245        dbg!(&schema);
246        schema.validate(&context, &load_simple("20")).unwrap();
247    }
248}