use crate::errors::JslError;
use failure::{bail, Error};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
#[derive(Clone, PartialEq, Debug)]
pub struct Schema {
defs: Option<HashMap<String, Schema>>,
form: Box<Form>,
extra: HashMap<String, Value>,
}
impl Schema {
pub fn from_parts(
defs: Option<HashMap<String, Schema>>,
form: Box<Form>,
extra: HashMap<String, Value>,
) -> Schema {
Schema { defs, form, extra }
}
pub fn from_serde(mut serde_schema: Serde) -> Result<Self, Error> {
let mut defs = HashMap::new();
let serde_defs = serde_schema.defs;
serde_schema.defs = None;
for (name, sub_schema) in serde_defs.unwrap_or_default() {
defs.insert(name, Self::_from_serde(sub_schema)?);
}
let mut schema = Self::_from_serde(serde_schema)?;
schema.defs = Some(defs);
Self::check_refs(&schema.defs.as_ref().unwrap(), &schema)?;
for sub_schema in schema.defs.as_ref().unwrap().values() {
Self::check_refs(&schema.defs.as_ref().unwrap(), &sub_schema)?;
}
Ok(schema)
}
fn _from_serde(serde_schema: Serde) -> Result<Self, Error> {
let mut form = Form::Empty;
if let Some(rxf) = serde_schema.rxf {
form = Form::Ref(rxf);
}
if let Some(typ) = serde_schema.typ {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
form = Form::Type(match typ.as_ref() {
"boolean" => Type::Boolean,
"number" => Type::Number,
"string" => Type::String,
"timestamp" => Type::Timestamp,
_ => bail!(JslError::InvalidForm),
});
}
if let Some(enm) = serde_schema.enm {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
let mut values = HashSet::new();
for val in enm {
if values.contains(&val) {
bail!(JslError::InvalidForm);
} else {
values.insert(val);
}
}
if values.is_empty() {
bail!(JslError::InvalidForm);
}
form = Form::Enum(values);
}
if let Some(elements) = serde_schema.elems {
if form != Form::Empty {
bail!(JslError::InvalidForm);
}
form = Form::Elements(Self::_from_serde(*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(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(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(*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(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 {
defs: None,
form: Box::new(form),
extra: serde_schema.extra,
})
}
fn check_refs(defs: &HashMap<String, Schema>, schema: &Schema) -> Result<(), Error> {
match schema.form() {
Form::Ref(ref def) => {
if !defs.contains_key(def) {
bail!(JslError::NoSuchDefinition {
definition: def.clone()
})
}
}
Form::Elements(ref schema) => {
Self::check_refs(defs, schema)?;
}
Form::Properties(ref required, ref optional, _) => {
for schema in required.values() {
Self::check_refs(defs, schema)?;
}
for schema in optional.values() {
Self::check_refs(defs, schema)?;
}
}
Form::Values(ref schema) => {
Self::check_refs(defs, schema)?;
}
Form::Discriminator(_, ref mapping) => {
for schema in mapping.values() {
Self::check_refs(defs, schema)?;
}
}
_ => {}
};
Ok(())
}
pub fn into_serde(self) -> Serde {
let mut out = Serde::default();
match *self.form {
Form::Empty => {}
Form::Ref(def) => {
out.rxf = Some(def);
}
Form::Type(Type::Boolean) => {
out.typ = Some("boolean".to_owned());
}
Form::Type(Type::Number) => {
out.typ = Some("number".to_owned());
}
Form::Type(Type::String) => {
out.typ = Some("string".to_owned());
}
Form::Type(Type::Timestamp) => {
out.typ = Some("timestamp".to_owned());
}
Form::Enum(vals) => {
out.enm = Some(vals.into_iter().collect());
}
Form::Elements(sub_schema) => out.elems = Some(Box::new(sub_schema.into_serde())),
Form::Properties(required, optional, has_required) => {
if has_required || !required.is_empty() {
out.props = Some(
required
.into_iter()
.map(|(k, v)| (k, v.into_serde()))
.collect(),
);
}
if !has_required || !optional.is_empty() {
out.opt_props = Some(
optional
.into_iter()
.map(|(k, v)| (k, v.into_serde()))
.collect(),
);
}
}
Form::Values(sub_schema) => out.values = Some(Box::new(sub_schema.into_serde())),
Form::Discriminator(tag, mapping) => {
out.discriminator = Some(SerdeDiscriminator {
tag,
mapping: mapping
.into_iter()
.map(|(k, v)| (k, v.into_serde()))
.collect(),
});
}
}
out.extra = self.extra;
out
}
pub fn is_root(&self) -> bool {
self.defs.is_some()
}
pub fn definitions(&self) -> &Option<HashMap<String, Schema>> {
&self.defs
}
pub fn form(&self) -> &Form {
&self.form
}
pub fn extra(&self) -> &HashMap<String, Value> {
&self.extra
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Form {
Empty,
Ref(String),
Type(Type),
Enum(HashSet<String>),
Elements(Schema),
Properties(HashMap<String, Schema>, HashMap<String, Schema>, bool),
Values(Schema),
Discriminator(String, HashMap<String, Schema>),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Type {
Boolean,
Number,
String,
Timestamp,
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Default, Clone)]
pub struct Serde {
#[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 = "enum")]
pub enm: Option<Vec<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#"{
"definitions": {
"a": {}
},
"ref": "http://example.com/bar",
"type": "foo",
"enum": [
"FOO",
"BAR"
],
"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 {
rxf: Some("http://example.com/bar".to_owned()),
defs: Some(
[("a".to_owned(), Serde::default())]
.iter()
.cloned()
.collect()
),
typ: Some("foo".to_owned()),
enm: Some(vec!["FOO".to_owned(), "BAR".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!({
"definitions": {
"a": { "type": "boolean" }
}
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(
[(
"a".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
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 {
defs: Some(HashMap::new()),
form: Box::new(Form::Empty),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_extra() {
assert_eq!(
Schema::from_serde(serde_json::from_value(json!({ "foo": "bar" })).unwrap()).unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Empty),
extra: serde_json::from_value(json!({ "foo": "bar" })).unwrap(),
}
);
}
#[test]
fn from_serde_ref() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"definitions": {
"a": { "type": "boolean" }
},
"ref": "a",
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(
[(
"a".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
},
)]
.iter()
.cloned()
.collect()
),
form: Box::new(Form::Ref("a".to_owned())),
extra: HashMap::new(),
}
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"definitions": {
"a": { "type": "boolean" }
},
"ref": "",
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_type() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "boolean",
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(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 {
defs: Some(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 {
defs: Some(HashMap::new()),
form: Box::new(Form::Type(Type::String)),
extra: HashMap::new(),
},
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"type": "timestamp",
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Type(Type::Timestamp)),
extra: HashMap::new(),
},
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"type": "nonsense",
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_enum() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"enum": ["FOO", "BAR"],
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Enum(
vec!["FOO".to_owned(), "BAR".to_owned()]
.iter()
.cloned()
.collect()
)),
extra: HashMap::new(),
},
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"enum": [],
}))
.unwrap()
)
.is_err());
assert!(Schema::from_serde(
serde_json::from_value(json!({
"enum": ["FOO", "FOO"],
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_elements() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"elements": {
"type": "boolean",
},
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Elements(Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
})),
extra: HashMap::new(),
}
);
}
#[test]
fn from_serde_properties() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"properties": {
"a": { "type": "boolean" },
},
"optionalProperties": {
"b": { "type": "boolean" },
},
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Properties(
[(
"a".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
[(
"b".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
true,
)),
extra: HashMap::new(),
}
);
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"optionalProperties": {
"b": { "type": "boolean" },
},
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Properties(
HashMap::new(),
[(
"b".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
extra: HashMap::new(),
}
)]
.iter()
.cloned()
.collect(),
false,
)),
extra: HashMap::new(),
}
);
assert!(Schema::from_serde(
serde_json::from_value(json!({
"properties": {
"a": { "type": "boolean" },
},
"optionalProperties": {
"a": { "type": "boolean" },
},
}))
.unwrap()
)
.is_err());
}
#[test]
fn from_serde_values() {
assert_eq!(
Schema::from_serde(
serde_json::from_value(json!({
"values": {
"type": "boolean",
},
}))
.unwrap()
)
.unwrap(),
Schema {
defs: Some(HashMap::new()),
form: Box::new(Form::Values(Schema {
defs: None,
form: Box::new(Form::Type(Type::Boolean)),
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 {
defs: Some(HashMap::new()),
form: Box::new(Form::Discriminator(
"foo".to_owned(),
[
(
"a".to_owned(),
Schema {
defs: None,
form: Box::new(Form::Properties(
HashMap::new(),
HashMap::new(),
true
)),
extra: HashMap::new(),
}
),
(
"b".to_owned(),
Schema {
defs: 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": "boolean" },
}
},
}))
.unwrap()
)
.is_err());
assert!(Schema::from_serde(
serde_json::from_value(json!({
"discriminator": {
"tag": "foo",
"mapping": {
"a": {
"properties": {
"foo": { "type": "boolean" },
},
},
},
},
}))
.unwrap()
)
.is_err());
}
}