codegenr_lib/
resolver.rs

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  /// Path of the reference to import in the destination file
187  pub path: Option<String>,
188  /// True if the reference is nested in the same document
189  pub is_nested: bool,
190  /// File path of the document containing the reference
191  pub document_path: DocumentPath,
192  /// Last part of the $ref value
193  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(&current_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}