codegenr_lib/
loader.rs

1use path_dedot::ParseDot;
2use serde_json::{Map, Value};
3use std::{fs::File, io::Read, path::Path};
4use url::Url;
5
6#[derive(Debug, PartialEq, Clone, Hash, Eq)]
7pub enum DocumentPath {
8  /// Full url to a file : https://mywebsite/api.yaml
9  Url(Url),
10  /// File name or relative file name
11  FileName(String),
12  /// json or yaml out of thin silicon
13  None,
14}
15
16#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)]
17pub(crate) enum FormatHint {
18  /// The content should be json
19  Json,
20  /// The content should be yaml
21  Yaml,
22  /// We have no f.....g idea
23  NoIdea,
24}
25
26impl DocumentPath {
27  pub fn parse(ref_path: &str) -> Result<Self, anyhow::Error> {
28    Ok(if ref_path.trim() == "" {
29      Self::None
30    } else {
31      match Url::parse(ref_path) {
32        Ok(url) => DocumentPath::Url(url),
33        Err(_) => DocumentPath::FileName(ref_path.into()),
34      }
35    })
36  }
37
38  pub fn relate_from(self, refed_from: &Self) -> Result<Self, anyhow::Error> {
39    use DocumentPath::*;
40    Ok(match (refed_from, self) {
41      (Url(_), Url(url)) => Url(url),
42      (Url(url_from), FileName(path_to)) => {
43        let mut url = url_from.clone();
44        url.path_segments_mut().map_err(|_| anyhow::anyhow!("Url cannot be a base."))?.pop();
45        let path = url.path();
46        let new_path = Path::new(path).join(&path_to);
47        let new_path = new_path.parse_dot()?;
48        let new_path = new_path
49          .to_str()
50          .ok_or_else(|| anyhow::anyhow!("Unable to append path `{}` to `{}`", path_to, url_from))?;
51        url.set_path(new_path);
52        Url(url)
53      }
54      (Url(_), None) => refed_from.clone(),
55      (FileName(path_from), FileName(path_to)) => {
56        let folder = Path::new(path_from)
57          .parent()
58          .ok_or_else(|| anyhow::anyhow!("The origin path should be a file and have parent."))?;
59        folder
60          .join(&path_to)
61          .parse_dot()?
62          .to_str()
63          .map(|s| FileName(s.to_owned()))
64          .ok_or_else(|| anyhow::anyhow!("Unable to append path `{}` to `{}`", path_to, path_from))?
65      }
66      (FileName(_), Url(url)) => Url(url),
67      (FileName(_path_from), None) => refed_from.clone(),
68      (None, s) => s,
69    })
70  }
71
72  pub(crate) fn guess_format(&self) -> FormatHint {
73    let s = match self {
74      DocumentPath::Url(url) => url.as_str(),
75      DocumentPath::FileName(s) => s,
76      DocumentPath::None => return FormatHint::NoIdea,
77    };
78    if s.ends_with(".json") {
79      FormatHint::Json
80    } else if s.ends_with(".yaml") || s.ends_with(".yml") {
81      FormatHint::Yaml
82    } else {
83      FormatHint::NoIdea
84    }
85  }
86
87  pub fn load_raw(&self) -> Result<Value, anyhow::Error> {
88    let hint = self.guess_format();
89    match self {
90      DocumentPath::Url(url) => {
91        let body = reqwest::blocking::get(url.clone())?.text()?;
92        json_from_string(&body, hint)
93      }
94      DocumentPath::FileName(file_name) => {
95        let mut file = File::open(file_name)?;
96        let mut content = String::new();
97        file.read_to_string(&mut content)?;
98        json_from_string(&content, hint)
99      }
100      DocumentPath::None => unreachable!("This is a non sense to try loading a 'None' document path."),
101    }
102  }
103}
104
105fn json_from_string(content: &str, hint: FormatHint) -> Result<Value, anyhow::Error> {
106  match hint {
107    FormatHint::Json | FormatHint::NoIdea => {
108      let json_error = match serde_json::from_str(content) {
109        Ok(json) => return Ok(json),
110        Err(e) => e,
111      };
112
113      let yaml_error = match serde_yaml::from_str(content) {
114        Ok(yaml) => return yaml_to_json(yaml),
115        Err(e) => e,
116      };
117
118      Err(anyhow::anyhow!(
119        "Could not read file content as json:\n-json_error: `{}`\n-yaml_error:`{}`",
120        json_error,
121        yaml_error,
122      ))
123    }
124    FormatHint::Yaml => {
125      let yaml_error = match serde_yaml::from_str(content) {
126        Ok(yaml) => return yaml_to_json(yaml),
127        Err(e) => e,
128      };
129
130      let json_error = match serde_json::from_str(content) {
131        Ok(json) => return Ok(json),
132        Err(e) => e,
133      };
134
135      Err(anyhow::anyhow!(
136        "Could not read file content as json:\n-yaml_error:`{}`\n-json_error: `{}`",
137        yaml_error,
138        json_error,
139      ))
140    }
141  }
142}
143
144fn yaml_to_json(yaml: serde_yaml::Value) -> Result<Value, anyhow::Error> {
145  use serde_yaml::Value::*;
146  Ok(match yaml {
147    Null => Value::Null,
148    Bool(b) => Value::Bool(b),
149    Number(n) => Value::Number(yaml_to_json_number(n)?),
150    String(s) => Value::String(s),
151    Sequence(values) => Value::Array(values.into_iter().map(yaml_to_json).collect::<Result<Vec<_>, _>>()?),
152    Mapping(map) => {
153      let mut json = Map::<_, _>::with_capacity(map.len());
154      for (key, value) in map {
155        if let String(s) = key {
156          json.insert(s, yaml_to_json(value)?);
157        } else {
158          return Err(anyhow::anyhow!("Object keys should be strings."));
159        }
160      }
161      Value::Object(json)
162    }
163  })
164}
165
166fn yaml_to_json_number(n: serde_yaml::Number) -> Result<serde_json::Number, anyhow::Error> {
167  use serde_json::Number;
168  let number = if n.is_f64() {
169    let f = n.as_f64().ok_or_else(|| anyhow::format_err!("The number should be an f64."))?;
170    Number::from_f64(f).ok_or_else(|| anyhow::format_err!("The number couldn't map to json."))?
171  } else if n.is_u64() {
172    let u = n.as_u64().ok_or_else(|| anyhow::format_err!("The number should be an u64."))?;
173    Number::from(u)
174  } else if n.is_i64() {
175    let u = n.as_i64().ok_or_else(|| anyhow::format_err!("The number should be an i64."))?;
176    Number::from(u)
177  } else {
178    return Err(anyhow::anyhow!("There is a new number flavor in yaml ?"));
179  };
180  Ok(number)
181}
182
183#[cfg(test)]
184mod test {
185  use super::*;
186  use test_case::test_case;
187
188  #[test_case("h://f", "h://f", "h://f")]
189  #[test_case("h://w.com/api.yaml", "components.yaml", "h://w.com/components.yaml")]
190  #[test_case("h://w.com/v1/api.yaml", "../v2/components.yaml", "h://w.com/v2/components.yaml")]
191  #[test_case("file.yaml", "other.json", "other.json")]
192  #[test_case("test/file.yaml", "other.json", "test/other.json")]
193  #[test_case("test/file.yaml", "./other2.json", "test/other2.json")]
194  #[test_case("test/file.yaml", "../other3.json", "other3.json")]
195  #[test_case("test/file.yaml", "plop/other.json", "test/plop/other.json")]
196  #[test_case("file.yaml", "http://w.com/other.json", "http://w.com/other.json")]
197  #[test_case("file.json", "", "file.json")]
198  #[test_case("", "f", "f")]
199  #[test_case("", "h://f", "h://f")]
200  #[test_case("_samples/petshop_with_external.yaml", "petshop_externals.yaml", "_samples/petshop_externals.yaml")]
201  // #[test_case(
202  //   "./_samples/petshop_with_external.yaml",
203  //   "petshop_externals.yaml",
204  //   "./_samples/petshop_externals.yaml"
205  // )]
206  fn relate_test(doc_path: &str, ref_path: &str, expected_related: &str) {
207    let doc_path = DocumentPath::parse(doc_path).expect("?");
208    let r_path = DocumentPath::parse(ref_path).expect("?");
209    let expected_related = DocumentPath::parse(expected_related).expect("?");
210    let related = r_path.relate_from(&doc_path).expect("?");
211    assert_eq!(related, expected_related);
212  }
213
214  #[test]
215  fn read_json_file_test() -> Result<(), anyhow::Error> {
216    let _result = DocumentPath::parse("./_samples/resolver/Merge1_rest.json")?.load_raw()?;
217    Ok(())
218  }
219
220  #[test]
221  fn read_yaml_file_test() -> Result<(), anyhow::Error> {
222    let _result = DocumentPath::parse("./_samples/resolver/Merge1.yaml")?.load_raw()?;
223    Ok(())
224  }
225
226  #[test]
227  #[ignore]
228  fn read_beezup_openapi() -> Result<(), anyhow::Error> {
229    let _result = DocumentPath::parse("https://api-docs.beezup.com/swagger.json")?.load_raw()?;
230    Ok(())
231  }
232
233  #[test]
234  fn yaml_to_json_tests() -> Result<(), anyhow::Error> {
235    use serde_yaml::Value::*;
236    assert_eq!(yaml_to_json(Null)?, Value::Null);
237    assert_eq!(yaml_to_json(Bool(true))?, Value::Bool(true));
238    assert_eq!(yaml_to_json(Bool(false))?, Value::Bool(false));
239    assert_eq!(yaml_to_json(String("test".into()))?, Value::String("test".into()));
240    assert_eq!(
241      yaml_to_json(Number(serde_yaml::from_str("2")?))?,
242      Value::Number(serde_json::from_str("2")?)
243    );
244
245    assert_eq!(
246      yaml_to_json(Sequence(vec!(Null, Bool(true), String("test".into()))))?,
247      Value::Array(vec!(Value::Null, Value::Bool(true), Value::String("test".into())))
248    );
249
250    let mut map = serde_yaml::Mapping::new();
251    map.insert(String("key".into()), String("value".into()));
252    let mut expected = Map::new();
253    expected.insert("key".into(), Value::String("value".into()));
254
255    assert_eq!(yaml_to_json(Mapping(map))?, Value::Object(expected));
256
257    let mut map = serde_yaml::Mapping::new();
258    map.insert(Null, String("value".into()));
259    let expected_failed = yaml_to_json(Mapping(map));
260    let e = expected_failed.expect_err("Should be an error");
261    assert_eq!(e.to_string(), "Object keys should be strings.");
262
263    Ok(())
264  }
265
266  #[test]
267  fn yaml_to_json_number_tests() -> Result<(), anyhow::Error> {
268    use serde_yaml::Number;
269
270    let expected_failed_for_f64 = yaml_to_json_number(Number::from(f64::INFINITY));
271    let f64_error = expected_failed_for_f64.expect_err("Should be an error");
272    assert_eq!(f64_error.to_string(), "The number couldn't map to json.");
273
274    let _success_for_f64 = yaml_to_json_number(Number::from(256.2))?;
275    let _success_for_u64 = yaml_to_json_number(Number::from(-42))?;
276    let _success_for_i64 = yaml_to_json_number(Number::from(42))?;
277    let _success_for_neg_value = yaml_to_json_number(Number::from(-40285.5))?;
278
279    Ok(())
280  }
281}