use monostate::MustBe;
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use crate::common::bool_or::BoolOr;
use crate::common::formats::{IntegerFormat, NumberFormat, StringFormat};
use crate::common::helpers::{Context, ValidateWithContext, validate_pattern};
use crate::common::reference::RefOr;
use crate::v3_0::discriminator::Discriminator;
use crate::v3_0::external_documentation::ExternalDocumentation;
use crate::v3_0::spec::Spec;
use crate::v3_0::xml::XML;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Schema {
AllOf(Box<AllOfSchema>),
AnyOf(Box<AnyOfSchema>),
OneOf(Box<OneOfSchema>),
Not(Box<NotSchema>),
Single(Box<SingleSchema>), }
impl Default for Schema {
fn default() -> Self {
Schema::Single(Box::default())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct AllOfSchema {
#[serde(rename = "allOf")]
pub all_of: Vec<RefOr<Box<Schema>>>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct AnyOfSchema {
#[serde(rename = "anyOf")]
pub any_of: Vec<RefOr<Box<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct OneOfSchema {
#[serde(rename = "oneOf")]
pub one_of: Vec<RefOr<Box<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct NotSchema {
#[serde(rename = "allOf")]
pub not: RefOr<Box<Schema>>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum SingleSchema {
#[serde(rename = "string")]
String(StringSchema),
#[serde(rename = "integer")]
Integer(IntegerSchema),
#[serde(rename = "number")]
Number(NumberSchema),
#[serde(rename = "boolean")]
Boolean(BooleanSchema),
#[serde(rename = "array")]
Array(ArraySchema),
#[serde(rename = "null")]
Null(NullSchema),
#[serde(rename = "object")]
Object(ObjectSchema),
}
impl Default for SingleSchema {
fn default() -> Self {
SingleSchema::Object(ObjectSchema::default())
}
}
impl Display for SingleSchema {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SingleSchema::String(_) => write!(f, "string"),
SingleSchema::Integer(_) => write!(f, "integer"),
SingleSchema::Number(_) => write!(f, "number"),
SingleSchema::Boolean(_) => write!(f, "boolean"),
SingleSchema::Array(_) => write!(f, "array"),
SingleSchema::Object(_) => write!(f, "object"),
SingleSchema::Null(_) => write!(f, "null"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct StringSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("string"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<StringFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<String>>,
#[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u64>,
#[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct IntegerSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("integer"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<IntegerFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<i64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<i64>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<i64>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<bool>,
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct NumberSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("number"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<NumberFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<bool>,
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct BooleanSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("boolean"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ArraySchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("array"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<RefOr<Box<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<serde_json::Value>>,
#[serde(rename = "maxItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
#[serde(rename = "minItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(rename = "uniqueItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ObjectSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("object"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, RefOr<Box<Schema>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<BTreeMap<String, serde_json::Value>>,
#[serde(rename = "maxProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u64>,
#[serde(rename = "minProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u64>,
#[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<BoolOr<RefOr<Box<Schema>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct NullSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("null"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Schema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
Schema::Single(s) => s.validate_with_context(ctx, path),
Schema::AllOf(s) => {
for (i, schema) in s.all_of.iter().enumerate() {
schema.validate_with_context_boxed(ctx, format!("{path}.allOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::AnyOf(s) => {
for (i, schema) in s.any_of.iter().enumerate() {
schema.validate_with_context_boxed(ctx, format!("{path}.anyOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::OneOf(s) => {
for (i, schema) in s.one_of.iter().enumerate() {
schema.validate_with_context_boxed(ctx, format!("{path}.oneOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::Not(s) => {
s.not
.validate_with_context_boxed(ctx, format!("{path}.not"));
}
}
}
}
impl ValidateWithContext<Spec> for SingleSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
SingleSchema::String(s) => s.validate_with_context(ctx, path),
SingleSchema::Integer(s) => s.validate_with_context(ctx, path),
SingleSchema::Number(s) => s.validate_with_context(ctx, path),
SingleSchema::Boolean(s) => s.validate_with_context(ctx, path),
SingleSchema::Array(s) => s.validate_with_context(ctx, path),
SingleSchema::Object(s) => s.validate_with_context(ctx, path),
SingleSchema::Null(s) => s.validate_with_context(ctx, path),
}
}
}
impl ValidateWithContext<Spec> for StringSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(pattern) = &self.pattern {
validate_pattern(pattern, ctx, format!("{path}.pattern"));
}
}
}
impl ValidateWithContext<Spec> for IntegerSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
impl ValidateWithContext<Spec> for NumberSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
impl ValidateWithContext<Spec> for BooleanSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
impl ValidateWithContext<Spec> for ArraySchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(items) = &self.items {
items.validate_with_context_boxed(ctx, format!("{path}.items"));
}
}
}
impl ValidateWithContext<Spec> for ObjectSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(properties) = &self.properties {
for (name, schema) in properties {
schema.validate_with_context_boxed(ctx, format!("{path}.properties.{name}"));
}
}
if let Some(additional_properties) = &self.additional_properties {
match additional_properties {
BoolOr::Bool(_) => {}
BoolOr::Item(schema) => {
schema.validate_with_context_boxed(ctx, format!("{path}.additionalProperties"));
}
}
}
}
}
impl ValidateWithContext<Spec> for NullSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_deserialize() {
let spec = serde_json::from_value::<Schema>(serde_json::json!({
"type": "string",
"title": "foo",
}))
.unwrap();
if let Schema::Single(val) = &spec {
if let SingleSchema::String(string) = &**val {
assert_eq!(string.title, Some("foo".to_owned()));
} else {
panic!("expected StringSchema");
}
} else {
panic!("expected Schema::Single");
}
assert_eq!(
spec,
Schema::Single(Box::new(SingleSchema::String(StringSchema {
title: Some("foo".to_owned()),
..Default::default()
}))),
);
}
#[test]
fn test_single_serialize() {
assert_eq!(
serde_json::to_value(Schema::Single(Box::new(SingleSchema::String(
StringSchema {
title: Some("foo".to_owned()),
..Default::default()
}
))))
.unwrap(),
serde_json::json!({
"type": "string",
"title": "foo",
}),
);
assert_eq!(
serde_json::to_value(Schema::Single(Box::new(SingleSchema::Object(
ObjectSchema {
title: Some("foo".to_owned()),
required: Some(vec!["bar".to_owned()]),
properties: Some({
let mut map = BTreeMap::new();
map.insert(
"bar".to_owned(),
RefOr::new_item(Box::new(Schema::Single(Box::new(
SingleSchema::String(StringSchema {
title: Some("foo bar".to_owned()),
..Default::default()
}),
)))),
);
map
}),
..Default::default()
}
))))
.unwrap(),
serde_json::json!({
"type": "object",
"title": "foo",
"required": ["bar"],
"properties": {
"bar": {
"type": "string",
"title": "foo bar",
},
},
}),
);
}
#[test]
fn test_all_of_deserialize() {
let spec = serde_json::from_value::<Schema>(serde_json::json!({
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"type": "object",
"title": "foo",
},
],
}))
.unwrap();
if let Schema::AllOf(schema) = &spec {
assert_eq!(schema.all_of.len(), 2);
match schema.all_of[0].clone() {
RefOr::Ref(r) => {
assert_eq!(r.reference, "#/definitions/bar".to_owned());
}
_ => panic!("expected Ref"),
}
match schema.all_of[1].clone() {
RefOr::Item(o) => {
if let Schema::Single(o) = *o {
if let SingleSchema::Object(o) = *o {
assert_eq!(o.title, Some("foo".to_owned()));
} else {
panic!("expected SingleSchema::Object");
}
} else {
panic!("expected Schema::Single");
}
}
_ => panic!("expected Schema"),
}
} else {
panic!("expected AllOf schema");
}
}
#[test]
fn test_all_of_serialize() {
assert_eq!(
serde_json::to_value(Schema::AllOf(Box::new(AllOfSchema {
all_of: vec![
RefOr::new_ref("#/definitions/bar".to_owned()),
RefOr::new_item(Box::new(Schema::Single(Box::new(SingleSchema::Object(
ObjectSchema {
title: Some("foo".to_owned()),
..Default::default()
}
))))),
],
..Default::default()
})))
.unwrap(),
serde_json::json!({
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"type": "object",
"title": "foo",
},
],
}),
);
}
}