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 #[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 #[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 #[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 Url(Url),
47 FileName(String),
49 None,
51}
52
53#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)]
54pub(crate) enum FormatHint {
55 Json,
57 Yaml,
59 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}