Skip to main content

valico/json_schema/keywords/
dependencies.rs

1use serde_json::Value;
2
3use crate::json_schema::validators::dependencies::DepKind;
4
5use super::super::helpers;
6use super::super::schema;
7use super::super::validators;
8
9enum DepsMode {
10    AllowAny,
11    DependentSchemas,
12    DependentRequired,
13}
14
15impl DepsMode {
16    fn get_error(&self) -> String {
17        match self {
18            DepsMode::AllowAny => {
19                "Each value of dependencies MUST be either an object, an array or a boolean."
20            }
21            DepsMode::DependentSchemas => {
22                "Each value of 'dependentSchemas' MUST be an object or a boolean."
23            }
24            DepsMode::DependentRequired => "Each value of 'dependentRequired' MUST be an array.",
25        }
26        .to_owned()
27    }
28}
29
30impl PartialEq for DepsMode {
31    fn eq(&self, other: &Self) -> bool {
32        match (self, other) {
33            (DepsMode::AllowAny, _) => true,
34            (_, DepsMode::AllowAny) => true,
35            (a, b) => core::mem::discriminant(a) == core::mem::discriminant(b),
36        }
37    }
38}
39
40#[allow(missing_copy_implementations)]
41pub struct Dependencies;
42impl Dependencies {
43    fn extract_dependencies(
44        &self,
45        deps: &Value,
46        ctx: &schema::WalkContext<'_>,
47        deps_key: &str,
48        mode: DepsMode,
49    ) -> Result<Vec<(String, DepKind)>, schema::SchemaError> {
50        if !deps.is_object() {
51            return Err(schema::SchemaError::Malformed {
52                path: ctx.fragment.join("/"),
53                detail: "The value of this keyword MUST be an object.".to_string(),
54            });
55        }
56
57        let deps = deps.as_object().unwrap();
58        let mut items = vec![];
59        for (key, item) in deps.iter() {
60            if (item.is_object() || item.is_boolean()) && mode == DepsMode::DependentSchemas {
61                items.push((
62                    key.clone(),
63                    validators::dependencies::DepKind::Schema(helpers::alter_fragment_path(
64                        ctx.url.clone(),
65                        [
66                            ctx.escaped_fragment().as_ref(),
67                            deps_key,
68                            helpers::encode(key).as_ref(),
69                        ]
70                        .join("/"),
71                    )),
72                ));
73            } else if item.is_array() && mode == DepsMode::DependentRequired {
74                let item = item.as_array().unwrap();
75                let mut keys = vec![];
76                for key in item.iter() {
77                    if key.is_string() {
78                        keys.push(key.as_str().unwrap().to_string())
79                    } else {
80                        return Err(schema::SchemaError::Malformed {
81                            path: ctx.fragment.join("/"),
82                            detail: "Each element MUST be a string, and elements in the array MUST be unique.".to_string()
83                        });
84                    }
85                }
86                items.push((
87                    key.clone(),
88                    validators::dependencies::DepKind::Property(keys),
89                ));
90            } else {
91                return Err(schema::SchemaError::Malformed {
92                    path: ctx.fragment.join("/"),
93                    detail: mode.get_error(),
94                });
95            }
96        }
97
98        Ok(items)
99    }
100}
101
102impl super::Keyword for Dependencies {
103    fn compile(&self, def: &Value, ctx: &schema::WalkContext<'_>) -> super::KeywordResult {
104        let items = if let Some(deps) = def.get("dependencies") {
105            self.extract_dependencies(deps, ctx, "dependencies", DepsMode::AllowAny)?
106        } else {
107            let required = def.get("dependentRequired");
108            let schemas = def.get("dependentSchemas");
109
110            if required.is_none() && schemas.is_none() {
111                return Err(schema::SchemaError::Malformed {
112                    path: ctx.fragment.join("/"),
113                    detail: "dependencies has changed to dependentRequired and dependentSchemas in Draft 2019-09.".to_string(),
114                });
115            }
116
117            let mut items = vec![];
118            items.extend(
119                required
120                    .map(|v| {
121                        self.extract_dependencies(
122                            v,
123                            ctx,
124                            "dependentRequired",
125                            DepsMode::DependentRequired,
126                        )
127                    })
128                    .transpose()?
129                    .into_iter()
130                    .flatten(),
131            );
132            items.extend(
133                schemas
134                    .map(|v| {
135                        self.extract_dependencies(
136                            v,
137                            ctx,
138                            "dependentSchemas",
139                            DepsMode::DependentSchemas,
140                        )
141                    })
142                    .transpose()?
143                    .into_iter()
144                    .flatten(),
145            );
146            items
147        };
148
149        Ok(Some(Box::new(validators::Dependencies { items })))
150    }
151}
152
153#[cfg(test)]
154use super::super::builder;
155#[cfg(test)]
156use super::super::scope;
157
158#[cfg(test)]
159fn mk_schema() -> Value {
160    json!({
161        "dependencies": {
162            "x": {
163                "properties": {
164                    "y": {
165                        "type": "string",
166                        "default": "buh"
167                    },
168                }
169            }
170        }
171    })
172}
173
174#[test]
175fn no_default_for_schema() {
176    let mut scope = scope::Scope::new().supply_defaults();
177    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
178    assert_eq!(schema.get_default(), None);
179}
180
181#[test]
182fn default_when_needed() {
183    let mut scope = scope::Scope::new().supply_defaults();
184    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
185    let result = schema.validate(&json!({"x": 12}));
186    assert!(result.is_strictly_valid());
187    assert_eq!(result.replacement, Some(json!({"x": 12, "y": "buh"})));
188}
189
190#[test]
191fn no_default_otherwise() {
192    let mut scope = scope::Scope::new().supply_defaults();
193    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
194    let result = schema.validate(&json!({"x": 12, "y": "a"}));
195    assert!(result.is_strictly_valid());
196    assert_eq!(result.replacement, None);
197}
198
199#[test]
200fn no_default_otherwise2() {
201    let mut scope = scope::Scope::new().supply_defaults();
202    let schema = scope.compile_and_return(mk_schema(), true).unwrap();
203    let result = schema.validate(&json!(12));
204    assert!(result.is_strictly_valid());
205    assert_eq!(result.replacement, None);
206}
207
208#[test]
209fn validate_dependencies() {
210    let mut scope = scope::Scope::new();
211    let schema = scope
212        .compile_and_return(
213            builder::schema(|s| {
214                s.dependencies(|deps| {
215                    deps.schema("isbn", |isbn| {
216                        isbn.required(vec!["price".to_string()]);
217                        isbn.properties(|props| {
218                            props.insert("price", |price| {
219                                price.multiple_of(5f64);
220                            })
221                        })
222                    });
223                    deps.property("item_id", vec!["item_name".to_string()]);
224                });
225            })
226            .into_json(),
227            true,
228        )
229        .ok()
230        .unwrap();
231
232    assert_eq!(
233        schema
234            .validate(
235                &jsonway::object(|obj| {
236                    obj.set("isbn", "some_isbn".to_string());
237                })
238                .unwrap()
239            )
240            .is_valid(),
241        false
242    );
243
244    assert_eq!(
245        schema
246            .validate(
247                &jsonway::object(|obj| {
248                    obj.set("isbn", "some_isbn".to_string());
249                    obj.set("price", 773);
250                })
251                .unwrap()
252            )
253            .is_valid(),
254        false
255    );
256
257    assert_eq!(
258        schema
259            .validate(
260                &jsonway::object(|obj| {
261                    obj.set("isbn", "some_isbn".to_string());
262                    obj.set("price", 775);
263                })
264                .unwrap()
265            )
266            .is_valid(),
267        true
268    );
269
270    assert_eq!(
271        schema
272            .validate(
273                &jsonway::object(|obj| {
274                    obj.set("item_id", "some_id".to_string());
275                })
276                .unwrap()
277            )
278            .is_valid(),
279        false
280    );
281
282    assert_eq!(
283        schema
284            .validate(
285                &jsonway::object(|obj| {
286                    obj.set("item_id", "some_id".to_string());
287                    obj.set("item_name", "some_name".to_string());
288                })
289                .unwrap()
290            )
291            .is_valid(),
292        true
293    );
294}
295
296#[test]
297fn malformed() {
298    let mut scope = scope::Scope::new();
299
300    assert!(scope
301        .compile_and_return(
302            jsonway::object(|schema| {
303                schema.object("dependencies", |deps| {
304                    deps.set("isbn", 10);
305                });
306            })
307            .unwrap(),
308            true
309        )
310        .is_err());
311
312    assert!(scope
313        .compile_and_return(
314            jsonway::object(|schema| {
315                schema.object("dependencies", |deps| {
316                    deps.array("item_id", |item_id| item_id.push(10));
317                });
318            })
319            .unwrap(),
320            true
321        )
322        .is_err());
323}