//! JSL schema representations.
//!
//! This module provides both an abstract ([`Schema`](struct.Schema.html)) and a
//! serializable/deserializable ([`SerdeSchema`](struct.SerdeSchema.html))
//! representation of JSL schemas.
use crate::errors::JslError;
use failure::{bail, Error};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
/// An abstract representation of a JSL schema.
///
/// This struct is meant for use by validators, code generators, or other
/// high-level processors of schemas. For serialization and deserialization of
/// schemas, instead use [`Serde`](struct.Serde.html).
#[derive(Clone, PartialEq, Debug)]
pub struct Schema {
defs: Option<HashMap<String, Schema>>,
form: Box<Form>,
extra: HashMap<String, Value>,
}
impl Schema {
/// Construct a new schema from its constituent parts.
///
/// `defs` should be present (i.e. not `None`) if and only if the
/// constructed schema is a root one. This invariant is not enforced, but
/// many users of this crate will presume that root schemas have definitions
/// they can unwrap. Likewise, some tooling will assume that any schema
/// which has non-`None` definitions are root schemas.
pub fn from_parts(
defs: Option<HashMap<String, Schema>>,
form: Box<Form>,
extra: HashMap<String, Value>,
) -> Schema {
Schema { defs, form, extra }
}
/// Construct a new, root schema from a `Serde`.
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,
"float32" => Type::Float32,
"float64" => Type::Float64,
"int8" => Type::Int8,
"uint8" => Type::Uint8,
"int16" => Type::Int16,
"uint16" => Type::Uint16,
"int32" => Type::Int32,
"uint32" => Type::Uint32,
"int64" => Type::Int64,
"uint64" => Type::Uint64,
"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(())
}
/// Convert this schema into a `Serde`.
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::Float32) => {
out.typ = Some("float32".to_owned());
}
Form::Type(Type::Float64) => {
out.typ = Some("float64".to_owned());
}
Form::Type(Type::Int8) => {
out.typ = Some("int8".to_owned());
}
Form::Type(Type::Uint8) => {
out.typ = Some("uint8".to_owned());
}
Form::Type(Type::Int16) => {
out.typ = Some("int16".to_owned());
}
Form::Type(Type::Uint16) => {
out.typ = Some("uint16".to_owned());
}
Form::Type(Type::Int32) => {
out.typ = Some("int32".to_owned());
}
Form::Type(Type::Uint32) => {
out.typ = Some("uint32".to_owned());
}
Form::Type(Type::Int64) => {
out.typ = Some("int64".to_owned());
}
Form::Type(Type::Uint64) => {
out.typ = Some("uint64".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
}
/// Is this schema a root schema?
///
/// Under the hood, this is entirely equivalent to checking whether
/// `definitions().is_some()`.
pub fn is_root(&self) -> bool {
self.defs.is_some()
}
/// Get the definitions associated with this schema.
///
/// If this schema is non-root, this returns None.
pub fn definitions(&self) -> &Option<HashMap<String, Schema>> {
&self.defs
}
/// Get the form of the schema.
pub fn form(&self) -> &Form {
&self.form
}
/// Get extra data associated with this schema.
///
/// Essentially, this function returns a JSON object of properties that
/// aren't JSL keywords, but which were included in the schema's JSON. You
/// might use these nonstandard fields to implement custom behavior.
pub fn extra(&self) -> &HashMap<String, Value> {
&self.extra
}
}
/// The various forms which a schema may take on, and their respective data.
#[derive(Clone, Debug, PartialEq)]
pub enum Form {
/// The empty form.
///
/// This schema accepts all data.
Empty,
/// The ref form.
///
/// This schema refers to another schema, and does whatever that other
/// schema does. The contained string is the name of the definition of the
/// referred-to schema -- it is an index into the `defs` of the root schema.
Ref(String),
/// The type form.
///
/// This schema asserts that the data is one of the primitive types.
Type(Type),
/// The enum form.
///
/// This schema asserts that the data is a string, and that it is one of a
/// set of values.
Enum(HashSet<String>),
/// The elements form.
///
/// This schema asserts that the instance is an array, and that every
/// element of the array matches a given schema.
Elements(Schema),
/// The properties form.
///
/// This schema asserts that the instance is an object, and that the
/// properties all satisfy their respective schemas.
///
/// The first map is the set of required properties and their schemas. The
/// second map is the set of optional properties and their schemas.
///
/// The final property indicates whether `properties` exists on the schema.
/// This allows implementations to distinguish the case of an empty
/// `properties` field from an omitted one. This is necessary for tooling
/// which wants to link to a particular part of a schema in JSON form.
Properties(HashMap<String, Schema>, HashMap<String, Schema>, bool),
/// The values form.
///
/// This schema asserts that the instance is an object, and that all the
/// values in the object all satisfy the same schema.
Values(Schema),
/// The discriminator form.
///
/// This schema asserts that the instance is an object, and that it has a
/// "tag" property. The value of that tag must be one of the expected
/// "mapping" keys, and the corresponding mapping value is a schema that the
/// instance is expected to satisfy.
///
/// The first parameter is the name of the tag property. The second
/// parameter is the mapping from tag values to their corresponding schemas.
Discriminator(String, HashMap<String, Schema>),
}
/// The values that the "type" keyword may check for.
///
/// In a certain sense, you can consider these types to be JSON's "primitive"
/// types, with the remaining two types, arrays and objects, being the "complex"
/// types covered by other keywords.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Type {
/// The "true" or "false" JSON values.
Boolean,
/// Any JSON number.
///
/// Note that JSON only has one kind of number, and JSON numbers may have a
/// decimal part.
Number,
/// A floating-point number. This validates just like `Number`, but signals
/// the intention that the data is meant to be a single-precision float.
Float32,
/// A floating-point number. This validates just like `Number`, but signals
/// the intention that the data is meant to be a double-precision float.
Float64,
/// An integer in the range covered by `i8`.
Int8,
/// An integer in the range covered by `u8`.
Uint8,
/// An integer in the range covered by `i16`.
Int16,
/// An integer in the range covered by `u16`.
Uint16,
/// An integer in the range covered by `i32`.
Int32,
/// An integer in the range covered by `u32`.
Uint32,
/// An integer in the range covered by `i64`.
Int64,
/// An integer in the range covered by `u64`.
Uint64,
/// Any JSON string.
String,
/// A string encoding an RFC3339 timestamp.
Timestamp,
}
/// A serialization/deserialization-friendly representation of a JSL schema.
///
/// This struct is meant for use with the `serde` crate. It is excellent for
/// parsing from various data formats, but does not enforce all the semantic
/// rules about how schemas must be formed. For that, consider converting
/// instances of `Serde` into [`Schema`](struct.Schema.html) using
/// [`Schema::from_serde`](struct.Schema.html#method.from_serde).
#[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>,
}
/// A serialization/deserialization-friendly representation of a JSL
/// discriminator.
///
/// This struct is useful mostly in the context of
/// [`SerdeSchema`](struct.SerdeSchema.html).
#[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());
}
}