codegenr_lib/
loader.rs

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