yaml_schema/validation/
objects.rs1use hashlink::LinkedHashMap;
3use log::{debug, error};
4
5use crate::Error;
6use crate::Result;
7use crate::Validator;
8use crate::YamlSchema;
9use crate::schemas::BooleanOrSchema;
10use crate::schemas::ObjectSchema;
11use crate::utils::{format_marker, format_yaml_data, scalar_to_string};
12use crate::validation::Context;
13
14impl Validator for ObjectSchema<'_> {
15 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
17 let data = &value.data;
18 debug!("Validating object: {}", format_yaml_data(data));
19 if let saphyr::YamlData::Mapping(mapping) = data {
20 self.validate_object_mapping(context, value, mapping)
21 } else {
22 let error_message = format!(
23 "[ObjectSchema] {} Expected an object, but got: {data:?}",
24 format_marker(&value.span.start)
25 );
26 error!("{error_message}");
27 context.add_error(value, error_message);
28 Ok(())
29 }
30 }
31}
32
33pub fn try_validate_value_against_properties(
34 context: &Context,
35 key: &String,
36 value: &saphyr::MarkedYaml,
37 properties: &LinkedHashMap<String, YamlSchema<'_>>,
38) -> Result<bool> {
39 let sub_context = context.append_path(key);
40 if let Some(schema) = properties.get(key) {
41 debug!("Validating property '{key}' with schema: {schema}");
42 let result = schema.validate(&sub_context, value);
43 return match result {
44 Ok(_) => Ok(true),
45 Err(e) => Err(e),
46 };
47 }
48 Ok(false)
49}
50
51pub fn try_validate_value_against_additional_properties(
55 context: &Context,
56 key: &String,
57 value: &saphyr::MarkedYaml,
58 additional_properties: &BooleanOrSchema,
59) -> Result<bool> {
60 let sub_context = context.append_path(key);
61
62 match additional_properties {
63 BooleanOrSchema::Boolean(true) => { }
65 BooleanOrSchema::Boolean(false) => {
67 context.add_error(
68 value,
69 format!("Additional property '{key}' is not allowed!"),
70 );
71 return Ok(false);
73 }
74 BooleanOrSchema::Schema(schema) => {
76 schema.validate(&sub_context, value)?;
77 }
78 }
79 Ok(true)
80}
81
82impl ObjectSchema<'_> {
83 fn validate_object_mapping<'r>(
84 &self,
85 context: &Context<'r>,
86 object: &saphyr::MarkedYaml,
87 mapping: &saphyr::AnnotatedMapping<'r, saphyr::MarkedYaml<'r>>,
88 ) -> Result<()> {
89 for (k, value) in mapping {
90 let key_string = match &k.data {
91 saphyr::YamlData::Value(scalar) => scalar_to_string(scalar),
92 v => {
93 return Err(expected_scalar!(
94 "[{}] Expected a scalar key, got: {:?}",
95 format_marker(&k.span.start),
96 v
97 ));
98 }
99 };
100 let span = &k.span;
101 debug!("validate_object_mapping: key: \"{key_string}\"");
102 debug!(
103 "validate_object_mapping: span.start: {:?}",
104 format_marker(&span.start)
105 );
106 debug!(
107 "validate_object_mapping: span.end: {:?}",
108 format_marker(&span.end)
109 );
110
111 if key_string == "$schema" {
114 continue;
115 }
116
117 if let Some(properties) = &self.properties
119 && try_validate_value_against_properties(context, &key_string, value, properties)?
120 {
121 continue;
122 }
123
124 let mut matched_pattern_property = false;
125 if let Some(pattern_properties) = &self.pattern_properties {
126 for pp in pattern_properties {
127 log::debug!("pattern: {}", pp.regex.as_str());
128 if pp.regex.is_match(key_string.as_ref()) {
129 matched_pattern_property = true;
130 pp.schema.validate(context, value)?;
131 }
132 }
133 }
134
135 if !matched_pattern_property
138 && let Some(additional_properties) = &self.additional_properties
139 {
140 try_validate_value_against_additional_properties(
141 context,
142 &key_string,
143 value,
144 additional_properties,
145 )?;
146 }
147 if let Some(property_names) = &self.property_names {
149 if let Some(re) = &property_names.pattern {
150 debug!("Regex for property names: {}", re.as_str());
151 if !re.is_match(key_string.as_ref()) {
152 context.add_error(
153 k,
154 format!(
155 "Property name '{}' does not match pattern '{}'",
156 key_string,
157 re.as_str()
158 ),
159 );
160 fail_fast!(context)
161 }
162 } else {
163 return Err(Error::GenericError(
164 "Expected a pattern for `property_names`".to_string(),
165 ));
166 }
167 }
168 }
169
170 if let Some(required) = &self.required {
172 for required_property in required {
173 if !mapping
174 .keys()
175 .filter_map(|k| k.data.as_str())
176 .any(|s| s == required_property)
177 {
178 context.add_error(
179 object,
180 format!("Required property '{required_property}' is missing!"),
181 );
182 fail_fast!(context)
183 }
184 }
185 }
186
187 if let Some(min_properties) = &self.min_properties
189 && mapping.len() < *min_properties
190 {
191 context.add_error(
192 object,
193 format!("Object has too few properties! Minimum is {min_properties}!"),
194 );
195 fail_fast!(context)
196 }
197 if let Some(max_properties) = &self.max_properties
199 && mapping.len() > *max_properties
200 {
201 context.add_error(
202 object,
203 format!("Object has too many properties! Maximum is {max_properties}!"),
204 );
205 fail_fast!(context)
206 }
207
208 Ok(())
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::RootSchema;
215 use crate::YamlSchema;
216 use crate::engine;
217 use crate::schemas::NumberSchema;
218 use crate::schemas::StringSchema;
219 use hashlink::LinkedHashMap;
220
221 use super::*;
222
223 #[test]
224 fn test_should_validate_properties() {
225 let mut properties = LinkedHashMap::new();
226 properties.insert(
227 "foo".to_string(),
228 YamlSchema::typed_string(StringSchema::default()),
229 );
230 properties.insert(
231 "bar".to_string(),
232 YamlSchema::typed_number(NumberSchema::default()),
233 );
234 let object_schema = ObjectSchema {
235 properties: Some(properties),
236 ..Default::default()
237 };
238 let root_schema = RootSchema::new(YamlSchema::typed_object(object_schema));
239 let value = r#"
240 foo: "I'm a string"
241 bar: 42
242 "#;
243 let result = engine::Engine::evaluate(&root_schema, value, true);
244 assert!(result.is_ok());
245
246 let value2 = r#"
247 foo: 42
248 baz: "I'm a string"
249 "#;
250 let context = engine::Engine::evaluate(&root_schema, value2, true).unwrap();
251 assert!(context.has_errors());
252 let errors = context.errors.borrow();
253 let first_error = errors.first().unwrap();
254 assert_eq!(first_error.path, "foo");
255 assert_eq!(
256 first_error.error,
257 "Expected a string, but got: Value(Integer(42))"
258 );
259 }
260}