Skip to main content

yaml_schema/schemas/
yaml_schema.rs

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/// YamlSchema is the base of the validation model
36#[derive(Debug, PartialEq)]
37pub enum YamlSchema<'r> {
38    Empty,                // no value
39    Null,                 // `null`
40    BooleanLiteral(bool), // `true` or `false`
41    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    /// Create a YamlSchema with a single type: `boolean`
57    pub fn typed_boolean() -> Self {
58        Self::subschema(Subschema {
59            r#type: SchemaType::new("boolean"),
60            ..Default::default()
61        })
62    }
63
64    /// Create a YamlSchema with a single type: `number`
65    pub fn typed_number(number_schema: NumberSchema) -> Self {
66        number_schema.into()
67    }
68
69    /// Create a YamlSchema with a single type: `string`
70    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    /// Create a YamlSchema with a single type: `object`
79    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    /// Resolve a portion of a JSON Pointer to an element in the schema.
88    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/// Represents either a literal boolean value or a YamlSchema
206#[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    /// No `type:` was provided
231    None,
232    /// A single type
233    Single(String),
234    /// Multiple types
235    Multiple(Vec<String>),
236}
237
238impl SchemaType {
239    /// Create a new SchemaType with a single value
240    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    /// Checks if this SchemaType matches or contains the given type string.
257    ///
258    /// Returns `true` if:
259    /// - The schema type is `Single` and matches the given type string, or
260    /// - The schema type is `Multiple` and contains the given type string
261    ///
262    /// Returns `false` if:
263    /// - The schema type is `None`, or
264    /// - The schema type doesn't match/contain the given type string
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use yaml_schema::schemas::SchemaType;
270    ///
271    /// // Test with a single type
272    /// let single = SchemaType::new("string");
273    /// assert!(single.is_or_contains("string"));
274    /// assert!(!single.is_or_contains("number"));
275    ///
276    /// // Test with multiple types
277    /// let multiple = SchemaType::Multiple(vec!["string".to_string(), "number".to_string()]);
278    /// assert!(multiple.is_or_contains("string"));
279    /// assert!(multiple.is_or_contains("number"));
280    /// assert!(!multiple.is_or_contains("boolean"));
281    ///
282    /// // Test with None (no type specified)
283    /// let none = SchemaType::None;
284    /// assert!(!none.is_or_contains("string"));
285    /// ```
286    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(()), // No `type:` was provided
299            SchemaType::Single(value) => write!(f, "{value}"),
300            SchemaType::Multiple(values) => write!(f, "{}", format_vec(values)),
301        }
302    }
303}
304
305/// A Subschema contains the core schema elements and validation
306#[derive(Debug, Default, PartialEq)]
307pub struct Subschema<'r> {
308    /// `$id` and `$schema` metadata and `title` and `description` annotations
309    pub metadata_and_annotations: MetadataAndAnnotations,
310    /// `$anchor` metadata
311    pub anchor: Option<String>,
312    /// `$ref`
313    pub r#ref: Option<Reference<'r>>,
314    /// `$defs`
315    pub defs: Option<LinkedHashMap<String, YamlSchema<'r>>>,
316    /// `anyOf`
317    pub any_of: Option<AnyOfSchema<'r>>,
318    /// `allOf`
319    pub all_of: Option<AllOfSchema<'r>>,
320    /// `oneOf`
321    pub one_of: Option<OneOfSchema<'r>>,
322    /// `not`
323    pub not: Option<NotSchema<'r>>,
324    /// `type`
325    pub r#type: SchemaType,
326    /// `const`
327    pub r#const: Option<ConstValue>,
328    /// `enum`
329    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    /// Resolve a portion of a JSON Pointer to an element in the schema.
340    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
376// Try to load a Subschema from a MarkedYaml. Delegate to the TryFrom<&AnnotatedMapping<'_>> for mappings.
377// If the MarkedYaml is not a mapping, returns an error.
378impl<'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        // $defs
433        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        // $ref
443        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        // anyOf
452        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        // allOf
461        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        // oneOf
470        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        // not
479        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        // const
488        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        // enum
494        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        // type
500        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        // Instantiate the appropriate schema based on the type(s)
527        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                // No subschema needed for boolean, we handle it in the validate_by_type method
546                "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/// The `$id` and `$schema` metadata
845#[derive(Debug, Default, PartialEq)]
846pub struct MetadataAndAnnotations {
847    /// `$id` metadata
848    pub id: Option<String>,
849    /// `$schema` metadata
850    pub schema: Option<String>,
851    /// `title` annotation
852    pub title: Option<String>,
853    /// `description` annotation
854    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}