use serde_json::Value;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::mem;
use std::path::PathBuf;
use url::Url;
use snafu::{Snafu, ResultExt};
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Could not open schema from {}: {}", filename, source))]
SchemaFromFile {
filename: String,
source: std::io::Error,
},
#[snafu(display("Could not open schema from url {}: {}", url, source))]
SchemaFromUrl {
url: String,
source: ureq::Error,
},
#[snafu(display("Parse error for url {}: {}", url, source))]
UrlParseError {
url: String,
source: url::ParseError,
},
#[snafu(display("schema from {} not valid JSON: {}", url, source))]
SchemaNotJson {
url: String,
source: std::io::Error,
},
#[snafu(display("schema from {} not valid JSON: {}", url, source))]
SchemaNotJsonSerde {
url: String,
source: serde_json::Error,
},
#[snafu(display("json pointer {} not found", pointer))]
JsonPointerNotFound {
pointer: String,
},
#[snafu(display("{}", "Json Ref Error"))]
JSONRefError {
source: std::io::Error,
}
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct JsonRef {
schema_cache: HashMap<String, Value>,
reference_key: Option<String>,
}
impl JsonRef {
pub fn new() -> JsonRef {
return JsonRef {
schema_cache: HashMap::new(),
reference_key: None,
};
}
pub fn set_reference_key(&mut self, reference_key: &str) {
self.reference_key = Some(reference_key.to_owned());
}
pub fn deref_value(&mut self, value: &mut Value) -> Result<()> {
let anon_file_url = format!("file://{}/anon.json", env::current_dir().context(JSONRefError {})?.to_string_lossy());
self.schema_cache
.insert(anon_file_url.clone(), value.clone());
self.deref(value, anon_file_url, &vec![])?;
Ok(())
}
pub fn deref_url(&mut self, url: &str) -> Result<Value> {
let mut value: Value = ureq::get(url).call().context(SchemaFromUrl {url: url.to_owned()})?.into_json().context(SchemaNotJson {url: url.to_owned()})?;
self.schema_cache.insert(url.to_string(), value.clone());
self.deref(&mut value, url.to_string(), &vec![])?;
Ok(value)
}
pub fn deref_file(&mut self, file_path: &str) -> Result<Value> {
let file = fs::File::open(file_path).context(SchemaFromFile {filename: file_path.to_owned()})?;
let mut value: Value = serde_json::from_reader(file).context(SchemaNotJsonSerde {url: file_path.to_owned()})?;
let path = PathBuf::from(file_path);
let absolute_path = fs::canonicalize(path).context(JSONRefError {})?;
let url = format!("file://{}", absolute_path.to_string_lossy());
self.schema_cache.insert(url.clone(), value.clone());
self.deref(&mut value, url, &vec![])?;
Ok(value)
}
fn deref(
&mut self,
value: &mut Value,
id: String,
used_refs: &Vec<String>,
) -> Result<()> {
let mut new_id = id;
if let Some(id_value) = value.get("$id") {
if let Some(id_string) = id_value.as_str() {
new_id = id_string.to_string()
}
}
if let Some(obj) = value.as_object_mut() {
if let Some(ref_value) = obj.remove("$ref") {
if let Some(ref_string) = ref_value.as_str() {
let id_url = Url::parse(&new_id).context(UrlParseError {url: new_id.clone()})?;
let ref_url = id_url.join(ref_string).context(UrlParseError {url: ref_string.to_owned()})?;
let mut ref_url_no_fragment = ref_url.clone();
ref_url_no_fragment.set_fragment(None);
let ref_no_fragment = ref_url_no_fragment.to_string();
let mut schema = match self.schema_cache.get(&ref_no_fragment) {
Some(cached_schema) => cached_schema.clone(),
None => {
if ref_no_fragment.starts_with("http") {
ureq::get(&ref_no_fragment)
.call().context(SchemaFromUrl {url: ref_no_fragment.clone()})?
.into_json().context(SchemaNotJson {url: ref_no_fragment.clone()})?
} else if ref_no_fragment.starts_with("file") {
let file = fs::File::open(ref_url_no_fragment.path()).context(SchemaFromFile {filename: ref_no_fragment.clone()})?;
serde_json::from_reader(file).context(SchemaNotJsonSerde {url: ref_no_fragment.clone()} )?
} else {
panic!("need url to be a file or a http based url")
}
}
};
if !self.schema_cache.contains_key(&ref_no_fragment) {
self.schema_cache
.insert(ref_no_fragment.clone(), schema.clone());
}
let ref_url_string = ref_url.to_string();
if let Some(ref_fragment) = ref_url.fragment() {
schema = schema.pointer(ref_fragment).ok_or(
Error::JsonPointerNotFound {pointer: format!("ref `{}` can not be resolved as pointer `{}` can not be found in the schema", ref_string, ref_fragment)}
)?.clone();
}
if used_refs.contains(&ref_url_string) {
return Ok(());
}
let mut new_used_refs = used_refs.clone();
new_used_refs.push(ref_url_string);
self.deref(&mut schema, ref_no_fragment, &new_used_refs)?;
let old_value = mem::replace(value, schema);
if let Some(reference_key) = &self.reference_key {
if let Some(new_obj) = value.as_object_mut() {
new_obj.insert(reference_key.clone(), old_value);
}
}
}
}
}
if let Some(obj) = value.as_object_mut() {
for obj_value in obj.values_mut() {
self.deref(obj_value, new_id.clone(), used_refs)?
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::JsonRef;
use serde_json::{json, Value};
use std::fs;
#[test]
fn json_no_refs() {
let no_ref_example = json!({"properties": {"prop1": {"title": "proptitle"}}});
let mut jsonref = JsonRef::new();
let mut input = no_ref_example.clone();
jsonref.deref_value(&mut input).unwrap();
assert_eq!(input, no_ref_example)
}
#[test]
fn json_with_recursion() {
let mut simple_refs_example = json!(
{"properties": {"prop1": {"$ref": "#"}}}
);
let simple_refs_expected = json!(
{"properties": {"prop1": {"properties": {"prop1": {}}}}
}
);
let mut jsonref = JsonRef::new();
jsonref.deref_value(&mut simple_refs_example).unwrap();
jsonref.set_reference_key("__reference__");
println!(
"{}",
serde_json::to_string_pretty(&simple_refs_example).unwrap()
);
assert_eq!(simple_refs_example, simple_refs_expected)
}
#[test]
fn simple_from_url() {
let mut simple_refs_example = json!(
{"properties": {"prop1": {"title": "name"},
"prop2": {"$ref": "https://gist.githubusercontent.com/kindly/35a631d33792413ed8e34548abaa9d61/raw/b43dc7a76cc2a04fde2a2087f0eb389099b952fb/test.json", "title": "old_title"}}
}
);
let simple_refs_expected = json!(
{"properties": {"prop1": {"title": "name"},
"prop2": {"title": "title from url", "__reference__": {"title": "old_title"}}}
}
);
let mut jsonref = JsonRef::new();
jsonref.set_reference_key("__reference__");
jsonref.deref_value(&mut simple_refs_example).unwrap();
assert_eq!(simple_refs_example, simple_refs_expected)
}
#[test]
fn nested_with_ref_from_url() {
let mut simple_refs_example = json!(
{"properties": {"prop1": {"title": "name"},
"prop2": {"$ref": "https://gist.githubusercontent.com/kindly/35a631d33792413ed8e34548abaa9d61/raw/0a691c035251f742e8710f71ba92ead307823385/test_nested.json"}}
}
);
let simple_refs_expected = json!(
{"properties": {"prop1": {"title": "name"},
"prop2": {"__reference__": {},
"title": "title from url",
"properties": {"prop1": {"title": "sub property title in url"},
"prop2": {"__reference__": {}, "title": "sub property title in url"}}
}}
}
);
let mut jsonref = JsonRef::new();
jsonref.set_reference_key("__reference__");
jsonref.deref_value(&mut simple_refs_example).unwrap();
assert_eq!(simple_refs_example, simple_refs_expected)
}
#[test]
fn nested_ref_from_local_file() {
let mut jsonref = JsonRef::new();
jsonref.set_reference_key("__reference__");
let file_example = jsonref
.deref_file("fixtures/nested_relative/base.json")
.unwrap();
let file = fs::File::open("fixtures/nested_relative/expected.json").unwrap();
let file_expected: Value = serde_json::from_reader(file).unwrap();
println!("{}", serde_json::to_string_pretty(&file_example).unwrap());
assert_eq!(file_example, file_expected)
}
}