use crate::errors::JslError;
use failure::{bail, Error};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::ToOwned;
use std::collections::HashMap;
use url::Url;
#[derive(Clone, PartialEq, Debug)]
pub struct Schema {
root: Option<RootData>,
form: Box<Form>,
extra: HashMap<String, Value>,
}
impl Schema {
pub fn new_empty() -> Self {
Self {
root: None,
form: Box::new(Form::Empty),
extra: HashMap::new(),
}
}
pub fn from_serde(serde_schema: Serde) -> Result<Self, Error> {
let base = if let Some(ref id) = serde_schema.id {
Some(id.parse()?)
} else {
None
};
Self::_from_serde(&base, true, serde_schema)
}
fn _from_serde(base: &Option<Url>, root: bool, serde_schema: Serde) -> Result<Self, Error> {
let root = if root {
let defs = if let Some(defs) = serde_schema.defs {
let mut out = HashMap::new();
for (name, sub_schema) in defs {
out.insert(name, Self::_from_serde(base, false, sub_schema)?);
}
out
} else {
HashMap::new()
};
Some(RootData {
id: base.clone(),
defs,
})
} else {
None
};
let mut form = Form::Empty;
if let Some(rxf) = serde_schema.rxf {
let (uri, def) = if let Some(ref base) = base {
let mut resolved = base.join(&rxf)?;
let frag = resolved.fragment().and_then(|f| {
if f.is_empty() {
None
} else {
Some(f.to_owned())
}
});
resolved.set_fragment(None);
(Some(resolved), frag)
} else {
if rxf.is_empty() || rxf == "#" {
(None, None)
} else if rxf.starts_with('#') {
(None, Some(rxf[1..].to_owned()))
} else {
let mut resolved: Url = rxf.parse()?;
let frag = resolved.fragment().map(ToOwned::to_owned);
resolved.set_fragment(None);
(Some(resolved), frag)
}
};
form = Form::Ref(uri, def)
}
if let Some(typ) = serde_schema.typ {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
form = Form::Type(match typ.as_ref() {
"null" => Type::Null,
"boolean" => Type::Boolean,
"number" => Type::Number,
"string" => Type::String,
_ => bail!(JslError::InvalidForm),
});
}
if let Some(elements) = serde_schema.elems {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
form = Form::Elements(Self::_from_serde(base, false, *elements)?);
}
if serde_schema.props.is_some() || serde_schema.opt_props.is_some() {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
let has_required = serde_schema.props.is_some();
let mut required = HashMap::new();
for (name, sub_schema) in serde_schema.props.unwrap_or_default() {
required.insert(name, Self::_from_serde(base, false, sub_schema)?);
}
let mut optional = HashMap::new();
for (name, sub_schema) in serde_schema.opt_props.unwrap_or_default() {
if required.contains_key(&name) {
bail!(JslError::AmbiguousProperty { property: name });
}
optional.insert(name, Self::_from_serde(base, false, sub_schema)?);
}
form = Form::Properties(required, optional, has_required);
}
if let Some(values) = serde_schema.values {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
form = Form::Values(Self::_from_serde(base, false, *values)?);
}
if let Some(discriminator) = serde_schema.discriminator {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
let mut mapping = HashMap::new();
for (name, sub_schema) in discriminator.mapping {
let sub_schema = Self::_from_serde(base, false, sub_schema)?;
match sub_schema.form.as_ref() {
Form::Properties(required, optional, _) => {
if required.contains_key(&discriminator.tag)
|| optional.contains_key(&discriminator.tag)
{
bail!(JslError::AmbiguousProperty {
property: discriminator.tag,
});
}
}
_ => bail!(JslError::InvalidForm),
};
mapping.insert(name, sub_schema);
}
form = Form::Discriminator(discriminator.tag, mapping);
}
Ok(Self {
root,
form: Box::new(form),
extra: HashMap::new(),
})
}
pub fn is_root(&self) -> bool {
self.root.is_some()
}
pub fn root_data(&self) -> &Option<RootData> {
&self.root
}
pub fn root_data_mut(&mut self) -> &mut Option<RootData> {
&mut self.root
}
pub fn root_data_root(self) -> Option<RootData> {
self.root
}
pub fn form(&self) -> &Form {
&self.form
}
pub fn form_mut(&mut self) -> &mut Form {
&mut self.form
}
pub fn into_form(self) -> Form {
*self.form
}
pub fn extra(&self) -> &HashMap<String, Value> {
&self.extra
}
pub fn extra_mut(&mut self) -> &mut HashMap<String, Value> {
&mut self.extra
}
pub fn into_extra(self) -> HashMap<String, Value> {
self.extra
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RootData {
id: Option<Url>,
defs: HashMap<String, Schema>,
}
impl RootData {
pub fn is_anonymous(&self) -> bool {
self.id.is_none()
}
pub fn id(&self) -> &Option<Url> {
&self.id
}
pub fn id_mut(&mut self) -> &mut Option<Url> {
&mut self.id
}
pub fn into_id(self) -> Option<Url> {
self.id
}
pub fn definitions(&self) -> &HashMap<String, Schema> {
&self.defs
}
pub fn definitions_mut(&mut self) -> &mut HashMap<String, Schema> {
&mut self.defs
}
pub fn into_definitions(self) -> HashMap<String, Schema> {
self.defs
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Form {
Empty,
Ref(Option<Url>, Option<String>),
Type(Type),
Elements(Schema),
Properties(HashMap<String, Schema>, HashMap<String, Schema>, bool),
Values(Schema),
Discriminator(String, HashMap<String, Schema>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Type {
Null,
Boolean,
Number,
String,
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)]
pub struct Serde {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "definitions")]
pub defs: Option<HashMap<String, Serde>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "ref")]
pub rxf: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub typ: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "elements")]
pub elems: Option<Box<Serde>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "properties")]
pub props: Option<HashMap<String, Serde>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "optionalProperties")]
pub opt_props: Option<HashMap<String, Serde>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<Box<Serde>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<SerdeDiscriminator>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)]
pub struct SerdeDiscriminator {
#[serde(rename = "tag")]
pub tag: String,
pub mapping: HashMap<String, Serde>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn roundtrip_json() {
let data = r#"{
"id": "http://example.com/foo",
"definitions": {
"a": {}
},
"ref": "http://example.com/bar",
"type": "foo",
"elements": {},
"properties": {
"a": {}
},
"optionalProperties": {
"a": {}
},
"values": {},
"discriminator": {
"tag": "foo",
"mapping": {
"a": {}
}
},
"extra": "foo"
}"#;
let parsed: Serde = serde_json::from_str(data).expect("failed to parse json");
assert_eq!(
parsed,
Serde {
id: Some("http://example.com/foo".to_owned()),
rxf: Some("http://example.com/bar".to_owned()),
defs: Some(
[("a".to_owned(), Serde::default())]
.iter()
.cloned()
.collect()
),
typ: Some("foo".to_owned()),
elems: Some(Box::new(Serde::default())),
props: Some(
[("a".to_owned(), Serde::default())]
.iter()
.cloned()
.collect()
),
opt_props: Some(
[("a".to_owned(), Serde::default())]
.iter()
.cloned()
.collect()
),
values: Some(Box::new(Serde::default())),
discriminator: Some(SerdeDiscriminator {
tag: "foo".to_owned(),
mapping: [("a".to_owned(), Serde::default())]
.iter()
.cloned()
.collect(),
}),
extra: [("extra".to_owned(), json!("foo"))]
.iter()
.cloned()
.collect(),
}
);
let round_trip = serde_json::to_string_pretty(&parsed).expect("failed to serialize json");
assert_eq!(round_trip, data);
}
#[test]
fn from_serde_root() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"definitions": {
"a": { "type": "null" }
}
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: [(
"a".to_owned(),
Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
},
)]
.iter()
.cloned()
.collect(),
}),
form: Box::new(Form::Empty),
extra: HashMap::new()
}
);
}
#[test]
fn from_serde_empty() {
assert_eq!(
Schema::from_serde(serde_json::from_value(json!({})).unwrap()).unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Empty),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_ref() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"ref": ""
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(None, None)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"ref": "#"
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(None, None)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"ref": ""
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(
Some("http://example.com/foo".parse().unwrap()),
None
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"ref": "#"
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(
Some("http://example.com/foo".parse().unwrap()),
None
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"ref": "/bar"
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(
Some("http://example.com/bar".parse().unwrap()),
None
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"ref": "#asdf"
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(
Some("http://example.com/foo".parse().unwrap()),
Some("asdf".to_owned()),
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"id": "http://example.com/foo",
"ref": "/bar#asdf"
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: Some("http://example.com/foo".parse().unwrap()),
defs: HashMap::new(),
}),
form: Box::new(Form::Ref(
Some("http://example.com/bar".parse().unwrap()),
Some("asdf".to_owned()),
)),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_type() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "null",
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
},
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "boolean",
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
},
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "number",
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Type(Type::Number)),
extra: HashMap::new(),
},
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "string",
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Type(Type::String)),
extra: HashMap::new(),
},
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"type": "nonsense",
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_elements() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"elements": {
"type": "null",
},
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Elements(Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
})),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_properties() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"properties": {
"a": { "type": "null" },
},
"optionalProperties": {
"b": { "type": "null" },
},
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Properties(
[(
"a".to_owned(),
Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
[(
"b".to_owned(),
Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
true,
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"optionalProperties": {
"b": { "type": "null" },
},
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Properties(
HashMap::new(),
[(
"b".to_owned(),
Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
false,
)),
extra: HashMap::new(),
}
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"properties": {
"a": { "type": "null" },
},
"optionalProperties": {
"a": { "type": "null" },
},
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_values() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"values": {
"type": "null",
},
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Values(Schema {
root: None,
form: Box::new(Form::Type(Type::Null)),
extra: HashMap::new(),
})),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_discriminator() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"discriminator": {
"tag": "foo",
"mapping": {
"a": { "properties": {} },
"b": { "properties": {} },
},
},
}))
.unwrap()
)
.unwrap(),
Schema {
root: Some(RootData {
id: None,
defs: HashMap::new(),
}),
form: Box::new(Form::Discriminator(
"foo".to_owned(),
[
(
"a".to_owned(),
Schema {
root: None,
form: Box::new(Form::Properties(
HashMap::new(),
HashMap::new(),
true
)),
extra: HashMap::new(),
}
),
(
"b".to_owned(),
Schema {
root: None,
form: Box::new(Form::Properties(
HashMap::new(),
HashMap::new(),
true
)),
extra: HashMap::new(),
}
)
]
.iter()
.cloned()
.collect(),
)),
extra: HashMap::new(),
}
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"discriminator": {
"tag": "foo",
"mapping": {
"a": { "type": "null" },
}
},
}))
.unwrap()
)
.is_err());
assert!(Schema::from_serde(
serde_json::from_value(json!({
"discriminator": {
"tag": "foo",
"mapping": {
"a": {
"properties": {
"foo": { "type": "null" },
},
},
},
},
}))
.unwrap()
)
.is_err());
}
}