1use std::borrow::Cow;
3use std::collections::HashSet;
4
5use hashlink::LinkedHashMap;
6use log::debug;
7use saphyr::Scalar;
8use saphyr::YamlData;
9
10use crate::Result;
11use crate::Validator;
12use crate::YamlSchema;
13use crate::schemas::BooleanOrSchema;
14use crate::schemas::ObjectSchema;
15use crate::utils::{format_marker, format_yaml_data, scalar_to_string};
16use crate::validation::Context;
17
18impl Validator for ObjectSchema {
19 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
21 let data = &value.data;
22 debug!("Validating object: {}", format_yaml_data(data));
23 if let saphyr::YamlData::Mapping(mapping) = data {
24 self.validate_object_mapping(context, value, mapping)
25 } else {
26 let error_message = format!(
27 "[ObjectSchema] {} Expected an object, but got: {data:?}",
28 format_marker(&value.span.start)
29 );
30 debug!("{error_message}");
31 context.add_error(value, error_message);
32 Ok(())
33 }
34 }
35}
36
37pub fn try_validate_value_against_properties(
38 context: &Context,
39 key: &String,
40 value: &saphyr::MarkedYaml,
41 properties: &LinkedHashMap<String, YamlSchema>,
42) -> Result<bool> {
43 let sub_context = context.append_path(key);
44 if let Some(schema) = properties.get(key) {
45 debug!("Validating property '{key}' with schema: {schema}");
46 let err_before = context.errors.borrow().len();
47 let result = schema.validate(&sub_context, value);
48 return match result {
49 Ok(()) => {
50 if context.errors.borrow().len() == err_before {
51 context.record_evaluated_property(key);
52 }
53 Ok(true)
54 }
55 Err(e) => Err(e),
56 };
57 }
58 Ok(false)
59}
60
61pub fn try_validate_value_against_additional_properties(
65 context: &Context,
66 key: &String,
67 value: &saphyr::MarkedYaml,
68 additional_properties: &BooleanOrSchema,
69) -> Result<bool> {
70 let sub_context = context.append_path(key);
71
72 match additional_properties {
73 BooleanOrSchema::Boolean(true) => { }
75 BooleanOrSchema::Boolean(false) => {
77 context.add_error(
78 value,
79 format!("Additional property '{key}' is not allowed!"),
80 );
81 return Ok(false);
83 }
84 BooleanOrSchema::Schema(schema) => {
86 schema.validate(&sub_context, value)?;
87 }
88 }
89 Ok(true)
90}
91
92impl ObjectSchema {
93 fn validate_object_mapping<'r>(
94 &self,
95 context: &Context<'r>,
96 object: &saphyr::MarkedYaml,
97 mapping: &saphyr::AnnotatedMapping<'r, saphyr::MarkedYaml<'r>>,
98 ) -> Result<()> {
99 for (k, value) in mapping {
100 let key_string = match &k.data {
101 saphyr::YamlData::Value(scalar) => scalar_to_string(scalar),
102 v => {
103 return Err(expected_scalar!(
104 "[{}] Expected a scalar key, got: {:?}",
105 format_marker(&k.span.start),
106 v
107 ));
108 }
109 };
110 let span = &k.span;
111 debug!("validate_object_mapping: key: \"{key_string}\"");
112 debug!(
113 "validate_object_mapping: span.start: {:?}",
114 format_marker(&span.start)
115 );
116 debug!(
117 "validate_object_mapping: span.end: {:?}",
118 format_marker(&span.end)
119 );
120
121 if key_string == "$schema" {
124 continue;
125 }
126
127 let covered_by_properties = if let Some(properties) = &self.properties {
129 try_validate_value_against_properties(context, &key_string, value, properties)?
130 } else {
131 false
132 };
133
134 let mut matched_pattern_property = false;
135 if let Some(pattern_properties) = &self.pattern_properties {
136 let pattern_context = context.append_path(&key_string);
137 let err_before_patterns = context.errors.borrow().len();
138 for pp in pattern_properties {
139 log::debug!("pattern: {}", pp.regex.as_str());
140 if pp.regex.is_match(key_string.as_ref()) {
141 matched_pattern_property = true;
142 pp.schema.validate(&pattern_context, value)?;
143 }
144 }
145 if matched_pattern_property && context.errors.borrow().len() == err_before_patterns
146 {
147 context.record_evaluated_property(&key_string);
148 }
149 }
150
151 if !covered_by_properties
154 && !matched_pattern_property
155 && let Some(additional_properties) = &self.additional_properties
156 {
157 let err_before_add = context.errors.borrow().len();
158 let passed = try_validate_value_against_additional_properties(
159 context,
160 &key_string,
161 value,
162 additional_properties,
163 )?;
164 if context.errors.borrow().len() == err_before_add {
165 context.record_evaluated_property(&key_string);
166 }
167 if !passed {
168 fail_fast!(context)
169 }
170 }
171 if let Some(property_names) = &self.property_names {
173 let names_context = context.append_path(&key_string);
174 let key_to_validate = if property_names_validates_string_projection(property_names)
175 {
176 string_projection_of_key(k, &key_string)
177 } else {
178 k.clone()
179 };
180 property_names.validate(&names_context, &key_to_validate)?;
181 }
182 }
183
184 if let Some(required) = &self.required {
186 for required_property in required {
187 if !mapping
188 .keys()
189 .filter_map(|k| k.data.as_str())
190 .any(|s| s == required_property)
191 {
192 context.add_error(
193 object,
194 format!("Required property '{required_property}' is missing!"),
195 );
196 fail_fast!(context)
197 }
198 }
199 }
200
201 if let Some(min_properties) = &self.min_properties
203 && mapping.len() < *min_properties
204 {
205 context.add_error(
206 object,
207 format!("Object has too few properties! Minimum is {min_properties}!"),
208 );
209 fail_fast!(context)
210 }
211 if let Some(max_properties) = &self.max_properties
213 && mapping.len() > *max_properties
214 {
215 context.add_error(
216 object,
217 format!("Object has too many properties! Maximum is {max_properties}!"),
218 );
219 fail_fast!(context)
220 }
221
222 if self.dependent_required.is_some() || self.dependent_schemas.is_some() {
224 let keys = Self::instance_property_keys(mapping)?;
225 if let Some(dr) = &self.dependent_required {
226 for (trigger, deps) in dr {
227 if keys.contains(trigger) {
228 for dep in deps {
229 if !keys.contains(dep) {
230 context.add_error(
231 object,
232 format!(
233 "{} When property '{}' is present, property '{}' is required by dependentRequired",
234 format_marker(&object.span.start),
235 trigger,
236 dep
237 ),
238 );
239 fail_fast!(context)
240 }
241 }
242 }
243 }
244 }
245 if let Some(ds) = &self.dependent_schemas {
246 for (trigger, subschema) in ds {
247 if keys.contains(trigger) {
248 subschema.validate(context, object)?;
249 }
250 }
251 }
252 }
253
254 Ok(())
255 }
256
257 fn instance_property_keys<'r>(
259 mapping: &saphyr::AnnotatedMapping<'r, saphyr::MarkedYaml<'r>>,
260 ) -> Result<HashSet<String>> {
261 let mut keys = HashSet::new();
262 for (k, _) in mapping {
263 let key_string = match &k.data {
264 saphyr::YamlData::Value(scalar) => scalar_to_string(scalar),
265 v => {
266 return Err(expected_scalar!(
267 "[{}] Expected a scalar key, got: {:?}",
268 format_marker(&k.span.start),
269 v
270 ));
271 }
272 };
273 keys.insert(key_string);
274 }
275 Ok(keys)
276 }
277}
278
279fn property_names_validates_string_projection(schema: &YamlSchema) -> bool {
281 if let YamlSchema::Subschema(subschema) = schema {
282 if subschema.one_of.is_some() || subschema.any_of.is_some() || subschema.all_of.is_some() {
285 return false;
286 }
287
288 subschema.r#type.is_none_or_string()
289 } else {
290 true
291 }
292}
293
294fn string_projection_of_key<'r>(
296 key: &saphyr::MarkedYaml<'r>,
297 key_string: &str,
298) -> saphyr::MarkedYaml<'r> {
299 let mut projected = key.clone();
300 projected.data = YamlData::Value(Scalar::String(Cow::Owned(key_string.to_string())));
301 projected
302}
303
304#[cfg(test)]
305mod tests {
306 use crate::RootSchema;
307 use crate::YamlSchema;
308 use crate::engine;
309 use crate::loader;
310 use crate::schemas::NumberSchema;
311 use crate::schemas::StringSchema;
312 use hashlink::LinkedHashMap;
313
314 use super::*;
315
316 #[test]
317 fn test_should_validate_properties() {
318 let mut properties = LinkedHashMap::new();
319 properties.insert(
320 "foo".to_string(),
321 YamlSchema::typed_string(StringSchema::default()),
322 );
323 properties.insert(
324 "bar".to_string(),
325 YamlSchema::typed_number(NumberSchema::default()),
326 );
327 let object_schema = ObjectSchema {
328 properties: Some(properties),
329 ..Default::default()
330 };
331 let root_schema = RootSchema::new(YamlSchema::typed_object(object_schema));
332 let value = r#"
333 foo: "I'm a string"
334 bar: 42
335 "#;
336 let result = engine::Engine::evaluate(&root_schema, value, true);
337 assert!(result.is_ok());
338
339 let value2 = r#"
340 foo: 42
341 baz: "I'm a string"
342 "#;
343 let context = engine::Engine::evaluate(&root_schema, value2, true).unwrap();
344 assert!(context.has_errors());
345 let errors = context.errors.borrow();
346 let first_error = errors.first().unwrap();
347 assert_eq!(first_error.path, "foo");
348 assert_eq!(first_error.error, "Expected a string, but got: 42 (int)");
349 }
350
351 #[test]
352 fn dependent_required_validation() {
353 let yaml = r#"
354 type: object
355 dependentRequired:
356 credit_card:
357 - billing_address
358 properties:
359 credit_card:
360 type: string
361 billing_address:
362 type: string
363 "#;
364 let root_schema = loader::load_from_str(yaml).unwrap();
365 let ok = engine::Engine::evaluate(
366 &root_schema,
367 "credit_card: \"4111\"\nbilling_address: \"1 Main\"",
368 false,
369 )
370 .unwrap();
371 assert!(!ok.has_errors());
372
373 let bad = engine::Engine::evaluate(&root_schema, "credit_card: \"4111\"", false).unwrap();
374 assert!(bad.has_errors());
375 }
376
377 #[test]
378 fn dependent_schemas_validation() {
379 let yaml = r#"
380 type: object
381 dependentSchemas:
382 credit_card:
383 type: object
384 required:
385 - billing_address
386 properties:
387 credit_card:
388 type: string
389 billing_address:
390 type: string
391 "#;
392 let root_schema = loader::load_from_str(yaml).unwrap();
393 let ok = engine::Engine::evaluate(
394 &root_schema,
395 "credit_card: \"4111\"\nbilling_address: \"1 Main\"",
396 false,
397 )
398 .unwrap();
399 assert!(!ok.has_errors());
400
401 let bad = engine::Engine::evaluate(&root_schema, "credit_card: \"4111\"", false).unwrap();
402 assert!(bad.has_errors());
403 }
404}