1use crate::loader::DocumentPath;
2use serde_json::{Map, Value};
3use std::collections::HashMap;
4
5const REF: &str = "$ref";
6const PATH_SEP: char = '/';
7const SHARP_SEP: char = '#';
8const FROM_REF: &str = "x-fromRef";
9const REF_NAME: &str = "x-refName";
10
11type DocumentsHash = HashMap<DocumentPath, Value>;
12
13pub struct RefResolver {
14 _hash: DocumentsHash,
15}
16
17impl RefResolver {
18 fn new() -> Self {
19 Self { _hash: Default::default() }
20 }
21
22 }
30
31pub fn resolve_refs_raw(json: Value) -> Result<Value, anyhow::Error> {
65 resolve_refs_recurse(&DocumentPath::None, json.clone(), &json, &mut RefResolver::new())
66}
67
68pub fn resolve_refs(document: DocumentPath) -> Result<Value, anyhow::Error> {
69 let json = document.load_raw()?;
70 resolve_refs_recurse(&document, json.clone(), &json, &mut RefResolver::new())
71}
72
73fn resolve_refs_recurse(
74 current_doc: &DocumentPath,
75 json: Value,
76 original: &Value,
77 cache: &mut RefResolver,
78) -> Result<Value, anyhow::Error> {
79 match json {
80 Value::Array(a) => {
81 let mut new = Vec::<_>::with_capacity(a.len());
82 for v in a {
83 new.push(resolve_refs_recurse(current_doc, v, original, cache)?);
84 }
85 Ok(Value::Array(new))
86 }
87 Value::Object(obj) => {
88 let mut map = Map::new();
89 for (key, value) in obj.into_iter() {
90 if key != REF {
91 map.insert(key, resolve_refs_recurse(current_doc, value, original, cache)?);
92 } else if let Value::String(ref_value) = value {
93 let ref_info = RefInfo::parse(current_doc, &ref_value)?;
94 let is_nested = ref_info.document_path == *current_doc;
95
96 let new_value = if is_nested {
97 let v = fetch_reference_value(original, &ref_info.path)?;
98 resolve_refs_recurse(current_doc, v, original, cache)?
99 } else {
100 let doc_path = ref_info.document_path;
101 let json = doc_path.load_raw()?;
102 let v = fetch_reference_value(&json, &ref_info.path)?;
103 resolve_refs_recurse(&doc_path, v, &json, cache)?
104 };
105
106 match new_value {
107 Value::Object(m) => {
108 for (k, v) in m {
109 map.insert(k, v);
110 }
111 map.insert(FROM_REF.into(), Value::String(ref_value.clone()));
112 map.insert(
113 REF_NAME.into(),
114 Value::String(ref_info.path.map(|p| get_ref_name(&p)).unwrap_or_default()),
115 );
116 }
117 v => return Ok(v),
118 }
119 } else {
120 return Err(anyhow::anyhow!("{} value should be a String", REF));
121 }
122 }
123 Ok(Value::Object(map))
124 }
125 _ => Ok(json),
126 }
127}
128
129fn get_ref_name(path: &str) -> String {
130 path.split(PATH_SEP).last().unwrap_or_default().to_string()
131}
132
133fn fetch_reference_value(json: &Value, path: &Option<String>) -> Result<Value, anyhow::Error> {
134 match path {
135 Some(p) => {
136 let parts = p.split(PATH_SEP);
137 let mut part = json;
138 for p in parts.filter(|p| !p.trim().is_empty()) {
139 if let Value::Object(o) = part {
140 part = o
141 .get(p)
142 .ok_or_else(|| anyhow::format_err!("Key `{}` was not found in json part `{}`", p, part))?;
143 } else {
144 return Err(anyhow::anyhow!("Could not follow path `{}` as json part is not an object.", p));
145 }
146 }
147 Ok(part.clone())
148 }
149 None => Ok(json.clone()),
150 }
151}
152
153#[derive(Debug)]
154pub struct RefInfo {
155 pub path: Option<String>,
157 pub is_nested: bool,
159 pub document_path: DocumentPath,
161 pub ref_friendly_name: Option<String>,
163 }
167
168impl RefInfo {
169 pub fn parse(doc_path: &DocumentPath, ref_value: &str) -> Result<Self, anyhow::Error> {
170 let mut parts = ref_value.split(SHARP_SEP);
171
172 let (ref_doc_path, path) = match (parts.next(), parts.next(), parts.next()) {
173 (_, _, Some(_)) => {
174 return Err(anyhow::anyhow!(
175 "There should be no more than 2 parts separated by # in a reference path."
176 ))
177 }
178 (Some(file), None, None) => (DocumentPath::parse(file)?.relate_from(doc_path)?, None),
179 (Some(""), Some(p), None) => (doc_path.clone(), Some(p.to_string())),
180 (Some(file), Some(p), None) => (DocumentPath::parse(file)?.relate_from(doc_path)?, Some(p.to_string())),
181 (None, _, _) => unreachable!("Split always returns at least one element"),
182 };
183
184 let is_nested: bool = doc_path == &ref_doc_path;
185 let ref_friendly_name = path.as_ref().map(|p| p.split('/').last().unwrap_or_default().to_string());
186
187 Ok(Self {
188 path,
189 is_nested,
190 document_path: ref_doc_path,
191 ref_friendly_name,
192 })
193 }
194}
195
196#[cfg(test)]
197mod test {
198 use super::*;
199 use crate::loader::*;
200 use serde_json::json;
201 use test_case::test_case;
202
203 #[test]
204 fn resolve_reference_test() -> Result<(), anyhow::Error> {
205 let json = json!({
206 "test": {
207 "data1": {
208 "value": 42
209 },
210 "data2": [
211 1,2,3
212 ]
213 },
214 "myref": {
215 "data": "test"
216 }
217 });
218
219 assert_eq!(
220 fetch_reference_value(&json, &Some("/test/data1/value".into()))?,
221 Value::Number(serde_json::Number::from(42))
222 );
223
224 assert_eq!(fetch_reference_value(&json, &Some("/test/data1".into()))?, json!({ "value": 42 }));
225
226 let path: &str = "/test/not_existing_path";
227 let failed_test = fetch_reference_value(&json, &Some(path.into()));
228 let err = failed_test.expect_err("Should be an error");
229 assert_eq!(
230 err.to_string(),
231 "Key `not_existing_path` was not found in json part `{\"data1\":{\"value\":42},\"data2\":[1,2,3]}`"
232 );
233
234 Ok(())
235 }
236
237 #[test]
238 fn resolve_refs_test_0() -> Result<(), anyhow::Error> {
239 let json = json!({
240 "test": {
241 "$ref": "#/myref"
242 },
243 "myref": {
244 "data": "test"
245 }
246 });
247
248 let expected = json!({
249 "test": {
250 "data": "test",
251 "x-fromRef": "#/myref",
252 "x-refName": "myref",
253 },
254 "myref": {
255 "data": "test"
256 }
257 });
258
259 let resolved = resolve_refs_raw(json)?;
260 println!("{}", resolved.to_string());
261 println!("{}", expected.to_string());
262 assert_eq!(resolved, expected);
263 Ok(())
264 }
265
266 #[test]
267 fn resolve_refs_test_1() -> Result<(), anyhow::Error> {
268 let json = json!({
269 "test": {
270 "$ref": "#myref"
271 },
272 "myref": {
273 "data": "test"
274 }
275 });
276
277 let expected = json!({
278 "test": {
279 "data": "test",
280 "x-fromRef": "#myref",
281 "x-refName": "myref",
282 },
283 "myref": {
284 "data": "test"
285 }
286 });
287
288 let resolved = resolve_refs_raw(json)?;
289 println!("{}", resolved.to_string());
290 println!("{}", expected.to_string());
291 assert_eq!(resolved, expected);
292 Ok(())
293 }
294
295 #[test]
296 fn resolve_refs_test_2() -> Result<(), anyhow::Error> {
297 let json = json!({
298 "test": {
299 "data1": {
300 "$ref": "#/myref"
301 },
302 "data2": {
303 "$ref": "#/myref"
304 }
305 },
306 "myref": {
307 "data": "test"
308 }
309 });
310
311 let expected = json!({
312 "test": {
313 "data1": {
314 "data": "test",
315 "x-fromRef": "#/myref",
316 "x-refName": "myref"
317 },
318 "data2": {
319 "data": "test",
320 "x-fromRef": "#/myref",
321 "x-refName": "myref"
322 }
323 },
324 "myref": {
325 "data": "test"
326 }
327 });
328
329 let resolved = resolve_refs_raw(json)?;
330 println!("{}", resolved.to_string());
331 println!("{}", expected.to_string());
332 assert_eq!(resolved, expected);
333 Ok(())
334 }
335
336 #[test]
337 fn resolve_refs_test_3() -> Result<(), anyhow::Error> {
338 let json = json!({
339 "test": {
340 "data1": {
341 "$ref": "#/myref"
342 },
343 "data2": {
344 "$ref": "#/myref"
345 }
346 },
347 "myref": {
348 "data": {
349 "$ref": "#/myref2"
350 }
351 },
352 "myref2": {
353 "content": {
354 "data": "test"
355 }
356 }
357 });
358
359 let expected = json!({
360 "test": {
361 "data1": {
362 "data": {
363 "content": {
364 "data": "test"
365 },
366 "x-fromRef": "#/myref2",
367 "x-refName": "myref2"
368 },
369 "x-fromRef": "#/myref",
370 "x-refName": "myref"
371 },
372 "data2": {
373 "data": {
374 "content": {
375 "data": "test"
376 },
377 "x-fromRef": "#/myref2",
378 "x-refName": "myref2"
379 },
380 "x-fromRef": "#/myref",
381 "x-refName": "myref"
382 }
383 },
384 "myref": {
385 "data": {
386 "content": {
387 "data": "test"
388 },
389 "x-fromRef": "#/myref2",
390 "x-refName": "myref2"
391 }
392 },
393 "myref2": {
394 "content": {
395 "data": "test"
396 }
397 }
398 });
399
400 let resolved = resolve_refs_raw(json)?;
401 println!("{}", resolved.to_string());
402 println!("{}", expected.to_string());
403 assert_eq!(resolved, expected);
404 Ok(())
405 }
406
407 #[test]
408 fn resolve_refs_test_4() -> Result<(), anyhow::Error> {
409 let json = json!({
410 "test": {
411 "data1": {
412 "$ref": "#/myref"
413 },
414 "data2": {
415 "$ref": "#/myref"
416 }
417 },
418 "myref": {
419 "data": {
420 "$ref": "#/myref2"
421 }
422 },
423 "myref2": {
424 "content": {
425 "data": "test"
426 }
427 }
428 });
429
430 let expected = json!({
431 "test": {
432 "data1": {
433 "data": {
434 "content": {
435 "data": "test"
436 },
437 "x-fromRef": "#/myref2",
438 "x-refName": "myref2"
439 },
440 "x-fromRef": "#/myref",
441 "x-refName": "myref"
442 },
443 "data2": {
444 "data": {
445 "content": {
446 "data": "test"
447 },
448 "x-fromRef": "#/myref2",
449 "x-refName": "myref2"
450 },
451 "x-fromRef": "#/myref",
452 "x-refName": "myref"
453 }
454 },
455 "myref": {
456 "data": {
457 "content": {
458 "data": "test"
459 },
460 "x-fromRef": "#/myref2",
461 "x-refName": "myref2"
462 }
463 },
464 "myref2": {
465 "content": {
466 "data": "test"
467 }
468 }
469 });
470
471 let resolved = resolve_refs_raw(json)?;
472 println!("{}", resolved.to_string());
473 println!("{}", expected.to_string());
474 assert_eq!(resolved, expected);
475 Ok(())
476 }
477
478 #[test]
479 fn should_resolve_nested_references() -> Result<(), anyhow::Error> {
480 let json = DocumentPath::parse("_samples/resolver/petshop.yaml")?.load_raw()?;
481 let json = resolve_refs_raw(json)?;
482 let string = json.to_string();
483 assert!(!string.contains(REF));
484 Ok(())
485 }
486
487 #[test]
488 fn should_resolve_external_references() -> Result<(), anyhow::Error> {
489 let document = DocumentPath::parse("_samples/resolver/petshop_with_external.yaml")?;
490 let json = resolve_refs(document)?;
491 let string = json.to_string();
492 assert!(!string.contains(REF));
493 Ok(())
494 }
495
496 #[rustfmt::skip]
497 #[test_case("", "", true, "", None, None)]
498 #[test_case("_samples/petshop.yaml", "../test.json", false, "test.json", None, None)]
499 #[test_case("_samples/petshop.yaml", "test.json", false, "_samples/test.json", None, None)]
500 #[test_case("_samples/petshop.yaml", "#test", true, "_samples/petshop.yaml", Some("test"), Some("test"))]
501 #[test_case("_samples/petshop.yaml", "test.json#test", false, "_samples/test.json", Some("test"), Some("test"))]
502 #[test_case("_samples/petshop.yaml", "http://google.com/test.json#test", false, "http://google.com/test.json", Some("test"), Some("test"))]
503 #[test_case("test.yaml", "test.yaml#/path", true, "test.yaml", Some("/path"), Some("path"))]
504 #[test_case("https://petstore.swagger.io/v2/swagger.json", "#/definitions/Pet", true, "https://petstore.swagger.io/v2/swagger.json", Some("/definitions/Pet"), Some("Pet"))]
505 #[test_case("https://petstore.swagger.io/v2/swagger.json", "http://google.com/test.json#test", false, "http://google.com/test.json", Some("test"), Some("test"))]
506 #[test_case("https://petstore.swagger.io/v2/swagger.json", "http://google.com/test.json", false, "http://google.com/test.json", None, None)]
507 #[test_case("https://petstore.swagger.io/v2/swagger.json", "../test.json", false, "https://petstore.swagger.io/test.json", None, None)]
508 #[test_case("https://petstore.swagger.io/v2/swagger.json", "../test.json#fragment", false, "https://petstore.swagger.io/test.json", Some("fragment"), Some("fragment"))]
509 fn refinfo_parse_tests(
510 current_doc: &str,
511 ref_path: &str,
512 expected_is_nested: bool,
513 expected_document_path: &str,
514 expected_path: Option<&str>,
515 expected_ref_friendly_name: Option<&str>,
516 ) {
517 let current_doc = DocumentPath::parse(current_doc).expect("?");
518 let ref_info = RefInfo::parse(¤t_doc, ref_path).expect("Should work");
519 assert_eq!(ref_info.path, expected_path.map(|s| s.to_string()));
520 assert_eq!(ref_info.is_nested, expected_is_nested);
521 assert_eq!(ref_info.document_path, DocumentPath::parse(expected_document_path).expect("?"));
522 assert_eq!(ref_info.ref_friendly_name, expected_ref_friendly_name.map(|s| s.to_string()));
523 }
524
525 #[test]
526 fn reference_with_more_than_1_sharp_should_fail() {
527 let failed = RefInfo::parse(&DocumentPath::None, "you.shall#not#path");
528 let err = failed.expect_err("Should be an error");
529 assert_eq!(
530 err.to_string(),
531 "There should be no more than 2 parts separated by # in a reference path."
532 );
533 }
534
535 #[test]
536 fn very_tricky_test() -> Result<(), anyhow::Error> {
537 let document = DocumentPath::parse("_samples/resolver/simple1.yaml")?;
538 let json = resolve_refs(document)?;
539 let string = json.to_string();
540 assert!(!string.contains(REF));
541
542 let expected = json!({
543 "test": {
544 "this": "will load multiple files"
545 },
546 "finalvalue": {
547 "value": "this is the real final value"
548 },
549 "value": {
550 "subvalue": {
551 "value": "this is the real final value",
552 "x-fromRef": "simple3.yaml#/subSubValue/value",
553 "x-refName": "value"
554 },
555 "x-fromRef": "simple2.json",
556 "x-refName": ""
557 }
558 });
559 assert_eq!(json, expected);
560
561 Ok(())
562 }
563}