valico/json_schema/keywords/
dependencies.rs1use 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}