jsonref/
lib.rs

1//! jsonref dereferences JSONSchema `$ref` attributes and creates a new dereferenced schema.
2//!
3//! Dereferencing is normally done by a JSONSchema validator in the process of validation, but
4//! it is sometimes useful to do this independent of the validator for tasks like:
5//!
6//! * Analysing a schema programatically to see what field there are.
7//! * Programatically modifying a schema.
8//! * Passing to tools that create fake JSON data from the schema.
9//! * Passing the schema to form generation tools.
10//!
11//!
12//! Example:
13//! ```
14//! use serde_json::json;
15//! use jsonref::JsonRef;
16//!
17//! let mut simple_example = json!(
18//!           {"properties": {"prop1": {"title": "name"},
19//!                           "prop2": {"$ref": "#/properties/prop1"}}
20//!           }
21//!        );
22//!
23//! let mut jsonref = JsonRef::new();
24//!
25//! jsonref.deref_value(&mut simple_example).unwrap();
26//!
27//! let dereffed_expected = json!(
28//!     {"properties":
29//!         {"prop1": {"title": "name"},
30//!          "prop2": {"title": "name"}}
31//!     }
32//! );
33//! assert_eq!(simple_example, dereffed_expected)
34//! ```
35//!
36//! **Note**:  If the JSONSchema has recursive `$ref` only the first recursion will happen.
37//! This is to stop an infinate loop.
38
39use serde_json::Value;
40use std::collections::HashMap;
41use std::env;
42use std::fs;
43use std::mem;
44use std::path::PathBuf;
45use url::Url;
46use snafu::{Snafu, ResultExt};
47
48#[derive(Debug, Snafu)]
49pub enum Error {
50    #[snafu(display("Could not open schema from {}: {}", filename, source))]
51    SchemaFromFile {
52        filename: String,
53        source: std::io::Error,
54    },
55    #[snafu(display("Could not open schema from url {}: {}", url, source))]
56    SchemaFromUrl {
57        url: String,
58        source: ureq::Error,
59    },
60    #[snafu(display("Parse error for url {}: {}", url, source))]
61    UrlParseError {
62        url: String,
63        source: url::ParseError,
64    },
65    #[snafu(display("schema from {} not valid JSON: {}", url, source))]
66    SchemaNotJson {
67        url: String,
68        source: std::io::Error,
69    },
70    #[snafu(display("schema from {} not valid JSON: {}", url, source))]
71    SchemaNotJsonSerde {
72        url: String,
73        source: serde_json::Error,
74    },
75    #[snafu(display("json pointer {} not found", pointer))]
76    JsonPointerNotFound {
77        pointer: String,
78    },
79    #[snafu(display("{}", "Json Ref Error"))]
80    JSONRefError {
81        source: std::io::Error,
82    }
83}
84
85type Result<T, E = Error> = std::result::Result<T, E>;
86
87/// Main struct that holds configuration for a JSONScheama derefferencing.
88///
89/// Instantiate with
90/// ```
91/// use jsonref::JsonRef;
92/// let jsonref = JsonRef::new();
93/// ```
94///
95/// Configuration is done through the `set_` methods on the struct.
96#[derive(Debug)]
97pub struct JsonRef {
98    schema_cache: HashMap<String, Value>,
99    reference_key: Option<String>,
100}
101
102impl JsonRef {
103    /// Create a new instance of JsonRef.
104    pub fn new() -> JsonRef {
105        return JsonRef {
106            schema_cache: HashMap::new(),
107            reference_key: None,
108        };
109    }
110
111    /// Set a key to store the data that the `$ref` replaced. 
112    ///
113    /// This example uses `__reference__` as the key.
114    /// 
115    /// ```
116    /// # use jsonref::JsonRef;
117    /// # let jsonref = JsonRef::new();
118    /// use serde_json::json;
119    ///
120    /// let mut input  = json!(
121    ///     {"properties": {"prop1": {"title": "name"},
122    ///                     "prop2": {"$ref": "#/properties/prop1", "title": "old_title"}}
123    ///     }
124    /// );
125    ///
126    /// let expected = json!(
127    ///     {"properties": {"prop1": {"title": "name"},
128    ///                     "prop2": {"title": "name", "__reference__": {"title": "old_title"}}}
129    ///     }
130    /// );
131    ///                                                                                          
132    /// let mut jsonref = JsonRef::new();
133    ///
134    /// jsonref.set_reference_key("__reference__");
135    ///
136    /// jsonref.deref_value(&mut input).unwrap();
137    ///                                                                                          
138    /// assert_eq!(input, expected)
139    /// ```
140
141    pub fn set_reference_key(&mut self, reference_key: &str) {
142        self.reference_key = Some(reference_key.to_owned());
143    }
144
145    /// deref a serde_json value directly. Uses the current working directory for any relative
146    /// refs.
147    pub fn deref_value(&mut self, value: &mut Value) -> Result<()> {
148        let anon_file_url = format!("file://{}/anon.json", env::current_dir().context(JSONRefError {})?.to_string_lossy());
149        self.schema_cache
150            .insert(anon_file_url.clone(), value.clone());
151
152        self.deref(value, anon_file_url, &vec![])?;
153        Ok(())
154    }
155
156    /// deref from a URL:
157    ///
158    /// ```
159    /// # use jsonref::JsonRef;
160    /// # let jsonref = JsonRef::new();
161    /// # use serde_json::Value;
162    /// # use std::fs;
163    /// let mut jsonref = JsonRef::new();
164    /// # jsonref.set_reference_key("__reference__");
165    /// let input_url = jsonref.deref_url("https://gist.githubusercontent.com/kindly/91e09f88ced65aaca1a15d85a56a28f9/raw/52f8477435cff0b73c54aacc70926c101ce6c685/base.json").unwrap();
166    /// # let file = fs::File::open("fixtures/nested_relative/expected.json").unwrap();
167    /// # let file_expected: Value = serde_json::from_reader(file).unwrap();
168    /// # assert_eq!(input_url, file_expected)
169    /// ```
170    pub fn deref_url(&mut self, url: &str) -> Result<Value> {
171        let mut value: Value = ureq::get(url).call().context(SchemaFromUrl {url: url.to_owned()})?.into_json().context(SchemaNotJson {url: url.to_owned()})?;
172
173        self.schema_cache.insert(url.to_string(), value.clone());
174        self.deref(&mut value, url.to_string(), &vec![])?;
175        Ok(value)
176    }
177
178    /// deref from a File:
179    ///
180    /// ```
181    /// # use jsonref::JsonRef;
182    /// # let jsonref = JsonRef::new();
183    /// # use serde_json::Value;
184    /// # use std::fs;
185    ///
186    /// let mut jsonref = JsonRef::new();
187    /// # jsonref.set_reference_key("__reference__");
188    /// let file_example = jsonref
189    ///     .deref_file("fixtures/nested_relative/base.json")
190    ///     .unwrap();
191    /// # let file = fs::File::open("fixtures/nested_relative/expected.json").unwrap();
192    /// # let file_expected: Value = serde_json::from_reader(file).unwrap();
193    /// # assert_eq!(file_example, file_expected)
194    /// ```
195    pub fn deref_file(&mut self, file_path: &str) -> Result<Value> {
196        let file = fs::File::open(file_path).context(SchemaFromFile {filename: file_path.to_owned()})?;
197        let mut value: Value = serde_json::from_reader(file).context(SchemaNotJsonSerde {url: file_path.to_owned()})?;
198        let path = PathBuf::from(file_path);
199        let absolute_path = fs::canonicalize(path).context(JSONRefError {})?;
200        let url = format!("file://{}", absolute_path.to_string_lossy());
201
202        self.schema_cache.insert(url.clone(), value.clone());
203        self.deref(&mut value, url, &vec![])?;
204        Ok(value)
205    }
206
207    fn deref(
208        &mut self,
209        value: &mut Value,
210        id: String,
211        used_refs: &Vec<String>,
212    ) -> Result<()> {
213        let mut new_id = id;
214        if let Some(id_value) = value.get("$id") {
215            if let Some(id_string) = id_value.as_str() {
216                new_id = id_string.to_string()
217            }
218        }
219
220        if let Some(obj) = value.as_object_mut() {
221            if let Some(ref_value) = obj.remove("$ref") {
222                if let Some(ref_string) = ref_value.as_str() {
223                    let id_url = Url::parse(&new_id).context(UrlParseError {url: new_id.clone()})?;
224                    let ref_url = id_url.join(ref_string).context(UrlParseError {url: ref_string.to_owned()})?;
225
226                    let mut ref_url_no_fragment = ref_url.clone();
227                    ref_url_no_fragment.set_fragment(None);
228                    let ref_no_fragment = ref_url_no_fragment.to_string();
229
230                    let mut schema = match self.schema_cache.get(&ref_no_fragment) {
231                        Some(cached_schema) => cached_schema.clone(),
232                        None => {
233                            if ref_no_fragment.starts_with("http") {
234                                ureq::get(&ref_no_fragment)
235                                    .call().context(SchemaFromUrl {url: ref_no_fragment.clone()})?
236                                    .into_json().context(SchemaNotJson {url: ref_no_fragment.clone()})?
237                            } else if ref_no_fragment.starts_with("file") {
238                                let file = fs::File::open(ref_url_no_fragment.path()).context(SchemaFromFile {filename: ref_no_fragment.clone()})?;
239                                serde_json::from_reader(file).context(SchemaNotJsonSerde {url: ref_no_fragment.clone()} )?
240                            } else {
241                                panic!("need url to be a file or a http based url")
242                            }
243                        }
244                    };
245
246                    if !self.schema_cache.contains_key(&ref_no_fragment) {
247                        self.schema_cache
248                            .insert(ref_no_fragment.clone(), schema.clone());
249                    }
250
251                    let ref_url_string = ref_url.to_string();
252                    if let Some(ref_fragment) = ref_url.fragment() {
253                        schema = schema.pointer(ref_fragment).ok_or(
254                            Error::JsonPointerNotFound {pointer: format!("ref `{}` can not be resolved as pointer `{}` can not be found in the schema", ref_string, ref_fragment)}
255                            )?.clone();
256                    }
257                    if used_refs.contains(&ref_url_string) {
258                        return Ok(());
259                    }
260
261                    let mut new_used_refs = used_refs.clone();
262                    new_used_refs.push(ref_url_string);
263
264                    self.deref(&mut schema, ref_no_fragment, &new_used_refs)?;
265                    let old_value = mem::replace(value, schema);
266
267                    if let Some(reference_key) = &self.reference_key {
268                        if let Some(new_obj) = value.as_object_mut() {
269                            new_obj.insert(reference_key.clone(), old_value);
270                        }
271                    }
272                }
273            }
274        }
275
276        if let Some(obj) = value.as_object_mut() {
277            for obj_value in obj.values_mut() {
278                self.deref(obj_value, new_id.clone(), used_refs)?
279            }
280        }
281        Ok(())
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::JsonRef;
288    use serde_json::{json, Value};
289    use std::fs;
290
291    #[test]
292    fn json_no_refs() {
293        let no_ref_example = json!({"properties": {"prop1": {"title": "proptitle"}}});
294
295        let mut jsonref = JsonRef::new();
296
297        let mut input = no_ref_example.clone();
298
299        jsonref.deref_value(&mut input).unwrap();
300
301        assert_eq!(input, no_ref_example)
302    }
303
304    #[test]
305    fn json_with_recursion() {
306        let mut simple_refs_example = json!(
307            {"properties": {"prop1": {"$ref": "#"}}}
308        );
309
310        let simple_refs_expected = json!(
311            {"properties": {"prop1": {"properties": {"prop1": {}}}}
312            }
313        );
314
315        let mut jsonref = JsonRef::new();
316        jsonref.deref_value(&mut simple_refs_example).unwrap();
317        jsonref.set_reference_key("__reference__");
318
319        println!(
320            "{}",
321            serde_json::to_string_pretty(&simple_refs_example).unwrap()
322        );
323
324        assert_eq!(simple_refs_example, simple_refs_expected)
325    }
326
327    #[test]
328    fn simple_from_url() {
329        let mut simple_refs_example = json!(
330            {"properties": {"prop1": {"title": "name"},
331                            "prop2": {"$ref": "https://gist.githubusercontent.com/kindly/35a631d33792413ed8e34548abaa9d61/raw/b43dc7a76cc2a04fde2a2087f0eb389099b952fb/test.json", "title": "old_title"}}
332            }
333        );
334
335        let simple_refs_expected = json!(
336            {"properties": {"prop1": {"title": "name"},
337                            "prop2": {"title": "title from url", "__reference__": {"title": "old_title"}}}
338            }
339        );
340
341        let mut jsonref = JsonRef::new();
342        jsonref.set_reference_key("__reference__");
343        jsonref.deref_value(&mut simple_refs_example).unwrap();
344
345        assert_eq!(simple_refs_example, simple_refs_expected)
346    }
347
348    #[test]
349    fn nested_with_ref_from_url() {
350        let mut simple_refs_example = json!(
351            {"properties": {"prop1": {"title": "name"},
352                            "prop2": {"$ref": "https://gist.githubusercontent.com/kindly/35a631d33792413ed8e34548abaa9d61/raw/0a691c035251f742e8710f71ba92ead307823385/test_nested.json"}}
353            }
354        );
355
356        let simple_refs_expected = json!(
357            {"properties": {"prop1": {"title": "name"},
358                            "prop2": {"__reference__": {},
359                                      "title": "title from url",
360                                      "properties": {"prop1": {"title": "sub property title in url"},
361                                                     "prop2": {"__reference__": {}, "title": "sub property title in url"}}
362                            }}
363            }
364        );
365
366        let mut jsonref = JsonRef::new();
367        jsonref.set_reference_key("__reference__");
368        jsonref.deref_value(&mut simple_refs_example).unwrap();
369
370        assert_eq!(simple_refs_example, simple_refs_expected)
371    }
372
373    #[test]
374    fn nested_ref_from_local_file() {
375        let mut jsonref = JsonRef::new();
376        jsonref.set_reference_key("__reference__");
377        let file_example = jsonref
378            .deref_file("fixtures/nested_relative/base.json")
379            .unwrap();
380
381        let file = fs::File::open("fixtures/nested_relative/expected.json").unwrap();
382        let file_expected: Value = serde_json::from_reader(file).unwrap();
383
384        println!("{}", serde_json::to_string_pretty(&file_example).unwrap());
385
386        assert_eq!(file_example, file_expected)
387    }
388
389}