Skip to main content

valico/json_schema/
scope.rs

1use serde_json::Value;
2use std::collections;
3
4use super::helpers;
5use super::keywords;
6use super::schema;
7use super::SchemaVersion;
8
9#[allow(dead_code)]
10#[derive(Debug)]
11pub struct Scope {
12    keywords: keywords::KeywordMap,
13    schemes: collections::HashMap<String, schema::Schema>,
14    pub(crate) supply_defaults: bool,
15    schema_version: SchemaVersion,
16}
17
18#[allow(dead_code)]
19impl Scope {
20    pub fn new() -> Scope {
21        let mut scope = Scope::without_formats(SchemaVersion::Draft7);
22        scope.add_keyword(vec!["format"], keywords::format::Format::new());
23        scope
24    }
25
26    pub fn without_formats(version: SchemaVersion) -> Scope {
27        Scope {
28            keywords: keywords::default(),
29            schemes: collections::HashMap::new(),
30            supply_defaults: false,
31            schema_version: version,
32        }
33    }
34
35    pub fn with_formats<F>(build_formats: F, version: SchemaVersion) -> Scope
36    where
37        F: FnOnce(&mut keywords::format::FormatBuilders),
38    {
39        let mut scope = Scope::without_formats(version);
40        scope.add_keyword(
41            vec!["format"],
42            keywords::format::Format::with(build_formats),
43        );
44        scope
45    }
46
47    pub fn set_version(mut self, version: SchemaVersion) -> Self {
48        self.schema_version = version;
49        self
50    }
51
52    /// ### use `default` values to compute an enriched version of the input
53    ///
54    /// JSON schema foresees the `default` attribute in any schema but does not assign
55    /// it a specific semantics; it is merely suggested that it may be used to supply
56    /// default values, e.g. for use in an interactive editor. The only specification
57    /// is that the value of a `default` attribute shall be a JSON value that SHOULD
58    /// validate its schema if supplied.
59    ///
60    /// This feature activates defaults as a mechanism for schema authors to include
61    /// defaults such that consuming programs can rely upon the presence of such paths
62    /// even if the validated JSON object does not contain values at these paths. This
63    /// allows for example non-`required` properties to be parsed as mandatory by
64    /// supplying the fallback within the schema.
65    ///
66    /// The most basic usage is to add defaults to scalar properties (like strings or
67    /// numbers). A more interesting aspect is that defaults bubble up through the
68    /// property tree:
69    ///
70    ///  - an element of `properties` with a default value will create a default value
71    ///    for its parent unless that one declares a default itself
72    ///  - if an array is given as the value of an `items` property and all schemas in
73    ///    that array provide a default, then a default is created for the schema
74    ///    containing the `items` clause unless that schema declares a default itself
75    ///  - the default of a `$ref` schema is the default of its referenced schema
76    ///
77    /// When validating an instance against the thus enriched schema, each path that
78    /// has a default in the schema and no value in the instance will have the default
79    /// added at that path (a copy will be made and returned within the ValidationState
80    /// structure).
81    ///
82    /// The following validators interact additionally with the defaults:
83    ///
84    ///  - `contains`: if there is an object in the array that validates the supplied schema,
85    ///    then that object is outfitted with the defaults of that schema; all other
86    ///    array elements remain unchanged (i.e. only the first match gets defaults)
87    ///  - `dependencies`: if the instance triggers a dependent schema and validates it,
88    ///    then that schema’s defaults will be applied
89    ///  - `not`: the supplied schema is used to validate a copy of the instance with
90    ///    defaults added to determine whether to reject the original instance, but
91    ///    the enriched instance is then discarded
92    ///  - `anyOf`: the search of a schema for which the supplied instance is valid is
93    ///    conducted with enriched instances according to the schema being tried; the
94    ///    first enrichted instance that validates the schema is returned
95    ///  - `oneOf`: just as for `anyOf`, apart from checking that the instance does not
96    ///    validate the remaining schemas
97    ///  - `allOf`: first, make one pass over the supplied schemas, handing each one the
98    ///    enriched instance from the previous (aborting in case of errors); second,
99    ///    another such pass, starting with the result from the first; third, a check
100    ///    whether the enrichment results from the two passes match (it is an error
101    ///    if they are different — this is an approximation, but a reasonable one)
102    ///
103    /// Please note that supplying default values this way can lead to a schema that
104    /// equates to the `false` schema, i.e. does not match any instance, so don’t try
105    /// to be too clever, especially with the `not`, `allOf`, and `oneOf` validators.
106    ///
107    /// ### Caveat emptor
108    ///
109    /// The order in which validators are applied to an instance is UNDEFINED apart from
110    /// the rule that `properties` and `items` will be moved to the front (but the order
111    /// between these is UNDEFINED as well). Therefore, if one validator depends on the
112    /// fact that a default value has been injected by processing another validator, then
113    /// the result is UNDEFINED (with the exception stated in the previous sentence).
114    #[must_use]
115    pub fn supply_defaults(self) -> Self {
116        Scope {
117            keywords: self.keywords,
118            schemes: self.schemes,
119            supply_defaults: true,
120            schema_version: SchemaVersion::Draft7,
121        }
122    }
123
124    pub fn compile(
125        &mut self,
126        def: Value,
127        ban_unknown: bool,
128    ) -> Result<url::Url, schema::SchemaError> {
129        let mut schema = schema::compile(
130            def,
131            None,
132            schema::CompilationSettings::new(&self.keywords, ban_unknown, self.schema_version),
133        )?;
134        let id = schema.id.clone().unwrap();
135        if self.supply_defaults {
136            schema.add_defaults(&id, self);
137        }
138        self.add(&id, schema)?;
139        Ok(id)
140    }
141
142    pub fn compile_with_id(
143        &mut self,
144        id: &url::Url,
145        def: Value,
146        ban_unknown: bool,
147    ) -> Result<(), schema::SchemaError> {
148        let mut schema = schema::compile(
149            def,
150            Some(id.clone()),
151            schema::CompilationSettings::new(&self.keywords, ban_unknown, self.schema_version),
152        )?;
153        if self.supply_defaults {
154            schema.add_defaults(id, self);
155        }
156        self.add(id, schema)
157    }
158
159    pub fn compile_and_return(
160        &'_ mut self,
161        def: Value,
162        ban_unknown: bool,
163    ) -> Result<schema::ScopedSchema<'_>, schema::SchemaError> {
164        let mut schema = schema::compile(
165            def,
166            None,
167            schema::CompilationSettings::new(&self.keywords, ban_unknown, self.schema_version),
168        )?;
169        let id = schema.id.clone().unwrap();
170        if self.supply_defaults {
171            schema.add_defaults(&id, self);
172        }
173        self.add_and_return(&id, schema)
174    }
175
176    pub fn compile_and_return_with_id<'a>(
177        &'a mut self,
178        id: &url::Url,
179        def: Value,
180        ban_unknown: bool,
181    ) -> Result<schema::ScopedSchema<'a>, schema::SchemaError> {
182        let mut schema = schema::compile(
183            def,
184            Some(id.clone()),
185            schema::CompilationSettings::new(&self.keywords, ban_unknown, self.schema_version),
186        )?;
187        if self.supply_defaults {
188            schema.add_defaults(id, self);
189        }
190        self.add_and_return(id, schema)
191    }
192
193    pub fn add_keyword<T>(&mut self, keys: Vec<&'static str>, keyword: T)
194    where
195        T: keywords::Keyword + 'static,
196    {
197        keywords::decouple_keyword((keys, Box::new(keyword)), &mut self.keywords);
198    }
199
200    #[allow(clippy::map_entry)] // allowing for the return values
201    fn add(&mut self, id: &url::Url, schema: schema::Schema) -> Result<(), schema::SchemaError> {
202        let (id_str, fragment) = helpers::serialize_schema_path(id);
203
204        if fragment.is_some() {
205            return Err(schema::SchemaError::WrongId);
206        }
207
208        if !self.schemes.contains_key(&id_str) {
209            self.schemes.insert(id_str, schema);
210            Ok(())
211        } else {
212            Err(schema::SchemaError::IdConflicts)
213        }
214    }
215
216    #[allow(clippy::map_entry)] // allowing for the return values
217    fn add_and_return<'a>(
218        &'a mut self,
219        id: &url::Url,
220        schema: schema::Schema,
221    ) -> Result<schema::ScopedSchema<'a>, schema::SchemaError> {
222        let (id_str, fragment) = helpers::serialize_schema_path(id);
223
224        if fragment.is_some() {
225            return Err(schema::SchemaError::WrongId);
226        }
227
228        if !self.schemes.contains_key(&id_str) {
229            self.schemes.insert(id_str.clone(), schema);
230            Ok(schema::ScopedSchema::new(self, &self.schemes[&id_str]))
231        } else {
232            Err(schema::SchemaError::IdConflicts)
233        }
234    }
235
236    pub fn resolve<'a>(&'a self, id: &url::Url) -> Option<schema::ScopedSchema<'a>> {
237        let (schema_path, fragment) = helpers::serialize_schema_path(id);
238
239        let schema = self.schemes.get(&schema_path).or_else(|| {
240            // Searching for inline schema in O(N)
241            for (_, schema) in self.schemes.iter() {
242                let internal_schema = schema.resolve(schema_path.as_ref());
243                if internal_schema.is_some() {
244                    return internal_schema;
245                }
246            }
247
248            None
249        });
250
251        schema.and_then(|schema| match fragment {
252            Some(ref fragment) => schema
253                .resolve_fragment(fragment)
254                .map(|schema| schema::ScopedSchema::new(self, schema)),
255            None => Some(schema::ScopedSchema::new(self, schema)),
256        })
257    }
258}
259
260#[test]
261fn lookup() {
262    let mut scope = Scope::new();
263
264    scope
265        .compile(
266            jsonway::object(|schema| schema.set("$id", "http://example.com/schema".to_string()))
267                .unwrap(),
268            false,
269        )
270        .ok()
271        .unwrap();
272
273    scope
274        .compile(
275            jsonway::object(|schema| {
276                schema.set("$id", "http://example.com/schema#sub".to_string());
277                schema.object("subschema", |subschema| {
278                    subschema.set("$id", "#subschema".to_string());
279                })
280            })
281            .unwrap(),
282            false,
283        )
284        .ok()
285        .unwrap();
286
287    assert!(scope
288        .resolve(&url::Url::parse("http://example.com/schema").ok().unwrap())
289        .is_some());
290    assert!(scope
291        .resolve(
292            &url::Url::parse("http://example.com/schema#sub")
293                .ok()
294                .unwrap()
295        )
296        .is_some());
297    assert!(scope
298        .resolve(
299            &url::Url::parse("http://example.com/schema#sub/subschema")
300                .ok()
301                .unwrap()
302        )
303        .is_some());
304    assert!(scope
305        .resolve(
306            &url::Url::parse("http://example.com/schema#subschema")
307                .ok()
308                .unwrap()
309        )
310        .is_some());
311}