1use std::borrow::Cow;
2
3use log::debug;
4use saphyr::AnnotatedMapping;
5use saphyr::MarkedYaml;
6use saphyr::YamlData;
7
8use crate::utils::format_annotated_mapping;
9
10#[derive(Clone, Debug, Default, PartialEq)]
13pub struct Reference<'r> {
14 pub ref_name: Cow<'r, str>,
15}
16
17impl std::fmt::Display for Reference<'_> {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 write!(f, "$ref: {}", self.ref_name)
20 }
21}
22
23impl<'r> Reference<'r> {
24 pub fn new(ref_name: Cow<'r, str>) -> Reference<'r> {
25 Reference { ref_name }
26 }
27}
28
29impl<'r> TryFrom<&MarkedYaml<'r>> for Reference<'r> {
30 type Error = crate::Error;
31
32 fn try_from(value: &MarkedYaml<'r>) -> std::result::Result<Self, Self::Error> {
33 if let YamlData::Mapping(mapping) = &value.data {
34 Self::try_from(mapping)
35 } else {
36 Err(expected_mapping!(value))
37 }
38 }
39}
40
41impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for Reference<'r> {
42 type Error = crate::Error;
43
44 fn try_from<'a>(
45 mapping: &AnnotatedMapping<'a, MarkedYaml<'a>>,
46 ) -> crate::Result<Reference<'a>> {
47 debug!("[Reference#try_from] {}", format_annotated_mapping(mapping));
48 let ref_key = MarkedYaml::value_from_str("$ref");
49 if let Some(ref_value) = mapping.get(&ref_key) {
50 match &ref_value.data {
51 YamlData::Value(saphyr::Scalar::String(s)) => {
52 if !s.starts_with("#/$defs/") && !s.starts_with("#/definitions/") {
53 return Err(generic_error!(
54 "Only local references, starting with #/$defs/ or #/definitions/ are supported for now. Found: {}",
55 s
56 ));
57 }
58 Ok(Reference::new(s.clone()))
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 "No $ref key found in mapping: {}",
68 format_annotated_mapping(mapping)
69 ))
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use crate::Validator as _;
77 use crate::YamlSchema;
78 use crate::loader;
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 = loader::load_from_str(schema).expect("Failed to load schema");
93 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
94 panic!("Expected a subschema");
95 };
96
97 assert!(
100 subschema.r#type.is_or_contains("object"),
101 "Expected object type"
102 );
103
104 let defs = subschema
106 .defs
107 .as_ref()
108 .expect("Expected $defs to be present");
109 let name_def = defs.get("name").expect("Expected 'name' in $defs");
110
111 let YamlSchema::Subschema(name_subschema) = name_def else {
113 panic!("Expected name definition to be a subschema");
114 };
115 assert!(
116 name_subschema.r#type.is_or_contains("string"),
117 "Expected name definition to be a string type"
118 );
119
120 let object_schema = subschema
122 .object_schema
123 .as_ref()
124 .expect("Expected object_schema");
125 let properties = object_schema
126 .properties
127 .as_ref()
128 .expect("Expected properties");
129
130 let name_property = properties.get("name").expect("Expected 'name' property");
132 let YamlSchema::Subschema(name_prop_subschema) = name_property else {
133 panic!("Expected name property to be a subschema");
134 };
135
136 let ref_value = name_prop_subschema
138 .r#ref
139 .as_ref()
140 .expect("Expected $ref in name property");
141 assert_eq!(
142 ref_value.ref_name.as_ref(),
143 "#/$defs/name",
144 "Expected reference to '#/$defs/name'"
145 );
146
147 let context = crate::Context::with_root_schema(&root_schema, true);
148 let value = r##"
149 name: "John Doe"
150 "##;
151 let docs = saphyr::MarkedYaml::load_from_str(value).unwrap();
152 let value = docs.first().unwrap();
153 let result = root_schema.validate(&context, value);
154 assert!(result.is_ok());
155 assert!(!context.has_errors());
156 }
157
158 #[test]
159 fn test_json_ptr() {
160 let ptr = jsonptr::Pointer::parse("/$defs/schema").expect("Failed to parse JSON pointer");
161 let components: Vec<_> = ptr.components().collect();
162 assert!(!components.is_empty());
163 }
164
165 #[test]
166 fn test_circular_reference_direct() {
167 let schema = r##"
168 $defs:
169 a:
170 $ref: "#/$defs/a"
171 $ref: "#/$defs/a"
172 "##;
173 let root_schema = loader::load_from_str(schema).expect("Failed to load schema");
174 let context = crate::Context::with_root_schema(&root_schema, false);
175 let docs = saphyr::MarkedYaml::load_from_str("test").unwrap();
176 let value = docs.first().unwrap();
177 let result = root_schema.validate(&context, value);
178 assert!(result.is_ok());
179 assert!(context.has_errors());
180 let errors = context.errors.borrow();
181 assert_eq!(errors.len(), 1);
182 assert!(
183 errors[0].error.contains("Circular $ref detected"),
184 "Expected circular ref error, got: {}",
185 errors[0].error
186 );
187 }
188
189 #[test]
190 fn test_circular_reference_indirect() {
191 let schema = r##"
192 $defs:
193 a:
194 $ref: "#/$defs/b"
195 b:
196 $ref: "#/$defs/a"
197 $ref: "#/$defs/a"
198 "##;
199 let root_schema = loader::load_from_str(schema).expect("Failed to load schema");
200 let context = crate::Context::with_root_schema(&root_schema, false);
201 let docs = saphyr::MarkedYaml::load_from_str("test").unwrap();
202 let value = docs.first().unwrap();
203 let result = root_schema.validate(&context, value);
204 assert!(result.is_ok());
205 assert!(context.has_errors());
206 let errors = context.errors.borrow();
207 assert_eq!(errors.len(), 1);
208 assert!(
209 errors[0].error.contains("Circular $ref detected"),
210 "Expected circular ref error, got: {}",
211 errors[0].error
212 );
213 }
214
215 #[test]
216 fn test_non_circular_ref_still_works() {
217 let schema = r##"
218 $defs:
219 name:
220 type: string
221 type: object
222 properties:
223 first_name:
224 $ref: "#/$defs/name"
225 last_name:
226 $ref: "#/$defs/name"
227 "##;
228 let root_schema = loader::load_from_str(schema).expect("Failed to load schema");
229 let context = crate::Context::with_root_schema(&root_schema, false);
230 let value = r#"
231 first_name: "Alice"
232 last_name: "Smith"
233 "#;
234 let docs = saphyr::MarkedYaml::load_from_str(value).unwrap();
235 let value = docs.first().unwrap();
236 let result = root_schema.validate(&context, value);
237 assert!(result.is_ok());
238 assert!(
239 !context.has_errors(),
240 "Expected no errors, got: {:?}",
241 context.errors.borrow()
242 );
243 }
244}