valico/json_schema/
schema.rs

1use url;
2use std::collections;
3use serde_json::{Value};
4use phf;
5use std::ops;
6
7use super::helpers;
8use super::scope;
9use super::keywords;
10use super::validators;
11
12#[derive(Debug)]
13pub struct WalkContext<'a> {
14    pub url: &'a url::Url,
15    pub fragment: Vec<String>,
16    pub scopes: &'a mut collections::HashMap<String, Vec<String>>
17}
18
19impl<'a> WalkContext<'a> {
20    pub fn escaped_fragment(&self) -> String {
21        helpers::connect(self.fragment.iter().map(|s| s.as_ref()).collect::<Vec<&str>>().as_ref())
22    }
23}
24
25#[derive(Debug)]
26#[allow(missing_copy_implementations)]
27pub enum SchemaError {
28    WrongId,
29    IdConflicts,
30    NotAnObject,
31    UrlParseError(url::ParseError),
32    UnknownKey(String),
33    Malformed {
34        path: String,
35        detail: String
36    }
37}
38
39#[derive(Debug)]
40pub struct ScopedSchema<'a> {
41    scope: &'a scope::Scope,
42    schema: &'a Schema
43}
44
45impl<'a> ops::Deref for ScopedSchema<'a> {
46    type Target = Schema;
47
48    fn deref(&self) -> &Schema {
49        &self.schema
50    }
51}
52
53impl<'a> ScopedSchema<'a> {
54    pub fn new(scope: &'a scope::Scope, schema: &'a Schema) -> ScopedSchema<'a> {
55        ScopedSchema {
56            scope: scope,
57            schema: schema
58        }
59    }
60
61    pub fn validate(&self, data: &Value) -> validators::ValidationState {
62        return self.schema.validate_in_scope(data, "", self.scope);
63    }
64
65    pub fn validate_in(&self, data: &Value, path: &str) -> validators::ValidationState {
66        return self.schema.validate_in_scope(data, path, self.scope);
67    }
68}
69
70#[derive(Debug)]
71#[allow(dead_code)]
72pub struct Schema {
73    pub id: Option<url::Url>,
74    schema: Option<url::Url>,
75    original: Value,
76    tree: collections::BTreeMap<String, Schema>,
77    validators: validators::Validators,
78    scopes: collections::HashMap<String, Vec<String>>
79}
80
81include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
82
83pub struct CompilationSettings<'a> {
84    pub keywords: &'a keywords::KeywordMap,
85    pub ban_unknown_keywords: bool
86}
87
88impl<'a> CompilationSettings<'a> {
89    pub fn new(keywords: &'a keywords::KeywordMap, ban_unknown_keywords: bool) -> CompilationSettings<'a> {
90        CompilationSettings {
91            keywords: keywords,
92            ban_unknown_keywords: ban_unknown_keywords,
93        }
94    }
95}
96
97impl Schema {
98    fn compile(def: Value, external_id: Option<url::Url>, settings: CompilationSettings) -> Result<Schema, SchemaError> {
99        if !def.is_object() {
100            return Err(SchemaError::NotAnObject)
101        }
102
103        let id = if external_id.is_some() {
104            external_id.unwrap()
105        } else {
106            try!(helpers::parse_url_key("id", &def)).clone().unwrap_or_else(|| helpers::generate_id())
107        };
108
109        let schema = try!(helpers::parse_url_key("$schema", &def));
110
111        let (tree, mut scopes) = {
112            let mut tree = collections::BTreeMap::new();
113            let obj = def.as_object().unwrap();
114
115            let mut scopes = collections::HashMap::new();
116
117            for (key, value) in obj.iter() {
118                if !value.is_object() && !value.is_array() { continue; }
119                if FINAL_KEYS.contains(&key[..]) { continue; }
120
121                let mut context = WalkContext {
122                    url: &id,
123                    fragment: vec![key.clone()],
124                    scopes: &mut scopes
125                };
126
127                let scheme = try!(Schema::compile_sub(
128                    value.clone(),
129                    &mut context,
130                    &settings,
131                    !NON_SCHEMA_KEYS.contains(&key[..])
132                ));
133
134                tree.insert(helpers::encode(key), scheme);
135            }
136
137            (tree, scopes)
138        };
139
140        let validators = try!(Schema::compile_keywords(&def, &WalkContext {
141            url: &id,
142            fragment: vec![],
143            scopes: &mut scopes,
144        }, &settings));
145
146        let schema = Schema {
147            id: Some(id),
148            schema: schema,
149            original: def,
150            tree: tree,
151            validators: validators,
152            scopes: scopes
153        };
154
155        Ok(schema)
156    }
157
158    fn compile_keywords(def: &Value, context: &WalkContext, settings: &CompilationSettings) -> Result<validators::Validators, SchemaError> {
159        let mut validators = vec![];
160        let mut keys: collections::HashSet<&str> = def.as_object().unwrap().keys().map(|key| key.as_ref()).collect();
161        let mut not_consumed = collections::HashSet::new();
162
163        loop {
164            let key = keys.iter().next().cloned();
165            if key.is_some() {
166                let key = key.unwrap();
167                match settings.keywords.get(&key) {
168                    Some(keyword) => {
169                        keyword.consume(&mut keys);
170
171                        let is_exclusive_keyword = keyword.keyword.is_exclusive();
172
173                        match try!(keyword.keyword.compile(def, context)) {
174                            Some(validator) => {
175                                if is_exclusive_keyword {
176                                    validators = vec![validator];
177                                } else {
178                                    validators.push(validator);
179                                }
180                            },
181                            None => ()
182                        };
183
184                        if is_exclusive_keyword {
185                            break;
186                        }
187                    },
188                    None => {
189                        keys.remove(&key);
190                        if settings.ban_unknown_keywords {
191                            not_consumed.insert(key);
192                        }
193                    }
194                }
195            } else {
196                break;
197            }
198        }
199
200        if settings.ban_unknown_keywords && not_consumed.len() > 0 {
201            for key in not_consumed.iter() {
202                if !ALLOW_NON_CONSUMED_KEYS.contains(&key[..]) {
203                    return Err(SchemaError::UnknownKey(key.to_string()))
204                }
205            }
206        }
207
208        Ok(validators)
209    }
210
211    fn compile_sub(def: Value, context: &mut WalkContext, keywords: &CompilationSettings, is_schema: bool) -> Result<Schema, SchemaError> {
212
213        let mut id = None;
214        let mut schema = None;
215
216        if is_schema {
217            id = try!(helpers::parse_url_key_with_base("id", &def, context.url));
218            schema = try!(helpers::parse_url_key("$schema", &def));
219        }
220
221        let tree = {
222            let mut tree = collections::BTreeMap::new();
223
224            if def.is_object() {
225                let obj = def.as_object().unwrap();
226                let parent_key = &context.fragment[context.fragment.len() - 1];
227
228                for (key, value) in obj.iter() {
229                    if !value.is_object() && !value.is_array() { continue; }
230                    if !PROPERTY_KEYS.contains(&parent_key[..]) && FINAL_KEYS.contains(&key[..]) { continue; }
231
232                    let mut current_fragment = context.fragment.clone();
233                    current_fragment.push(key.clone());
234
235                    let is_schema = PROPERTY_KEYS.contains(&parent_key[..]) || !NON_SCHEMA_KEYS.contains(&key[..]);
236
237                    let mut context = WalkContext {
238                        url: id.as_ref().unwrap_or(context.url),
239                        fragment: current_fragment,
240                        scopes: context.scopes
241                    };
242
243                    let scheme = try!(Schema::compile_sub(
244                        value.clone(),
245                        &mut context,
246                        keywords,
247                        is_schema
248                    ));
249
250                    tree.insert(helpers::encode(key), scheme);
251                }
252            } else if def.is_array() {
253                let array = def.as_array().unwrap();
254
255                for (idx, value) in array.iter().enumerate() {
256                    if !value.is_object() && !value.is_array() { continue; }
257
258                    let mut current_fragment = context.fragment.clone();
259                    current_fragment.push(idx.to_string().clone());
260
261                    let mut context = WalkContext {
262                        url: id.as_ref().unwrap_or(context.url),
263                        fragment: current_fragment,
264                        scopes: context.scopes
265                    };
266
267                    let scheme = try!(Schema::compile_sub(
268                        value.clone(),
269                        &mut context,
270                        keywords,
271                        true
272                    ));
273
274                    tree.insert(idx.to_string().clone(), scheme);
275                }
276            }
277
278            tree
279        };
280
281        if id.is_some() {
282            context.scopes.insert(id.clone().unwrap().into_string(), context.fragment.clone());
283        }
284
285        let validators = if is_schema && def.is_object() {
286            try!(Schema::compile_keywords(&def, context, keywords))
287        } else {
288            vec![]
289        };
290
291        let schema = Schema {
292            id: id,
293            schema: schema,
294            original: def,
295            tree: tree,
296            validators: validators,
297            scopes: collections::HashMap::new()
298        };
299
300        Ok(schema)
301    }
302
303    pub fn resolve(&self, id: &str) -> Option<&Schema> {
304        let path = self.scopes.get(id);
305        path.map(|path| {
306            let mut schema = self;
307            for item in path.iter() {
308                schema = schema.tree.get(item).unwrap()
309            }
310            schema
311        })
312    }
313
314    pub fn resolve_fragment(&self, fragment: &str) -> Option<&Schema> {
315        assert!(fragment.starts_with("/"), "Can't resolve id fragments");
316
317        let parts = fragment[1..].split("/");
318        let mut schema = self;
319        for part in parts {
320            match schema.tree.get(part) {
321                Some(sch) => schema = sch,
322                None => return None
323            }
324        }
325
326        Some(schema)
327    }
328}
329
330impl Schema {
331    fn validate_in_scope(&self, data: &Value, path: &str, scope: &scope::Scope) -> validators::ValidationState {
332        let mut state = validators::ValidationState::new();
333
334        for validator in self.validators.iter() {
335            state.append(validator.validate(data, path, scope))
336        }
337
338        state
339    }
340}
341
342pub fn compile(def: Value, external_id: Option<url::Url>, settings: CompilationSettings) -> Result<Schema, SchemaError> {
343    Schema::compile(def, external_id, settings)
344}
345
346#[test]
347fn schema_doesnt_compile_not_object() {
348    assert!(Schema::compile(Value::Bool(true), None, CompilationSettings::new(&keywords::default(), true)).is_err());
349}