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