yaml_schema/
reference.rs

1use saphyr::MarkedYaml;
2use saphyr::YamlData;
3
4use crate::utils::format_marker;
5
6/// A Reference is a reference to another schema, usually one that is
7/// declared in the `$defs` section of the root schema.
8#[derive(Clone, Debug, Default, PartialEq)]
9pub struct Reference {
10    pub ref_name: String,
11}
12
13impl std::fmt::Display for Reference {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "$ref: {}", self.ref_name)
16    }
17}
18
19impl Reference {
20    pub fn new<S>(ref_name: S) -> Reference
21    where
22        S: Into<String>,
23    {
24        Reference {
25            ref_name: ref_name.into(),
26        }
27    }
28}
29
30impl TryFrom<&MarkedYaml<'_>> for Reference {
31    type Error = crate::Error;
32
33    fn try_from(value: &MarkedYaml<'_>) -> std::result::Result<Self, Self::Error> {
34        if let YamlData::Mapping(mapping) = &value.data {
35            let ref_key = MarkedYaml::value_from_str("$ref");
36            if !mapping.contains_key(&ref_key) {
37                return Err(generic_error!(
38                    "{} Expected a $ref key, but got: {:#?}",
39                    format_marker(&value.span.start),
40                    mapping
41                ));
42            }
43
44            let ref_value = mapping.get(&ref_key).unwrap();
45            match &ref_value.data {
46                YamlData::Value(saphyr::Scalar::String(s)) => {
47                    if !s.starts_with("#/$defs/") && !s.starts_with("#/definitions/") {
48                        return Err(generic_error!(
49                            "Only local references, starting with #/$defs/ or #/definitions/ are supported for now. Found: {}",
50                            s
51                        ));
52                    }
53                    let ref_name = match s.strip_prefix("#/$defs/") {
54                        Some(ref_name) => ref_name,
55                        _ => s.strip_prefix("#/definitions/").unwrap(),
56                    };
57
58                    Ok(Reference::new(ref_name))
59                }
60                _ => Err(generic_error!(
61                    "Expected a string value for $ref, but got: {:#?}",
62                    ref_value
63                )),
64            }
65        } else {
66            Err(generic_error!(
67                "{} value is not a mapping: {:?}",
68                format_marker(&value.span.start),
69                value
70            ))
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use crate::RootSchema;
78    use crate::Schema;
79    use saphyr::LoadableYamlNode;
80
81    #[test]
82    fn test_reference() {
83        let schema = r##"
84            $defs:
85                name:
86                    type: string
87            type: object
88            properties:
89                name:
90                    $ref: "#/$defs/name"
91        "##;
92        let root_schema = RootSchema::load_from_str(schema).unwrap();
93        let yaml_schema = root_schema.schema.as_ref();
94        println!("yaml_schema: {yaml_schema:#?}");
95        let schema = yaml_schema.schema.as_ref().unwrap();
96        println!("schema: {schema:#?}");
97        if let Schema::Typed(typed_schema) = schema {
98            let first_type = typed_schema.r#type.first().unwrap();
99            if let crate::schemas::TypedSchemaType::Object(object_schema) = first_type {
100                if let Some(properties) = &object_schema.properties {
101                    if let Some(name_property) = properties.get("name") {
102                        let name_ref = name_property.r#ref.as_ref().unwrap();
103                        assert_eq!(name_ref.ref_name, "name");
104                    }
105                }
106            }
107        } else {
108            panic!("Expected Schema::Typed, but got: {schema:?}");
109        }
110        let context = crate::Context::with_root_schema(&root_schema, true);
111        let value = r##"
112            name: "John Doe"
113        "##;
114        let docs = saphyr::MarkedYaml::load_from_str(value).unwrap();
115        let value = docs.first().unwrap();
116        let result = root_schema.validate(&context, value);
117        assert!(result.is_ok());
118        assert!(!context.has_errors());
119    }
120}