1use std::collections::HashMap;
2
3use hashlink::LinkedHashMap;
4use log::debug;
5use regex::Regex;
6use saphyr::AnnotatedMapping;
7use saphyr::MarkedYaml;
8use saphyr::Scalar;
9use saphyr::YamlData;
10
11use crate::AnyOfSchema;
12use crate::BoolOrTypedSchema;
13use crate::Error;
14use crate::Reference;
15use crate::Result;
16use crate::StringSchema;
17use crate::TypedSchema;
18use crate::YamlSchema;
19use crate::loader::load_array_of_schemas_marked;
20use crate::loader::load_integer_marked;
21use crate::schemas::BaseSchema;
22use crate::schemas::SchemaMetadata;
23use crate::utils::format_marker;
24use crate::utils::hash_map;
25use crate::utils::linked_hash_map;
26
27#[derive(Debug, Default, PartialEq)]
29pub struct ObjectSchema {
30 pub base: BaseSchema,
31 pub metadata: Option<HashMap<String, String>>,
32 pub properties: Option<LinkedHashMap<String, YamlSchema>>,
33 pub required: Option<Vec<String>>,
34 pub additional_properties: Option<BoolOrTypedSchema>,
35 pub pattern_properties: Option<LinkedHashMap<String, YamlSchema>>,
36 pub property_names: Option<StringSchema>,
37 pub min_properties: Option<usize>,
38 pub max_properties: Option<usize>,
39 pub any_of: Option<AnyOfSchema>,
40}
41
42impl SchemaMetadata for ObjectSchema {
43 fn get_accepted_keys() -> &'static [&'static str] {
44 &[
45 "properties",
46 "required",
47 "additionalProperties",
48 "patternProperties",
49 "propertyNames",
50 "minProperties",
51 "maxProperties",
52 "anyOf",
53 ]
54 }
55}
56
57impl ObjectSchema {
58 pub fn from_base(base: BaseSchema) -> Self {
59 Self {
60 base,
61 ..Default::default()
62 }
63 }
64
65 pub fn builder() -> ObjectSchemaBuilder {
66 ObjectSchemaBuilder::new()
67 }
68}
69
70impl TryFrom<&MarkedYaml<'_>> for ObjectSchema {
71 type Error = crate::Error;
72
73 fn try_from(marked_yaml: &MarkedYaml<'_>) -> Result<Self> {
74 debug!("[ObjectSchema]: TryFrom {marked_yaml:?}");
75 if let YamlData::Mapping(mapping) = &marked_yaml.data {
76 Ok(ObjectSchema::try_from(mapping)?)
77 } else {
78 Err(expected_mapping!(marked_yaml))
79 }
80 }
81}
82
83impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for ObjectSchema {
84 type Error = crate::Error;
85
86 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
87 let mut object_schema = ObjectSchema::from_base(BaseSchema::try_from(mapping)?);
88 for (key, value) in mapping.iter() {
89 if let YamlData::Value(Scalar::String(s)) = &key.data {
90 if object_schema.base.handle_key_value(s, value)?.is_none() {
91 match s.as_ref() {
92 "properties" => {
93 let properties = load_properties_marked(value)?;
94 object_schema.properties = Some(properties);
95 }
96 "additionalProperties" => {
97 let additional_properties = load_additional_properties_marked(value)?;
98 object_schema.additional_properties = Some(additional_properties);
99 }
100 "minProperties" => {
101 object_schema.min_properties =
102 Some(load_integer_marked(value)? as usize);
103 }
104 "maxProperties" => {
105 object_schema.max_properties =
106 Some(load_integer_marked(value)? as usize);
107 }
108 "patternProperties" => {
109 let pattern_properties = load_properties_marked(value)?;
110 object_schema.pattern_properties = Some(pattern_properties);
111 }
112 "propertyNames" => {
113 if let YamlData::Mapping(mapping) = &value.data {
114 let pattern_key = MarkedYaml::value_from_str("pattern");
115 if !mapping.contains_key(&pattern_key) {
116 return Err(generic_error!(
117 "{} propertyNames: Missing required key: pattern",
118 format_marker(&value.span.start)
119 ));
120 }
121 if let YamlData::Value(Scalar::String(pattern)) =
122 &mapping.get(&pattern_key).unwrap().data
123 {
124 let regex = Regex::new(pattern.as_ref()).map_err(|_e| {
125 Error::InvalidRegularExpression(pattern.to_string())
126 })?;
127 object_schema.property_names =
128 Some(StringSchema::builder().pattern(regex).build());
129 }
130 } else {
131 return Err(unsupported_type!(
132 "propertyNames: Expected a mapping, but got: {:?}",
133 value
134 ));
135 }
136 }
137 "anyOf" => {
138 let any_of = load_array_of_schemas_marked(value)?;
139 let any_of_schema = AnyOfSchema { any_of };
140 object_schema.any_of = Some(any_of_schema);
141 }
142 "required" => {
143 if let YamlData::Sequence(values) = &value.data {
144 let required = values
145 .iter()
146 .map(|v| {
147 if let YamlData::Value(Scalar::String(s)) = &v.data {
148 Ok(s.to_string())
149 } else {
150 Err(generic_error!(
151 "{} Expected a string, got {:?}",
152 format_marker(&v.span.start),
153 v
154 ))
155 }
156 })
157 .collect::<Result<Vec<String>>>()?;
158 object_schema.required = Some(required);
159 } else {
160 return Err(unsupported_type!(
161 "required: Expected an array, but got: {:?}",
162 value
163 ));
164 }
165 }
166 "type" => {
168 if let YamlData::Value(Scalar::String(s)) = &value.data {
169 if s != "object" {
170 return Err(unsupported_type!(
171 "Expected type: object, but got: {}",
172 s
173 ));
174 }
175 } else {
176 return Err(expected_type_is_string!(value));
177 }
178 }
179 _ => {
180 if s.starts_with("$") {
181 if let YamlData::Value(Scalar::String(value)) = &key.data {
182 if object_schema.metadata.is_none() {
183 object_schema.metadata = Some(HashMap::new());
184 }
185 object_schema
186 .metadata
187 .as_mut()
188 .unwrap()
189 .insert(s.to_string(), value.to_string());
190 } else {
191 return Err(generic_error!(
192 "{} Expected a string value but got {:?}",
193 format_marker(&value.span.start),
194 value.data
195 ));
196 }
197 } else {
198 unimplemented!("Unsupported key for type: object: {}", s);
199 }
200 }
201 }
202 }
203 } else {
204 return Err(generic_error!(
205 "{} Expected a scalar key, got: {:#?}",
206 format_marker(&key.span.start),
207 key
208 ));
209 }
210 }
211 Ok(object_schema)
212 }
213}
214
215fn load_properties_marked(value: &MarkedYaml) -> Result<LinkedHashMap<String, YamlSchema>> {
216 if let YamlData::Mapping(mapping) = &value.data {
217 let mut properties = LinkedHashMap::new();
218 for (key, value) in mapping.iter() {
219 if let YamlData::Value(Scalar::String(key)) = &key.data {
220 if key.as_ref() == "$ref" {
221 let reference: Reference = value.try_into()?;
222 properties.insert(key.to_string(), YamlSchema::reference(reference));
223 } else if value.data.is_mapping() {
224 let schema: YamlSchema = value.try_into()?;
225 properties.insert(key.to_string(), schema);
226 } else {
227 return Err(generic_error!(
228 "properties: Expected a mapping for \"{}\", but got: {:?}",
229 key,
230 value
231 ));
232 }
233 } else {
234 return Err(generic_error!(
235 "{} Expected a string key, but got: {:?}",
236 format_marker(&key.span.start),
237 key
238 ));
239 }
240 }
241 Ok(properties)
242 } else {
243 Err(generic_error!(
244 "{} properties: expected a mapping, but got: {:#?}",
245 format_marker(&value.span.start),
246 value
247 ))
248 }
249}
250
251fn load_additional_properties_marked(marked_yaml: &MarkedYaml) -> Result<BoolOrTypedSchema> {
252 match &marked_yaml.data {
253 YamlData::Value(scalar) => match scalar {
254 Scalar::Boolean(b) => Ok(BoolOrTypedSchema::Boolean(*b)),
255 _ => Err(generic_error!(
256 "{} Expected a boolean scalar, but got: {:#?}",
257 format_marker(&marked_yaml.span.start),
258 scalar
259 )),
260 },
261 YamlData::Mapping(mapping) => {
262 let ref_key = MarkedYaml::value_from_str("$ref");
263 if mapping.contains_key(&ref_key) {
264 Ok(BoolOrTypedSchema::Reference(marked_yaml.try_into()?))
265 } else {
266 let schema: TypedSchema = marked_yaml.try_into()?;
267 Ok(BoolOrTypedSchema::TypedSchema(Box::new(schema)))
268 }
269 }
270 _ => Err(unsupported_type!(
271 "Expected type: boolean or mapping, but got: {:?}",
272 marked_yaml
273 )),
274 }
275}
276
277impl std::fmt::Display for ObjectSchema {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 write!(f, "Object {self:?}")
280 }
281}
282
283pub struct ObjectSchemaBuilder(ObjectSchema);
284
285impl Default for ObjectSchemaBuilder {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291impl ObjectSchemaBuilder {
292 pub fn new() -> Self {
293 Self(ObjectSchema::default())
294 }
295
296 pub fn build(&mut self) -> ObjectSchema {
297 std::mem::take(&mut self.0)
298 }
299
300 pub fn boxed(&mut self) -> Box<ObjectSchema> {
301 Box::new(self.build())
302 }
303
304 pub fn metadata<K, V>(&mut self, key: K, value: V) -> &mut Self
305 where
306 K: Into<String>,
307 V: Into<String>,
308 {
309 if let Some(metadata) = self.0.metadata.as_mut() {
310 metadata.insert(key.into(), value.into());
311 } else {
312 self.0.metadata = Some(hash_map(key.into(), value.into()));
313 }
314 self
315 }
316
317 pub fn properties(&mut self, properties: LinkedHashMap<String, YamlSchema>) -> &mut Self {
318 self.0.properties = Some(properties);
319 self
320 }
321
322 pub fn property<K>(&mut self, key: K, value: YamlSchema) -> &mut Self
323 where
324 K: Into<String>,
325 {
326 if let Some(properties) = self.0.properties.as_mut() {
327 properties.insert(key.into(), value);
328 self
329 } else {
330 self.properties(linked_hash_map(key.into(), value))
331 }
332 }
333
334 pub fn require<S>(&mut self, property_name: S) -> &mut Self
335 where
336 S: Into<String>,
337 {
338 if let Some(required) = self.0.required.as_mut() {
339 required.push(property_name.into());
340 } else {
341 self.0.required = Some(vec![property_name.into()]);
342 }
343 self
344 }
345
346 pub fn additional_properties(&mut self, additional_properties: bool) -> &mut Self {
347 self.0.additional_properties = Some(BoolOrTypedSchema::Boolean(additional_properties));
348 self
349 }
350
351 pub fn additional_property_types(&mut self, typed_schema: TypedSchema) -> &mut Self {
352 self.0.additional_properties = Some(BoolOrTypedSchema::TypedSchema(Box::new(typed_schema)));
353 self
354 }
355
356 pub fn pattern_properties(
357 &mut self,
358 pattern_properties: LinkedHashMap<String, YamlSchema>,
359 ) -> &mut Self {
360 self.0.pattern_properties = Some(pattern_properties);
361 self
362 }
363
364 pub fn pattern_property<K>(&mut self, key: K, value: YamlSchema) -> &mut Self
365 where
366 K: Into<String>,
367 {
368 if let Some(pattern_properties) = self.0.pattern_properties.as_mut() {
369 pattern_properties.insert(key.into(), value);
370 self
371 } else {
372 self.pattern_properties(linked_hash_map(key.into(), value))
373 }
374 }
375
376 pub fn property_names(&mut self, property_names: StringSchema) -> &mut Self {
377 self.0.property_names = Some(property_names);
378 self
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::Validator;
386 use saphyr::LoadableYamlNode;
387
388 #[test]
389 fn test_builder_default() {
390 let schema = ObjectSchema::builder().build();
391 assert_eq!(ObjectSchema::default(), schema);
392 }
393
394 #[test]
395 fn test_builder_metadata() {
396 let schema = ObjectSchema::builder()
397 .metadata("description", "The description")
398 .build();
399 assert!(schema.metadata.is_some());
400 assert_eq!(
401 schema.metadata.unwrap().get("description").unwrap(),
402 "The description"
403 );
404 }
405
406 #[test]
407 fn test_builder_properties() {
408 let schema = ObjectSchema::builder()
409 .property("type", YamlSchema::ref_str("schema_type"))
410 .build();
411 assert!(schema.properties.is_some());
412 assert_eq!(
413 *schema.properties.unwrap().get("type").unwrap(),
414 YamlSchema::ref_str("schema_type")
415 );
416 }
417
418 #[test]
419 fn test_additional_properties_as_schema() {
420 let docs = MarkedYaml::load_from_str(
421 "
422 type: object
423 properties:
424 number:
425 type: number
426 street_name:
427 type: string
428 street_type:
429 enum: [Street, Avenue, Boulevard]
430 additionalProperties:
431 type: string",
432 )
433 .unwrap();
434
435 let doc = docs.first().unwrap();
436
437 let schema: ObjectSchema = doc.try_into().unwrap();
438
439 let yaml_docs = MarkedYaml::load_from_str(
440 "
441number: 1600
442street_name: Pennsylvania
443street_type: Avenue
444office_number: 201",
445 )
446 .unwrap();
447
448 let yaml = yaml_docs.first().unwrap();
449
450 let context = crate::Context::default();
451 let result = schema.validate(&context, yaml);
452 if result.is_err() {
453 println!("{:?}", result.as_ref().unwrap());
454 panic!("Validation failed: {:?}", result.as_ref().unwrap());
455 }
456 assert!(context.has_errors());
457 for error in context.errors.as_ref().borrow().iter() {
458 println!("{error:?}");
459 }
460 }
461
462 #[test]
463 fn test_object_schema_with_description() {
464 let yaml = r#"
465 type: object
466 description: The description
467 "#;
468 let doc = MarkedYaml::load_from_str(yaml).unwrap();
469 let marked_yaml = doc.first().unwrap();
470 let object_schema = ObjectSchema::try_from(marked_yaml).unwrap();
471 assert_eq!(
472 object_schema.base.description,
473 Some("The description".to_string())
474 );
475 }
476}