use crate::common::formats::{CollectionFormat, IntegerFormat, NumberFormat, StringFormat};
use crate::common::helpers::validate_pattern;
use crate::v2::spec::Spec;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum Items {
#[serde(rename = "string")]
String(Box<StringItem>),
#[serde(rename = "integer")]
Integer(Box<IntegerItem>),
#[serde(rename = "number")]
Number(Box<NumberItem>),
#[serde(rename = "boolean")]
Boolean(Box<BooleanItem>),
#[serde(rename = "array")]
Array(Box<ArrayItem>),
}
impl Default for Items {
fn default() -> Self {
Items::String(Box::default())
}
}
impl From<StringItem> for Items {
fn from(i: StringItem) -> Self {
Items::String(Box::new(i))
}
}
impl From<IntegerItem> for Items {
fn from(i: IntegerItem) -> Self {
Items::Integer(Box::new(i))
}
}
impl From<NumberItem> for Items {
fn from(i: NumberItem) -> Self {
Items::Number(Box::new(i))
}
}
impl From<BooleanItem> for Items {
fn from(i: BooleanItem) -> Self {
Items::Boolean(Box::new(i))
}
}
impl From<ArrayItem> for Items {
fn from(i: ArrayItem) -> Self {
Items::Array(Box::new(i))
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct StringItem {
#[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(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 IntegerItem {
#[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(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 NumberItem {
#[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(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 BooleanItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub default: 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)]
pub struct ArrayItem {
pub items: Items,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<serde_json::Value>>,
#[serde(rename = "collectionFormat")]
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_format: Option<CollectionFormat>,
#[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(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 Items {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
Items::String(item) => item.validate_with_context(ctx, path),
Items::Integer(item) => item.validate_with_context(ctx, path),
Items::Number(item) => item.validate_with_context(ctx, path),
Items::Boolean(item) => item.validate_with_context(ctx, path),
Items::Array(item) => item.validate_with_context(ctx, path),
}
}
}
impl ValidateWithContext<Spec> for StringItem {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(pattern) = &self.pattern {
validate_pattern(pattern, ctx, format!("{path}.pattern"));
}
}
}
impl ValidateWithContext<Spec> for IntegerItem {
fn validate_with_context(&self, _ctx: &mut Context<Spec>, _path: String) {}
}
impl ValidateWithContext<Spec> for NumberItem {
fn validate_with_context(&self, _ctx: &mut Context<Spec>, _path: String) {}
}
impl ValidateWithContext<Spec> for BooleanItem {
fn validate_with_context(&self, _ctx: &mut Context<Spec>, _path: String) {}
}
impl ValidateWithContext<Spec> for ArrayItem {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(fmt) = &self.collection_format
&& fmt.is_multi()
{
ctx.error(
format!("{path}.collectionFormat"),
"`multi` is only allowed on `query` and `formData` parameters",
);
}
self.items
.validate_with_context(ctx, format!("{path}.items"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_items_deserialize() {
assert_eq!(
serde_json::from_value::<Items>(serde_json::json!({
"type": "string",
"format": "byte",
"default": "default",
"enum": ["enum1", "enum2"],
"maxLength": 10,
"minLength": 1,
"pattern": "pattern",
"x-internal-id": 123,
}))
.unwrap(),
Items::String(Box::new(StringItem {
format: Some(StringFormat::Byte),
default: Some(String::from("default")),
enum_values: Some(vec![String::from("enum1"), String::from("enum2")]),
max_length: Some(10),
min_length: Some(1),
pattern: Some(String::from("pattern")),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})),
"deserialize",
);
}
#[test]
fn test_string_items_serialize() {
assert_eq!(
serde_json::to_value(Items::String(Box::new(StringItem {
format: Some(StringFormat::Byte),
default: Some(String::from("default")),
enum_values: Some(vec![String::from("enum1"), String::from("enum2")]),
max_length: Some(10),
min_length: Some(1),
pattern: Some(String::from("pattern")),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})))
.unwrap(),
serde_json::json!({
"type": "string",
"format": "byte",
"default": "default",
"enum": ["enum1", "enum2"],
"maxLength": 10,
"minLength": 1,
"pattern": "pattern",
"x-internal-id": 123,
}),
"serialize",
);
}
#[test]
fn test_integer_items_deserialize() {
assert_eq!(
serde_json::from_value::<Items>(serde_json::json!({
"type": "integer",
"format": "int64",
"default": 42,
"enum": [42, 105],
"minimum": 1,
"exclusiveMinimum": true,
"maximum": 10,
"exclusiveMaximum": true,
"multipleOf": 2.0,
"x-internal-id": 123,
}))
.unwrap(),
Items::Integer(Box::new(IntegerItem {
format: Some(IntegerFormat::Int64),
default: Some(42),
enum_values: Some(vec![42, 105]),
minimum: Some(1.into()),
exclusive_minimum: Some(true),
maximum: Some(10.into()),
exclusive_maximum: Some(true),
multiple_of: Some(2.0),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})),
"deserialize",
);
}
#[test]
fn test_integer_items_serialize() {
assert_eq!(
serde_json::to_value(Items::Integer(Box::new(IntegerItem {
format: Some(IntegerFormat::Int64),
default: Some(42),
enum_values: Some(vec![42, 105]),
minimum: Some(1.into()),
exclusive_minimum: Some(true),
maximum: Some(10.into()),
exclusive_maximum: Some(true),
multiple_of: Some(2.0),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})))
.unwrap(),
serde_json::json!({
"type": "integer",
"format": "int64",
"default": 42,
"enum": [42, 105],
"minimum": 1,
"exclusiveMinimum": true,
"maximum": 10,
"exclusiveMaximum": true,
"multipleOf": 2.0,
"x-internal-id": 123,
}),
"serialize",
);
}
#[test]
fn test_number_items_deserialize() {
assert_eq!(
serde_json::from_value::<Items>(serde_json::json!({
"type": "number",
"format": "double",
"default": 42.0,
"enum": [42.0, 105.0],
"minimum": 1.0,
"exclusiveMinimum": true,
"maximum": 10.0,
"exclusiveMaximum": true,
"multipleOf": 2.0,
"x-internal-id": 123,
}))
.unwrap(),
Items::Number(Box::new(NumberItem {
format: Some(NumberFormat::Double),
default: Some(42.0),
enum_values: Some(vec![42.0, 105.0]),
minimum: Some(1.0),
exclusive_minimum: Some(true),
maximum: Some(10.0),
exclusive_maximum: Some(true),
multiple_of: Some(2.0),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})),
"deserialize",
);
}
#[test]
fn test_number_items_serialize() {
assert_eq!(
serde_json::to_value(Items::Number(Box::new(NumberItem {
format: Some(NumberFormat::Double),
default: Some(42.0),
enum_values: Some(vec![42.0, 105.0]),
minimum: Some(1.0),
exclusive_minimum: Some(true),
maximum: Some(10.0),
exclusive_maximum: Some(true),
multiple_of: Some(2.0),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})))
.unwrap(),
serde_json::json!({
"type": "number",
"format": "double",
"default": 42.0,
"enum": [42.0, 105.0],
"minimum": 1.0,
"exclusiveMinimum": true,
"maximum": 10.0,
"exclusiveMaximum": true,
"multipleOf": 2.0,
"x-internal-id": 123,
}),
"serialize",
);
}
#[test]
fn test_boolean_items_deserialize() {
assert_eq!(
serde_json::from_value::<Items>(serde_json::json!({
"type": "boolean",
"default": false,
"x-internal-id": 123,
}))
.unwrap(),
Items::Boolean(Box::new(BooleanItem {
default: Some(false),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})),
"deserialize",
);
}
#[test]
fn test_boolean_items_serialize() {
assert_eq!(
serde_json::to_value(Items::Boolean(Box::new(BooleanItem {
default: Some(true),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})))
.unwrap(),
serde_json::json!({
"type": "boolean",
"default": true,
"x-internal-id": 123,
}),
"serialize",
);
}
#[test]
fn test_array_items_deserialize() {
assert_eq!(
serde_json::from_value::<Items>(serde_json::json!({
"type": "array",
"items": {
"type": "number",
"format": "double",
},
"default": [42.0],
"collectionFormat": "csv",
"maxItems": 10,
"minItems": 1,
"uniqueItems": true,
"x-internal-id": 123,
}))
.unwrap(),
Items::Array(Box::new(ArrayItem {
items: Items::Number(Box::new(NumberItem {
format: Some(NumberFormat::Double),
..Default::default()
})),
default: Some(vec![serde_json::json!(42.0)]),
collection_format: Some(CollectionFormat::CSV),
max_items: Some(10),
min_items: Some(1),
unique_items: Some(true),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})),
"deserialize",
);
}
#[test]
fn test_array_items_serialize() {
assert_eq!(
serde_json::to_value(Items::Array(Box::new(ArrayItem {
items: Items::Number(Box::new(NumberItem {
format: Some(NumberFormat::Double),
..Default::default()
})),
default: Some(vec![serde_json::json!(42.0)]),
collection_format: Some(CollectionFormat::CSV),
max_items: Some(10),
min_items: Some(1),
unique_items: Some(true),
extensions: Some({
let mut map = BTreeMap::new();
map.insert(String::from("x-internal-id"), 123.into());
map
}),
})))
.unwrap(),
serde_json::json!({
"type": "array",
"items": {
"type": "number",
"format": "double",
},
"default": [42.0],
"collectionFormat": "csv",
"maxItems": 10,
"minItems": 1,
"uniqueItems": true,
"x-internal-id": 123,
}),
"serialize",
);
}
#[test]
fn array_item_rejects_multi_collection_format() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
let item = ArrayItem {
items: Items::String(Box::default()),
default: None,
collection_format: Some(CollectionFormat::Multi),
max_items: None,
min_items: None,
unique_items: None,
extensions: None,
};
item.validate_with_context(&mut ctx, "p".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("`multi` is only allowed")),
"errors: {:?}",
ctx.errors
);
let mut ctx = Context::new(&spec, crate::validation::Options::new());
let item = ArrayItem {
items: Items::String(Box::default()),
default: None,
collection_format: Some(CollectionFormat::CSV),
max_items: None,
min_items: None,
unique_items: None,
extensions: None,
};
item.validate_with_context(&mut ctx, "p".into());
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
}