yaml_schema/validation/
objects.rs1use hashlink::LinkedHashMap;
3use log::{debug, error};
4
5use crate::BoolOrTypedSchema;
6use crate::Error;
7use crate::ObjectSchema;
8use crate::Result;
9use crate::Validator;
10use crate::YamlSchema;
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: &BoolOrTypedSchema,
59) -> Result<bool> {
60 let sub_context = context.append_path(key);
61
62 match additional_properties {
63 BoolOrTypedSchema::Boolean(true) => { }
65 BoolOrTypedSchema::Boolean(false) => {
67 context.add_error(
68 value,
69 format!("Additional property '{key}' is not allowed!"),
70 );
71 return Ok(false);
73 }
74 BoolOrTypedSchema::TypedSchema(schema) => {
76 schema.validate(&sub_context, value)?;
77 }
78 BoolOrTypedSchema::Reference(reference) => {
79 let Some(root) = &context.root_schema else {
81 context.add_error(
82 value,
83 "No root schema was provided to look up references".to_string(),
84 );
85 return Ok(false);
86 };
87 let Some(def) = root.get_def(&reference.ref_name) else {
88 context.add_error(
89 value,
90 format!("No definition for {} found", reference.ref_name),
91 );
92 return Ok(false);
93 };
94
95 def.validate(context, value)?;
96 }
97 }
98 Ok(true)
99}
100
101impl ObjectSchema {
102 fn validate_object_mapping<'a>(
103 &self,
104 context: &Context,
105 object: &saphyr::MarkedYaml,
106 mapping: &saphyr::AnnotatedMapping<'a, saphyr::MarkedYaml<'a>>,
107 ) -> Result<()> {
108 for (k, value) in mapping {
109 let key_string = match &k.data {
110 saphyr::YamlData::Value(scalar) => scalar_to_string(scalar),
111 v => {
112 return Err(expected_scalar!(
113 "[{}] Expected a scalar key, got: {:?}",
114 format_marker(&k.span.start),
115 v
116 ));
117 }
118 };
119 let span = &k.span;
120 debug!("validate_object_mapping: key: \"{key_string}\"");
121 debug!(
122 "validate_object_mapping: span.start: {:?}",
123 format_marker(&span.start)
124 );
125 debug!(
126 "validate_object_mapping: span.end: {:?}",
127 format_marker(&span.end)
128 );
129 if let Some(properties) = &self.properties
131 && try_validate_value_against_properties(context, &key_string, value, properties)?
132 {
133 continue;
134 }
135
136 if let Some(additional_properties) = &self.additional_properties {
138 try_validate_value_against_additional_properties(
139 context,
140 &key_string,
141 value,
142 additional_properties,
143 )?;
144 }
145
146 if let Some(pattern_properties) = &self.pattern_properties {
148 for (pattern, schema) in pattern_properties {
149 log::debug!("pattern: {pattern}");
150 let re = regex::Regex::new(pattern).map_err(|e| {
152 Error::GenericError(format!("Invalid regular expression pattern: {e}"))
153 })?;
154 if re.is_match(key_string.as_ref()) {
155 schema.validate(context, value)?;
156 }
157 }
158 }
159 if let Some(property_names) = &self.property_names {
161 if let Some(re) = &property_names.pattern {
162 debug!("Regex for property names: {}", re.as_str());
163 if !re.is_match(key_string.as_ref()) {
164 context.add_error(
165 k,
166 format!(
167 "Property name '{}' does not match pattern '{}'",
168 key_string,
169 re.as_str()
170 ),
171 );
172 fail_fast!(context)
173 }
174 } else {
175 return Err(Error::GenericError(
176 "Expected a pattern for `property_names`".to_string(),
177 ));
178 }
179 }
180 }
181 if let Some(any_of) = &self.any_of {
183 any_of.validate(context, object)?;
184 }
185
186 if let Some(required) = &self.required {
188 for required_property in required {
189 if !mapping
190 .keys()
191 .map(|k| k.data.as_str().unwrap())
192 .any(|s| s == required_property)
193 {
194 context.add_error(
195 object,
196 format!("Required property '{required_property}' is missing!"),
197 );
198 fail_fast!(context)
199 }
200 }
201 }
202
203 if let Some(min_properties) = &self.min_properties
205 && mapping.len() < *min_properties
206 {
207 context.add_error(
208 object,
209 format!("Object has too few properties! Minimum is {min_properties}!"),
210 );
211 fail_fast!(context)
212 }
213 if let Some(max_properties) = &self.max_properties
215 && mapping.len() > *max_properties
216 {
217 context.add_error(
218 object,
219 format!("Object has too many properties! Maximum is {max_properties}!"),
220 );
221 fail_fast!(context)
222 }
223
224 Ok(())
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use crate::NumberSchema;
231 use crate::RootSchema;
232 use crate::Schema;
233 use crate::StringSchema;
234 use crate::engine;
235 use hashlink::LinkedHashMap;
236
237 use super::*;
238
239 #[test]
240 fn test_should_validate_properties() {
241 let mut properties = LinkedHashMap::new();
242 properties.insert(
243 "foo".to_string(),
244 YamlSchema::from(Schema::typed_string(StringSchema::default())),
245 );
246 properties.insert(
247 "bar".to_string(),
248 YamlSchema::from(Schema::typed_number(NumberSchema::default())),
249 );
250 let object_schema = ObjectSchema {
251 properties: Some(properties),
252 ..Default::default()
253 };
254 let root_schema = RootSchema::new_with_schema(Schema::typed_object(object_schema));
255 let value = r#"
256 foo: "I'm a string"
257 bar: 42
258 "#;
259 let result = engine::Engine::evaluate(&root_schema, value, true);
260 assert!(result.is_ok());
261
262 let value2 = r#"
263 foo: 42
264 baz: "I'm a string"
265 "#;
266 let context = engine::Engine::evaluate(&root_schema, value2, true).unwrap();
267 assert!(context.has_errors());
268 let errors = context.errors.borrow();
269 let first_error = errors.first().unwrap();
270 assert_eq!(first_error.path, "foo");
271 assert_eq!(
272 first_error.error,
273 "Expected a string, but got: Value(Integer(42))"
274 );
275 }
276}