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 Url(Url),
10 FileName(String),
12 None,
14}
15
16#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)]
17pub(crate) enum FormatHint {
18 Json,
20 Yaml,
22 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 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}