1use std::borrow::Cow;
2use std::fmt::Display;
3
4use hashlink::LinkedHashMap;
5use jsonptr::Token;
6use log::debug;
7use log::error;
8use saphyr::{AnnotatedMapping, MarkedYaml, Scalar, YamlData};
9
10use crate::ConstValue;
11use crate::Context;
12use crate::Error;
13use crate::Reference;
14use crate::Result;
15use crate::Validator;
16use crate::loader::marked_yaml_to_string;
17use crate::schemas::AllOfSchema;
18use crate::schemas::AnyOfSchema;
19use crate::schemas::ArraySchema;
20use crate::schemas::EnumSchema;
21use crate::schemas::IntegerSchema;
22use crate::schemas::NotSchema;
23use crate::schemas::NumberSchema;
24use crate::schemas::ObjectSchema;
25use crate::schemas::OneOfSchema;
26use crate::schemas::StringSchema;
27use crate::utils::format_annotated_mapping;
28use crate::utils::format_linked_hash_map;
29use crate::utils::format_marked_yaml;
30use crate::utils::format_marker;
31use crate::utils::format_scalar;
32use crate::utils::format_vec;
33use crate::utils::format_yaml_data;
34
35#[derive(Debug, PartialEq)]
37pub enum YamlSchema<'r> {
38 Empty, Null, BooleanLiteral(bool), Subschema(Box<Subschema<'r>>),
42}
43
44impl<'r> YamlSchema<'r> {
45 pub fn subschema(subschema: Subschema<'r>) -> Self {
46 Self::Subschema(Box::new(subschema))
47 }
48
49 pub fn ref_str(ref_name: impl Into<Cow<'r, str>>) -> Self {
50 Self::subschema(Subschema {
51 r#ref: Some(Reference::new(ref_name.into())),
52 ..Default::default()
53 })
54 }
55
56 pub fn typed_boolean() -> Self {
58 Self::subschema(Subschema {
59 r#type: SchemaType::new("boolean"),
60 ..Default::default()
61 })
62 }
63
64 pub fn typed_number(number_schema: NumberSchema) -> Self {
66 number_schema.into()
67 }
68
69 pub fn typed_string(string_schema: StringSchema) -> Self {
71 Self::subschema(Subschema {
72 r#type: SchemaType::new("string"),
73 string_schema: Some(string_schema),
74 ..Default::default()
75 })
76 }
77
78 pub fn typed_object(object_schema: ObjectSchema<'r>) -> Self {
80 Self::subschema(Subschema {
81 r#type: SchemaType::new("object"),
82 object_schema: Some(object_schema),
83 ..Default::default()
84 })
85 }
86
87 pub fn resolve(
89 &self,
90 key: Option<&Token>,
91 components: &[jsonptr::Component],
92 ) -> Option<&YamlSchema<'_>> {
93 debug!("[YamlSchema#resolve] self: {self}, key: {key:?}, components: {components:?}");
94 if components.is_empty() {
95 return Some(self);
96 }
97 match self {
98 YamlSchema::Subschema(subschema) => subschema.resolve(key, components),
99 _ => None,
100 }
101 }
102}
103
104impl<'r> TryFrom<&MarkedYaml<'r>> for YamlSchema<'r> {
105 type Error = crate::Error;
106 fn try_from(marked_yaml: &MarkedYaml<'r>) -> crate::Result<Self> {
107 match &marked_yaml.data {
108 YamlData::Value(scalar) => match scalar {
109 Scalar::Boolean(value) => Ok(YamlSchema::BooleanLiteral(*value)),
110 Scalar::Null => Ok(YamlSchema::Null),
111 _ => Err(generic_error!(
112 "[YamlSchema#try_from] Expected a boolean or null, but got: {}",
113 format_scalar(scalar)
114 )),
115 },
116 YamlData::Mapping(_) => Subschema::try_from(marked_yaml).map(YamlSchema::subschema),
117 _ => Err(generic_error!(
118 "[YamlSchema#try_from] Expected a boolean, null, or a mapping, but got: {}",
119 format_marked_yaml(marked_yaml)
120 )),
121 }
122 }
123}
124
125impl<'r> From<NumberSchema> for YamlSchema<'r> {
126 fn from(number_schema: NumberSchema) -> Self {
127 YamlSchema::subschema(Subschema {
128 r#type: SchemaType::new("number"),
129 number_schema: Some(number_schema),
130 ..Default::default()
131 })
132 }
133}
134
135impl<'r> From<IntegerSchema> for YamlSchema<'r> {
136 fn from(integer_schema: IntegerSchema) -> Self {
137 YamlSchema::subschema(Subschema {
138 r#type: SchemaType::new("integer"),
139 integer_schema: Some(integer_schema),
140 ..Default::default()
141 })
142 }
143}
144
145impl<'r> From<StringSchema> for YamlSchema<'r> {
146 fn from(string_schema: StringSchema) -> Self {
147 YamlSchema::subschema(Subschema {
148 r#type: SchemaType::new("string"),
149 string_schema: Some(string_schema),
150 ..Default::default()
151 })
152 }
153}
154
155impl Validator for YamlSchema<'_> {
156 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
157 debug!("[YamlSchema] self: {self}");
158 debug!(
159 "[YamlSchema] Validating value: {}",
160 format_yaml_data(&value.data)
161 );
162 match self {
163 YamlSchema::Empty => Ok(()),
164 YamlSchema::Null => {
165 if !matches!(&value.data, YamlData::Value(Scalar::Null)) {
166 context.add_error(
167 value,
168 format!("Expected null, but got: {}", format_yaml_data(&value.data)),
169 );
170 }
171 Ok(())
172 }
173 YamlSchema::BooleanLiteral(boolean) => {
174 if !*boolean {
175 context.add_error(value, "YamlSchema is `false`!");
176 }
177 Ok(())
178 }
179 YamlSchema::Subschema(subschema) => {
180 debug!("[YamlSchema#validate] Validating subschema: {subschema:?}");
181 subschema.validate(context, value)?;
182 Ok(())
183 }
184 }
185 }
186}
187
188impl<'r> From<Subschema<'r>> for YamlSchema<'r> {
189 fn from(subschema: Subschema<'r>) -> Self {
190 YamlSchema::subschema(subschema)
191 }
192}
193
194impl Display for YamlSchema<'_> {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 YamlSchema::Empty => write!(f, "<empty>"),
198 YamlSchema::Null => write!(f, "null"),
199 YamlSchema::BooleanLiteral(value) => write!(f, "{value}"),
200 YamlSchema::Subschema(subschema) => subschema.fmt(f),
201 }
202 }
203}
204
205#[derive(Debug, PartialEq)]
207pub enum BooleanOrSchema<'r> {
208 Boolean(bool),
209 Schema(YamlSchema<'r>),
210}
211
212impl BooleanOrSchema<'_> {
213 pub fn schema<'r>(schema: YamlSchema<'r>) -> BooleanOrSchema<'r> {
214 BooleanOrSchema::Schema(schema)
215 }
216}
217
218impl Display for BooleanOrSchema<'_> {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 BooleanOrSchema::Boolean(value) => write!(f, "{value}"),
222 BooleanOrSchema::Schema(schema) => schema.fmt(f),
223 }
224 }
225}
226
227#[derive(Debug, Default, PartialEq)]
228pub enum SchemaType {
229 #[default]
230 None,
232 Single(String),
234 Multiple(Vec<String>),
236}
237
238impl SchemaType {
239 pub fn new<S: Into<String>>(value: S) -> Self {
241 SchemaType::Single(value.into())
242 }
243
244 pub fn is_none(&self) -> bool {
245 matches!(self, SchemaType::None)
246 }
247
248 pub fn is_single(&self) -> bool {
249 matches!(self, SchemaType::Single(_))
250 }
251
252 pub fn is_multiple(&self) -> bool {
253 matches!(self, SchemaType::Multiple(_))
254 }
255
256 pub fn is_or_contains(&self, r#type: &str) -> bool {
287 match self {
288 SchemaType::None => false,
289 SchemaType::Single(s) => s == r#type,
290 SchemaType::Multiple(values) => values.contains(&r#type.to_string()),
291 }
292 }
293}
294
295impl Display for SchemaType {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 match self {
298 SchemaType::None => Ok(()), SchemaType::Single(value) => write!(f, "{value}"),
300 SchemaType::Multiple(values) => write!(f, "{}", format_vec(values)),
301 }
302 }
303}
304
305#[derive(Debug, Default, PartialEq)]
307pub struct Subschema<'r> {
308 pub metadata_and_annotations: MetadataAndAnnotations,
310 pub anchor: Option<String>,
312 pub r#ref: Option<Reference<'r>>,
314 pub defs: Option<LinkedHashMap<String, YamlSchema<'r>>>,
316 pub any_of: Option<AnyOfSchema<'r>>,
318 pub all_of: Option<AllOfSchema<'r>>,
320 pub one_of: Option<OneOfSchema<'r>>,
322 pub not: Option<NotSchema<'r>>,
324 pub r#type: SchemaType,
326 pub r#const: Option<ConstValue>,
328 pub r#enum: Option<EnumSchema>,
330
331 pub array_schema: Option<ArraySchema<'r>>,
332 pub integer_schema: Option<IntegerSchema>,
333 pub number_schema: Option<NumberSchema>,
334 pub object_schema: Option<ObjectSchema<'r>>,
335 pub string_schema: Option<StringSchema>,
336}
337
338impl<'r> Subschema<'r> {
339 pub fn resolve(
341 &self,
342 token: Option<&Token>,
343 components: &[jsonptr::Component],
344 ) -> Option<&YamlSchema<'_>> {
345 debug!("[Subschema#resolve] self: {self}, token: {token:?}, components: {components:?}");
346 if let Some(token) = token {
347 let s = token.decoded();
348 debug!("[Subschema#resolve] key: {s}");
349 match s.as_ref() {
350 "$defs" => {
351 debug!("[Subschema#resolve] Resolving $defs");
352 if let Some(defs) = self.defs.as_ref() {
353 debug!("[Subschema#resolve] defs: {}", format_linked_hash_map(defs));
354 if let Some(component) = components.first() {
355 debug!("[Subschema#resolve] component: {component:?}");
356 if let jsonptr::Component::Token(next_token) = component {
357 let decoded = next_token.decoded();
358 debug!("[Subschema#resolve] decoded: {decoded}");
359 debug!("[Subschema#resolve] defs: {defs:?}");
360 if let Some(schema) = defs.get(decoded.as_ref()) {
361 debug!("[Subschema#resolve] schema: {schema:?}");
362 return schema.resolve(Some(next_token), &components[1..]);
363 }
364 }
365 }
366 }
367 }
368 "anyOf" => {}
369 _ => (),
370 }
371 }
372 None
373 }
374}
375
376impl<'r> TryFrom<&MarkedYaml<'r>> for Subschema<'r> {
379 type Error = crate::Error;
380 fn try_from(marked_yaml: &MarkedYaml<'r>) -> crate::Result<Self> {
381 if let YamlData::Mapping(mapping) = &marked_yaml.data {
382 Self::try_from(mapping)
383 } else {
384 Err(generic_error!(
385 "{} Expected a mapping, but got: {:?}",
386 format_marker(&marked_yaml.span.start),
387 marked_yaml
388 ))
389 }
390 }
391}
392
393fn try_load_defs<'r>(
394 marked_yaml: &MarkedYaml<'r>,
395) -> Result<LinkedHashMap<String, YamlSchema<'r>>> {
396 debug!(
397 "[try_load_defs] marked_yaml: {}",
398 format_yaml_data(&marked_yaml.data)
399 );
400 if let YamlData::Mapping(mapping) = &marked_yaml.data {
401 debug!(
402 "[try_load_defs] mapping: {}",
403 format_annotated_mapping(mapping)
404 );
405 mapping
406 .iter()
407 .try_fold(LinkedHashMap::new(), |mut acc, (key, value)| {
408 let key = marked_yaml_to_string(key, "key must be a string")?;
409 acc.insert(key, value.try_into()?);
410 Ok(acc)
411 })
412 } else {
413 Err(expected_mapping!(marked_yaml))
414 }
415}
416
417impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for Subschema<'r> {
418 type Error = Error;
419
420 fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> crate::Result<Self> {
421 debug!(
422 "[Subschema#try_from] mapping has {} keys",
423 mapping.keys().len()
424 );
425 for key in mapping.keys() {
426 debug!("[Subschema#try_from] key: {:?}", key.data);
427 }
428
429 let metadata_and_annotations = MetadataAndAnnotations::try_from(mapping)?;
430 debug!("[Subschema#try_from] metadata_and_annotations: {metadata_and_annotations}");
431
432 let defs: Option<LinkedHashMap<String, YamlSchema<'r>>> = mapping
434 .get(&MarkedYaml::value_from_str("$defs"))
435 .map(|x| {
436 debug!("[Subschema#try_from] x: {}", format_yaml_data(&x.data));
437 debug!("[Subschema#try_from] Trying to load `$defs` as LinkedHashMap<String, YamlSchema<'r>>");
438 try_load_defs(x)
439 })
440 .transpose()?;
441
442 let reference: Option<Reference> = mapping
444 .get(&MarkedYaml::value_from_str("$ref"))
445 .map(|_| {
446 debug!("[Subschema#try_from] Trying to load `$ref` as Reference");
447 mapping.try_into()
448 })
449 .transpose()?;
450
451 let any_of: Option<AnyOfSchema> = mapping
453 .get(&MarkedYaml::value_from_str("anyOf"))
454 .map(|_| {
455 debug!("[Subschema#try_from] Trying to load `anyOf` as AnyOfSchema");
456 mapping.try_into()
457 })
458 .transpose()?;
459
460 let all_of: Option<AllOfSchema> = mapping
462 .get(&MarkedYaml::value_from_str("allOf"))
463 .map(|_| {
464 debug!("[Subschema#try_from] Trying to load `allOf` as AllOfSchema");
465 mapping.try_into()
466 })
467 .transpose()?;
468
469 let one_of: Option<OneOfSchema> = mapping
471 .get(&MarkedYaml::value_from_str("oneOf"))
472 .map(|_| {
473 debug!("[Subschema#try_from] Trying to load `oneOf` as OneOfSchema");
474 mapping.try_into()
475 })
476 .transpose()?;
477
478 let not: Option<NotSchema> = mapping
480 .get(&MarkedYaml::value_from_str("not"))
481 .map(|_| {
482 debug!("[Subschema#try_from] Trying to load `not` as NotSchema");
483 mapping.try_into()
484 })
485 .transpose()?;
486
487 let mut r#const: Option<ConstValue> = None;
489 if let Some(value) = mapping.get(&MarkedYaml::value_from_str("const")) {
490 r#const = Some(ConstValue::try_from(value)?);
491 }
492
493 let mut r#enum: Option<EnumSchema> = None;
495 if let Some(value) = mapping.get(&MarkedYaml::value_from_str("enum")) {
496 r#enum = Some(value.try_into()?);
497 }
498
499 let mut r#type: SchemaType = SchemaType::None;
501 if let Some(type_value) = mapping.get(&MarkedYaml::value_from_str("type")) {
502 match &type_value.data {
503 YamlData::Value(Scalar::Null) => {
504 r#type = SchemaType::new("null");
505 }
506 YamlData::Value(Scalar::String(s)) => r#type = SchemaType::new(s.as_ref()),
507 YamlData::Sequence(values) => {
508 r#type = SchemaType::Multiple(
509 values
510 .iter()
511 .map(|marked_yaml| {
512 marked_yaml_to_string(marked_yaml, "type must be a string")
513 })
514 .collect::<Result<Vec<String>>>()?,
515 )
516 }
517 _ => {
518 return Err(schema_loading_error!(
519 "[Subschema#try_from] Expected a string or sequence for `type`, but got: {:?}",
520 type_value.data
521 ));
522 }
523 }
524 }
525
526 let mut array_schema = None;
528 let mut integer_schema = None;
529 let mut number_schema = None;
530 let mut object_schema = None;
531 let mut string_schema = None;
532
533 let types: Vec<&str> = match r#type {
534 SchemaType::None => vec![],
535 SchemaType::Single(ref s) => vec![s],
536 SchemaType::Multiple(ref values) => values.iter().map(|s| s.as_ref()).collect(),
537 };
538
539 for s in types {
540 match s {
541 "array" => {
542 debug!("[Subschema#try_from] Instantiating array schema");
543 array_schema = ArraySchema::try_from(mapping).map(Some)?;
544 }
545 "boolean" => {}
547 "integer" => {
548 debug!("[Subschema#try_from] Instantiating integer schema");
549 integer_schema = IntegerSchema::try_from(mapping).map(Some)?;
550 }
551 "number" => {
552 debug!("[Subschema#try_from] Instantiating number schema");
553 number_schema = NumberSchema::try_from(mapping).map(Some)?;
554 }
555 "object" => {
556 debug!("[Subschema#try_from] Instantiating object schema");
557 object_schema = ObjectSchema::try_from(mapping).map(Some)?;
558 }
559 "string" => {
560 debug!("[Subschema#try_from] Instantiating string schema");
561 string_schema = StringSchema::try_from(mapping).map(Some)?;
562 }
563 "null" => (),
564 _ => {
565 return Err(unsupported_type!(
566 "Expected type: string, number, integer, object, array, boolean, or null, but got: {}",
567 s
568 ));
569 }
570 }
571 }
572
573 debug!("[Subschema#try_from] array_schema: {array_schema:?}");
574 debug!("[Subschema#try_from] integer_schema: {integer_schema:?}");
575 debug!("[Subschema#try_from] number_schema: {number_schema:?}");
576 debug!("[Subschema#try_from] object_schema: {object_schema:?}");
577 debug!("[Subschema#try_from] string_schema: {string_schema:?}");
578
579 Ok(Self {
580 metadata_and_annotations,
581 defs,
582 r#ref: reference,
583 any_of,
584 all_of,
585 one_of,
586 not,
587 r#type,
588 r#const,
589 r#enum,
590 array_schema,
591 integer_schema,
592 number_schema,
593 object_schema,
594 string_schema,
595 anchor: None,
596 })
597 }
598}
599
600impl Display for Subschema<'_> {
601 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602 write!(f, "{{")?;
603 if !self.metadata_and_annotations.is_empty() {
604 write!(f, " ")?;
605 self.metadata_and_annotations.fmt(f)?;
606 write!(f, " ")?;
607 }
608 if !self.r#type.is_none() {
609 write!(f, "type: ")?;
610 self.r#type.fmt(f)?;
611 }
612 if let Some(r#ref) = &self.r#ref {
613 write!(f, "$ref: ")?;
614 r#ref.fmt(f)?;
615 }
616 if let Some(defs) = &self.defs {
617 write!(f, "$defs: {}", format_linked_hash_map(defs))?;
618 }
619 if let Some(any_of) = &self.any_of {
620 write!(f, "anyOf: ")?;
621 any_of.fmt(f)?;
622 }
623 if let Some(all_of) = &self.all_of {
624 write!(f, "allOf: ")?;
625 all_of.fmt(f)?;
626 }
627 if let Some(one_of) = &self.one_of {
628 write!(f, "oneOf: ")?;
629 one_of.fmt(f)?;
630 }
631 if let Some(not) = &self.not {
632 write!(f, "not: ")?;
633 not.fmt(f)?;
634 }
635 write!(f, "}}")?;
636 Ok(())
637 }
638}
639
640impl Validator for Subschema<'_> {
641 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> crate::Result<()> {
642 debug!("[Subschema] self: {self}");
643 debug!(
644 "[Subschema] Validating value: {}",
645 format_yaml_data(&value.data)
646 );
647
648 if let Some(reference) = &self.r#ref {
649 debug!("[Subschema] Reference found: {reference}");
650 let ref_name = &reference.ref_name;
651 if let Some(root_schema) = context.root_schema {
652 if let Some(ref_path) = ref_name.strip_prefix("#") {
653 if context.is_resolving_ref(ref_name, value) {
654 context.add_error(value, format!("Circular $ref detected: {ref_name}"));
655 return Ok(());
656 }
657 let pointer = jsonptr::Pointer::parse(ref_path)?;
658 debug!("[Subschema] Pointer: {pointer}");
659 let schema = root_schema.resolve(pointer);
660 if let Some(schema) = schema {
661 debug!("[Subschema] Found {ref_path}: {schema}");
662 context.begin_resolving_ref(ref_name, value);
663 let result = schema.validate(context, value);
664 context.end_resolving_ref(ref_name, value);
665 result?;
666 } else {
667 error!("[Subschema] Cannot find definition: {ref_path}");
668 context.add_error(value, format!("Schema {ref_path} not found"));
669 }
670 } else {
671 error!("[Subschema] Cannot find definition: {ref_name}");
672 context.add_error(value, format!("Schema {ref_name} not found"));
673 }
674 return Ok(());
675 } else {
676 return Err(generic_error!(
677 "Subschema has a reference, but no root schema was provided!"
678 ));
679 }
680 }
681
682 if let Some(any_of) = &self.any_of {
683 debug!("[Subschema] Validating anyOf schema: {any_of:?}");
684 any_of.validate(context, value)?;
685 }
686
687 if let Some(all_of) = &self.all_of {
688 debug!("[Subschema] Validating allOf schema: {all_of:?}");
689 all_of.validate(context, value)?;
690 }
691
692 if let Some(one_of) = &self.one_of {
693 debug!("[Subschema] Validating oneOf schema: {one_of:?}");
694 one_of.validate(context, value)?;
695 }
696
697 if let Some(not) = &self.not {
698 debug!("[Subschema] Validating not schema: {not:?}");
699 not.validate(context, value)?;
700 }
701
702 match &self.r#type {
703 SchemaType::None => (),
704 SchemaType::Single(s) => self.validate_by_type(context, s.as_ref(), value)?,
705 SchemaType::Multiple(values) => {
706 debug!(
707 "[Subschema] Validating multiple types: {}",
708 values.join(", ")
709 );
710 let mut any_matched = false;
711 for s in values {
712 let sub_context = context.get_sub_context();
713 self.validate_by_type(&sub_context, s.as_ref(), value)?;
714 if !sub_context.has_errors() {
715 any_matched = true;
716 break;
717 }
718 }
719 if !any_matched {
720 context.add_error(
721 value,
722 format!("None of type: [{}] matched", values.join(", ")),
723 );
724 }
725 }
726 }
727
728 if let Some(r#const) = &self.r#const
729 && !r#const.accepts(value)
730 {
731 context.add_error(
732 value,
733 format!(
734 "Expected const: {:#?}, but got: {}",
735 r#const,
736 format_yaml_data(&value.data)
737 ),
738 );
739 }
740
741 if let Some(r#enum) = &self.r#enum {
742 debug!("[Subschema] Validating enum schema: {}", r#enum);
743 r#enum.validate(context, value)?;
744 }
745
746 Ok(())
747 }
748}
749
750impl Subschema<'_> {
751 fn validate_by_type(
752 &self,
753 context: &Context,
754 r#type: &str,
755 value: &saphyr::MarkedYaml,
756 ) -> Result<()> {
757 debug!("[Subschema#validate_by_type] r#type: {}", r#type);
758 match r#type {
759 "array" => {
760 if let Some(array_schema) = &self.array_schema {
761 debug!("[Subschema] Validating array schema: {array_schema:?}");
762 array_schema.validate(context, value)?;
763 } else {
764 error!("[Subschema#validate_by_type] No array schema found");
765 context.add_error(value, format!("No array schema found for type: {}", r#type));
766 }
767 }
768 "boolean" => {
769 if !matches!(&value.data, YamlData::Value(Scalar::Boolean(_))) {
770 context.add_error(
771 value,
772 format!(
773 "Expected boolean, but got: {}",
774 format_yaml_data(&value.data)
775 ),
776 );
777 }
778 }
779 "null" => {
780 if !matches!(&value.data, YamlData::Value(Scalar::Null)) {
781 context.add_error(
782 value,
783 format!("Expected null, but got: {}", format_yaml_data(&value.data)),
784 );
785 }
786 }
787 "string" => {
788 if let Some(string_schema) = &self.string_schema {
789 debug!("[Subschema] Validating string schema: {string_schema:?}");
790 string_schema.validate(context, value)?;
791 } else {
792 error!("[Subschema#validate_by_type] No string schema found");
793 context.add_error(
794 value,
795 format!("No string schema found for type: {}", r#type),
796 );
797 }
798 }
799 "number" => {
800 if let Some(number_schema) = &self.number_schema {
801 debug!("[Subschema] Validating number schema: {number_schema:?}");
802 number_schema.validate(context, value)?;
803 } else {
804 error!("[Subschema#validate_by_type] No number schema found");
805 context.add_error(
806 value,
807 format!("No number schema found for type: {}", r#type),
808 );
809 }
810 }
811 "integer" => {
812 if let Some(integer_schema) = &self.integer_schema {
813 debug!("[Subschema] Validating integer schema: {integer_schema:?}");
814 integer_schema.validate(context, value)?;
815 } else {
816 error!("[Subschema#validate_by_type] No integer schema found");
817 context.add_error(
818 value,
819 format!("No integer schema found for type: {}", r#type),
820 );
821 }
822 }
823 "object" => {
824 if let Some(object_schema) = &self.object_schema {
825 debug!("[Subschema] Validating object schema: {object_schema:?}");
826 object_schema.validate(context, value)?;
827 } else {
828 error!("[Subschema#validate_by_type] No object schema found");
829 context.add_error(
830 value,
831 format!("No object schema found for type: {}", r#type),
832 );
833 }
834 }
835 _ => {
836 error!("[Subschema#validate_by_type] Unsupported type: {}", r#type);
837 context.add_error(value, format!("Unsupported type: {}", r#type));
838 }
839 }
840 Ok(())
841 }
842}
843
844#[derive(Debug, Default, PartialEq)]
846pub struct MetadataAndAnnotations {
847 pub id: Option<String>,
849 pub schema: Option<String>,
851 pub title: Option<String>,
853 pub description: Option<String>,
855}
856
857impl MetadataAndAnnotations {
858 pub fn is_empty(&self) -> bool {
859 self.id.is_none()
860 && self.schema.is_none()
861 && self.title.is_none()
862 && self.description.is_none()
863 }
864}
865
866impl std::fmt::Display for MetadataAndAnnotations {
867 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868 write!(f, "{{")?;
869 if !self.is_empty() {
870 write!(f, " ")?;
871 if let Some(id) = &self.id {
872 write!(f, "id: {id}, ")?;
873 }
874 if let Some(schema) = &self.schema {
875 write!(f, "schema: {schema}, ")?;
876 }
877 if let Some(title) = &self.title {
878 write!(f, "title: {title}, ")?;
879 }
880 if let Some(description) = &self.description {
881 write!(f, "description: {description}, ")?;
882 }
883 write!(f, " ")?;
884 }
885 write!(f, "}}")?;
886 Ok(())
887 }
888}
889
890impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for MetadataAndAnnotations {
891 type Error = Error;
892
893 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
894 let mut metadata_and_annotations = MetadataAndAnnotations::default();
895 for (key, value) in mapping.iter() {
896 match &key.data {
897 YamlData::Value(Scalar::String(s)) => match s.as_ref() {
898 "$id" => {
899 metadata_and_annotations.id =
900 Some(marked_yaml_to_string(value, "$id must be a string")?);
901 }
902 "$schema" => {
903 metadata_and_annotations.schema =
904 Some(marked_yaml_to_string(value, "$schema must be a string")?);
905 }
906 "title" => {
907 metadata_and_annotations.title =
908 Some(marked_yaml_to_string(value, "title must be a string")?);
909 }
910 "description" => {
911 metadata_and_annotations.description = Some(marked_yaml_to_string(
912 value,
913 "description must be a string",
914 )?);
915 }
916 _ => {
917 debug!("[MetadataAndAnnotations#try_from] Unknown key: {s}");
918 }
919 },
920 _ => {
921 debug!("[MetadataAndAnnotations#try_from] Unsupported key data: {key:?}");
922 }
923 }
924 }
925 Ok(metadata_and_annotations)
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use saphyr::LoadableYamlNode;
932
933 use crate::loader;
934
935 use super::*;
936
937 #[test]
938 fn test_type_boolean() {
939 let yaml = r#"
940 type: boolean
941 "#;
942 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
943 let marked_yaml = doc.first().unwrap();
944 let yaml_schema = YamlSchema::try_from(marked_yaml).unwrap();
945 let YamlSchema::Subschema(subschema) = yaml_schema else {
946 panic!("Expected a subschema");
947 };
948 assert!(!subschema.r#type.is_none());
949 assert!(subschema.r#type.is_single());
950 let SchemaType::Single(type_value) = subschema.r#type else {
951 panic!("Expected a single type");
952 };
953 assert_eq!(type_value, "boolean");
954 }
955
956 #[test]
957 fn test_metadata_and_annotations_try_from() {
958 let yaml = r#"
959 $id: http://example.com/schema
960 $schema: http://example.com/schema
961 title: Example Schema
962 description: This is an example schema
963 "#;
964 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
965 let marked_yaml = doc.first().unwrap();
966 assert!(marked_yaml.data.is_mapping());
967 let YamlData::Mapping(mapping) = &marked_yaml.data else {
968 panic!("Expected a mapping");
969 };
970 let metadata_and_annotations = MetadataAndAnnotations::try_from(mapping).unwrap();
971 assert_eq!(
972 metadata_and_annotations.id,
973 Some("http://example.com/schema".to_string())
974 );
975 assert_eq!(
976 metadata_and_annotations.schema,
977 Some("http://example.com/schema".to_string())
978 );
979 assert_eq!(
980 metadata_and_annotations.title,
981 Some("Example Schema".to_string())
982 );
983 assert_eq!(
984 metadata_and_annotations.description,
985 Some("This is an example schema".to_string())
986 );
987 }
988
989 #[test]
990 fn test_yaml_schema_with_multiple_types() {
991 let yaml = r#"
992 type:
993 - boolean
994 - number
995 - integer
996 - string
997 "#;
998 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
999 let marked_yaml = doc.first().unwrap();
1000 let yaml_schema = YamlSchema::try_from(marked_yaml).unwrap();
1001 let YamlSchema::Subschema(subschema) = yaml_schema else {
1002 panic!("Expected a subschema");
1003 };
1004 assert!(!subschema.r#type.is_none());
1005 assert!(subschema.r#type.is_multiple());
1006 let SchemaType::Multiple(type_values) = subschema.r#type else {
1007 panic!("Expected a multiple type");
1008 };
1009 assert_eq!(type_values, vec!["boolean", "number", "integer", "string"]);
1010 }
1011
1012 #[test]
1013 fn test_multiple_types() {
1014 let schema = r#"
1015 type:
1016 - string
1017 - number
1018 "#;
1019 let schema = loader::load_from_str(schema).unwrap();
1020
1021 let s = "I'm a string";
1022 let docs = MarkedYaml::load_from_str(s).unwrap();
1023 let value = docs.first().unwrap();
1024 let context = Context::default();
1025 let result = schema.validate(&context, value);
1026 assert!(result.is_ok());
1027 assert!(!context.has_errors());
1028
1029 let s = "42";
1030 let docs = MarkedYaml::load_from_str(s).unwrap();
1031 let value = docs.first().unwrap();
1032 let context = Context::default();
1033 let result = schema.validate(&context, value);
1034 assert!(result.is_ok());
1035 assert!(!context.has_errors());
1036
1037 let s = "null";
1038 let docs = MarkedYaml::load_from_str(s).unwrap();
1039 let value = docs.first().unwrap();
1040 let context = Context::default();
1041 let result = schema.validate(&context, value);
1042 assert!(result.is_ok());
1043 assert!(context.has_errors());
1044 let errors = context.errors.borrow();
1045 assert_eq!(errors.len(), 1);
1046 assert_eq!(errors[0].error, "None of type: [string, number] matched");
1047 }
1048
1049 #[test]
1050 fn test_object_schema_with_const_property() {
1051 let schema = r#"
1052 type: object
1053 properties:
1054 const:
1055 description: A scalar value that must match the value
1056 type:
1057 - string
1058 - integer
1059 - number
1060 - boolean
1061 "#;
1062 let schema = loader::load_from_str(schema).expect("Failed to load schema");
1063
1064 let docs = MarkedYaml::load_from_str(
1065 r#"
1066 const: "I'm a string"
1067 "#,
1068 )
1069 .unwrap();
1070 let value = docs.first().unwrap();
1071 let context = Context::default();
1072 let result = schema.validate(&context, value);
1073 assert!(result.is_ok());
1074 assert!(!context.has_errors());
1075 }
1076}