use crate::common::bool_or::BoolOr;
use crate::common::formats::{IntegerFormat, NumberFormat, StringFormat};
use crate::common::helpers::{Context, PushError, ValidateWithContext, validate_pattern};
use crate::v2::external_documentation::ExternalDocumentation;
use crate::v2::reference::RefOr;
use crate::v2::spec::Spec;
use crate::v2::xml::XML;
use monostate::MustBe;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Schema {
#[serde(rename = "string")]
String(Box<StringSchema>),
#[serde(rename = "integer")]
Integer(Box<IntegerSchema>),
#[serde(rename = "number")]
Number(Box<NumberSchema>),
#[serde(rename = "boolean")]
Boolean(Box<BooleanSchema>),
#[serde(rename = "array")]
Array(Box<ArraySchema>),
#[serde(rename = "null")]
Null(Box<NullSchema>),
#[serde(rename = "object")]
Object(Box<ObjectSchema>),
AllOf(Box<AllOfSchema>),
}
impl Default for Schema {
fn default() -> Self {
Schema::Object(Box::default())
}
}
impl From<StringSchema> for Schema {
fn from(s: StringSchema) -> Self {
Schema::String(Box::new(s))
}
}
impl From<IntegerSchema> for Schema {
fn from(s: IntegerSchema) -> Self {
Schema::Integer(Box::new(s))
}
}
impl From<NumberSchema> for Schema {
fn from(s: NumberSchema) -> Self {
Schema::Number(Box::new(s))
}
}
impl From<BooleanSchema> for Schema {
fn from(s: BooleanSchema) -> Self {
Schema::Boolean(Box::new(s))
}
}
impl From<ArraySchema> for Schema {
fn from(s: ArraySchema) -> Self {
Schema::Array(Box::new(s))
}
}
impl From<NullSchema> for Schema {
fn from(s: NullSchema) -> Self {
Schema::Null(Box::new(s))
}
}
impl From<ObjectSchema> for Schema {
fn from(s: ObjectSchema) -> Self {
Schema::Object(Box::new(s))
}
}
impl From<AllOfSchema> for Schema {
fn from(s: AllOfSchema) -> Self {
Schema::AllOf(Box::new(s))
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct AllOfSchema {
#[serde(rename = "allOf")]
pub all_of: Vec<RefOr<Schema>>,
#[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 = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl Display for Schema {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Schema::String(_) => write!(f, "string"),
Schema::Integer(_) => write!(f, "integer"),
Schema::Number(_) => write!(f, "number"),
Schema::Boolean(_) => write!(f, "boolean"),
Schema::Array(_) => write!(f, "array"),
Schema::Object(_) => write!(f, "object"),
Schema::Null(_) => write!(f, "null"),
Schema::AllOf(_) => write!(f, "allOf"),
}
}
}
#[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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[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<serde_json::Number>,
#[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<serde_json::Number>,
#[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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[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<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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[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", default)]
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<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<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<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: 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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-nullable")]
pub x_nullable: Option<bool>,
#[serde(rename = "allOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<RefOr<ObjectSchema>>>,
#[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::String(s) => s.validate_with_context(ctx, path),
Schema::Integer(s) => s.validate_with_context(ctx, path),
Schema::Number(s) => s.validate_with_context(ctx, path),
Schema::Boolean(s) => s.validate_with_context(ctx, path),
Schema::Array(s) => s.validate_with_context(ctx, path),
Schema::Object(s) => s.validate_with_context(ctx, path),
Schema::Null(s) => s.validate_with_context(ctx, path),
Schema::AllOf(s) => s.validate_with_context(ctx, path),
}
}
}
impl ValidateWithContext<Spec> for AllOfSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if self.all_of.is_empty() {
ctx.error(path.clone(), ".allOf: must not be empty");
}
for (i, schema) in self.all_of.iter().enumerate() {
schema.validate_with_context(ctx, format!("{path}.allOf[{i}]"));
}
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
}
}
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(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(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(ctx, format!("{path}.additionalProperties"));
}
}
}
if let Some(all_of) = &self.all_of {
for (i, schema) in all_of.iter().enumerate() {
schema.validate_with_context(ctx, format!("{path}.allOf[{i}]"));
}
}
}
}
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::String(ref string) = spec {
assert_eq!(string.title, Some("foo".to_owned()));
} else {
panic!("expected StringSchema");
}
assert_eq!(
spec,
Schema::String(Box::new(StringSchema {
title: Some("foo".to_owned()),
..Default::default()
})),
);
}
#[test]
fn test_all_of_deserialize() {
let spec = serde_json::from_value::<Schema>(serde_json::json!({
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"type": "object",
"title": "foo",
},
],
"type": "object",
}))
.unwrap();
if let Schema::Object(schema) = spec.clone() {
if let Some(all_of) = schema.all_of {
assert_eq!(all_of.len(), 2);
match all_of[0].clone() {
RefOr::Ref(r) => {
assert_eq!(r.reference, "#/definitions/bar".to_owned());
}
_ => panic!("expected Ref"),
}
match all_of[1].clone() {
RefOr::Item(o) => {
assert_eq!(o.title, Some("foo".to_owned()));
}
_ => panic!("expected Schema"),
}
} else {
panic!("expected all_of to be set");
}
} else {
panic!("expected ObjectSchema");
}
}
#[test]
fn schema_without_type_parses_as_object() {
let json = serde_json::json!({
"title": "Untyped",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
});
let parsed: Schema = serde_json::from_value(json).expect("must parse");
match &parsed {
Schema::Object(o) => {
assert_eq!(o.title.as_deref(), Some("Untyped"));
assert_eq!(o.required.as_deref(), Some(&["name".to_owned()][..]));
assert!(o.properties.is_some());
}
other => panic!("expected Object, got {other:?}"),
}
let parsed: Schema = serde_json::from_value(serde_json::json!({})).expect("must parse");
assert!(matches!(parsed, Schema::Object(_)));
}
#[test]
fn schema_typed_string_still_dispatches_correctly_v2() {
let parsed: Schema =
serde_json::from_value(serde_json::json!({"type": "string"})).expect("must parse");
assert!(matches!(parsed, Schema::String(_)));
}
#[test]
fn test_single_serialize() {
assert_eq!(
serde_json::to_value(Schema::String(Box::new(StringSchema {
title: Some("foo".to_owned()),
..Default::default()
})))
.unwrap(),
serde_json::json!({
"type": "string",
"title": "foo",
}),
);
assert_eq!(
serde_json::to_value(Schema::Object(Box::new(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(Schema::from(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 integer_number_boolean_array_null_serde_roundtrip() {
let raw = serde_json::json!({"type": "integer", "format": "int32"});
let s: Schema = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(s, Schema::Integer(_)));
assert_eq!(serde_json::to_value(&s).unwrap(), raw);
let raw = serde_json::json!({"type": "number", "format": "double"});
let s: Schema = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(s, Schema::Number(_)));
assert_eq!(serde_json::to_value(&s).unwrap(), raw);
let raw = serde_json::json!({"type": "boolean"});
let s: Schema = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(s, Schema::Boolean(_)));
assert_eq!(serde_json::to_value(&s).unwrap(), raw);
let raw = serde_json::json!({
"type": "array",
"items": {"type": "string"}
});
let s: Schema = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(s, Schema::Array(_)));
assert_eq!(serde_json::to_value(&s).unwrap(), raw);
let raw = serde_json::json!({"type": "null"});
let s: Schema = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(s, Schema::Null(_)));
assert_eq!(serde_json::to_value(&s).unwrap(), raw);
}
#[test]
fn schema_validate_each_variant() {
let spec = Spec::default();
let s = Schema::String(Box::new(StringSchema {
pattern: Some("[".into()),
..Default::default()
}));
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors.iter().any(|e| e.contains("pattern")),
"errors: {:?}",
ctx.errors
);
let ed = crate::v2::external_documentation::ExternalDocumentation {
url: "not-a-url".into(),
..Default::default()
};
let s = Schema::Integer(Box::new(IntegerSchema {
external_docs: Some(ed.clone()),
..Default::default()
}));
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors.iter().any(|e| e.contains("must be a valid URL")),
"errors: {:?}",
ctx.errors
);
for s in [
Schema::Number(Box::new(NumberSchema {
external_docs: Some(ed.clone()),
..Default::default()
})),
Schema::Boolean(Box::new(BooleanSchema {
external_docs: Some(ed.clone()),
..Default::default()
})),
Schema::Array(Box::new(ArraySchema {
external_docs: Some(ed.clone()),
items: Some(RefOr::new_item(Schema::from(StringSchema::default()))),
..Default::default()
})),
Schema::Null(Box::new(NullSchema {
external_docs: Some(ed.clone()),
..Default::default()
})),
] {
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors.iter().any(|e| e.contains("must be a valid URL")),
"errors: {:?}",
ctx.errors
);
}
let s = Schema::Object(Box::new(ObjectSchema {
properties: Some({
let mut m = BTreeMap::new();
m.insert(
"k".into(),
RefOr::new_item(Schema::from(StringSchema {
pattern: Some("[".into()),
..Default::default()
})),
);
m
}),
additional_properties: Some(crate::common::bool_or::BoolOr::Item(RefOr::new_item(
Schema::from(StringSchema {
pattern: Some("[".into()),
..Default::default()
}),
))),
all_of: Some(vec![RefOr::new_item(ObjectSchema {
external_docs: Some(ed.clone()),
..Default::default()
})]),
..Default::default()
}));
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(ctx.errors.len() >= 2, "errors: {:?}", ctx.errors);
let s = Schema::Object(Box::new(ObjectSchema {
additional_properties: Some(crate::common::bool_or::BoolOr::Bool(true)),
..Default::default()
}));
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
#[test]
fn allof_schema_from_and_validate() {
let s = Schema::from(AllOfSchema {
all_of: vec![RefOr::new_item(Schema::from(StringSchema::default()))],
title: Some("t".into()),
..Default::default()
});
assert!(matches!(s, Schema::AllOf(_)));
assert_eq!(format!("{s}"), "allOf");
let s = Schema::AllOf(Box::default());
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains(".allOf: must not be empty")),
"errors: {:?}",
ctx.errors
);
let ed = crate::v2::external_documentation::ExternalDocumentation {
url: "not-a-url".into(),
..Default::default()
};
let s = Schema::AllOf(Box::new(AllOfSchema {
all_of: vec![RefOr::new_item(Schema::from(StringSchema::default()))],
external_docs: Some(ed),
..Default::default()
}));
let mut ctx = Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors.iter().any(|e| e.contains("must be a valid URL")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn allof_schema_serde_roundtrip() {
let raw = serde_json::json!({
"allOf": [{"type": "string"}],
"title": "T",
});
let s: AllOfSchema = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(s.title, Some("T".into()));
assert_eq!(s.all_of.len(), 1);
let v = serde_json::to_value(&s).unwrap();
assert_eq!(v, raw);
}
#[test]
fn schema_display_formats() {
assert_eq!(format!("{}", Schema::String(Box::default())), "string");
assert_eq!(format!("{}", Schema::Integer(Box::default())), "integer");
assert_eq!(format!("{}", Schema::Number(Box::default())), "number");
assert_eq!(format!("{}", Schema::Boolean(Box::default())), "boolean");
assert_eq!(format!("{}", Schema::Array(Box::default())), "array");
assert_eq!(format!("{}", Schema::Object(Box::default())), "object");
assert_eq!(format!("{}", Schema::Null(Box::default())), "null");
}
#[test]
fn schema_from_helpers() {
let _: Schema = StringSchema::default().into();
let _: Schema = IntegerSchema::default().into();
let _: Schema = NumberSchema::default().into();
let _: Schema = BooleanSchema::default().into();
let _: Schema = ArraySchema::default().into();
let _: Schema = NullSchema::default().into();
let _: Schema = ObjectSchema::default().into();
let _: Schema = AllOfSchema::default().into();
}
#[test]
fn test_all_of_serialize() {
assert_eq!(
serde_json::to_value(Schema::Object(Box::new(ObjectSchema {
all_of: Some(vec![
RefOr::new_ref("#/definitions/bar".to_owned()),
RefOr::new_item(ObjectSchema {
title: Some("foo".to_owned()),
..Default::default()
}),
]),
..Default::default()
})))
.unwrap(),
serde_json::json!({
"type": "object",
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"title": "foo",
"type": "object",
},
],
}),
);
}
#[test]
fn x_nullable_round_trip() {
let value = serde_json::json!({
"type": "string",
"x-nullable": true
});
let schema = serde_json::from_value::<Schema>(value.clone()).unwrap();
match &schema {
Schema::String(schema) => assert_eq!(schema.x_nullable, Some(true)),
_ => panic!("expected string schema"),
}
assert_eq!(serde_json::to_value(schema).unwrap(), value);
let value = serde_json::json!({
"type": "object",
"x-nullable": true
});
let schema = serde_json::from_value::<Schema>(value.clone()).unwrap();
match &schema {
Schema::Object(schema) => assert_eq!(schema.x_nullable, Some(true)),
_ => panic!("expected object schema"),
}
assert_eq!(serde_json::to_value(schema).unwrap(), value);
}
}