codegenr_lib/
resolver.rs

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  // fn jump<'a>(&'a mut self, parent_document_path: DocumentPath, parent_json: Value) -> RefResolverJump<'a> {
23  //   self.hash.insert(parent_document_path.clone(), parent_json);
24  //   RefResolverJump {
25  //     ref_resolver: self,
26  //     parent_document_path,
27  //   }
28  // }
29}
30
31// pub struct RefResolverJump<'a> {
32//   pub ref_resolver: &'a RefResolver,
33//   pub parent_document_path: DocumentPath,
34// }
35
36// impl<'a> Drop for RefResolverJump<'a> {
37//   fn drop(&mut self) {}
38// }
39
40// #[cfg(test)]
41// mod test2 {
42//   use super::*;
43//   #[test]
44//   fn test() {
45//     let mut rr = RefResolver::new();
46//     let jump = rr.jump(DocumentPath::None, Value::Null);
47//     drop(jump);
48//     let _jump = rr.jump(DocumentPath::None, Value::Null);
49//   }
50// }
51
52// impl RefResolver {
53//   pub fn resolve_from_value(json: Value) -> Result<Value, anyhow::Error> {
54//     todo!()
55//   }
56
57//   pub fn resolve_document(document_path: &str) -> Result<Value, anyhow::Error> {
58//     todo!()
59//   }
60// }
61
62// https://github.com/BeezUP/dotnet-codegen/tree/master/tests/CodegenUP.DocumentRefLoader.Tests
63
64pub 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  /// Path of the reference to import in the destination file
156  pub path: Option<String>,
157  /// True if the reference is nested in the same document
158  pub is_nested: bool,
159  /// File path of the document containing the reference
160  pub document_path: DocumentPath,
161  /// Last part of the $ref value
162  pub ref_friendly_name: Option<String>,
163  // pub abs_doc_uri: Url,
164  // pub is_false_abs_ref: bool,
165  // public Uri AbsoluteDocumentUri { get; }
166}
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(&current_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}