use crate::common::formats::{CollectionFormat, IntegerFormat, NumberFormat, StringFormat};
use crate::common::helpers::{
Context, ValidateWithContext, validate_pattern, validate_required_string,
};
use crate::v2::items::Items;
use crate::v2::reference::RefOr;
use crate::v2::schema::Schema;
use crate::v2::spec::Spec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "in")]
pub enum Parameter {
#[serde(rename = "body")]
Body(Box<InBody>),
#[serde(rename = "header")]
Header(Box<InHeader>),
#[serde(rename = "query")]
Query(Box<InQuery>),
#[serde(rename = "path")]
Path(Box<InPath>),
#[serde(rename = "formData")]
FormData(Box<InFormData>),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct InBody {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
pub schema: RefOr<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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)]
#[serde(tag = "type")]
pub enum InHeader {
#[serde(rename = "string")]
String(StringParameter),
#[serde(rename = "integer")]
Integer(IntegerParameter),
#[serde(rename = "number")]
Number(NumberParameter),
#[serde(rename = "boolean")]
Boolean(BooleanParameter),
#[serde(rename = "array")]
Array(ArrayParameter),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum InPath {
#[serde(rename = "string")]
String(StringParameter),
#[serde(rename = "integer")]
Integer(IntegerParameter),
#[serde(rename = "number")]
Number(NumberParameter),
#[serde(rename = "boolean")]
Boolean(BooleanParameter),
#[serde(rename = "array")]
Array(ArrayParameter),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum InQuery {
#[serde(rename = "string")]
String(StringParameter),
#[serde(rename = "integer")]
Integer(IntegerParameter),
#[serde(rename = "number")]
Number(NumberParameter),
#[serde(rename = "boolean")]
Boolean(BooleanParameter),
#[serde(rename = "array")]
Array(ArrayParameter),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum InFormData {
#[serde(rename = "string")]
String(StringParameter),
#[serde(rename = "integer")]
Integer(IntegerParameter),
#[serde(rename = "number")]
Number(NumberParameter),
#[serde(rename = "boolean")]
Boolean(BooleanParameter),
#[serde(rename = "array")]
Array(ArrayParameter),
#[serde(rename = "file")]
File(FileParameter),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct StringParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "allowEmptyValue")]
pub allow_empty_value: Option<bool>,
#[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 = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 IntegerParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "allowEmptyValue")]
pub allow_empty_value: Option<bool>,
#[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 = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 NumberParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "allowEmptyValue")]
pub allow_empty_value: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[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 = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 BooleanParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "allowEmptyValue")]
pub allow_empty_value: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 ArrayParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "allowEmptyValue")]
pub allow_empty_value: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
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(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 FileParameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x-examples")]
pub x_examples: Option<BTreeMap<String, 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 Parameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
Parameter::Body(p) => p.validate_with_context(ctx, path),
Parameter::Header(p) => p.validate_with_context(ctx, path),
Parameter::Query(p) => p.validate_with_context(ctx, path),
Parameter::Path(p) => p.validate_with_context(ctx, path),
Parameter::FormData(p) => p.validate_with_context(ctx, path),
}
}
}
impl ValidateWithContext<Spec> for InBody {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}name"));
self.schema
.validate_with_context(ctx, format!("{path}.schema"));
}
}
impl ValidateWithContext<Spec> for InHeader {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
InHeader::String(p) => {
p.validate_with_context(ctx, path.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
if let Some(pattern) = &p.pattern {
validate_pattern(pattern, ctx, format!("{path}.pattern"));
}
}
InHeader::Integer(p) => {
p.validate_with_context(ctx, path.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InHeader::Number(p) => {
p.validate_with_context(ctx, path.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InHeader::Boolean(p) => {
p.validate_with_context(ctx, path.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InHeader::Array(p) => {
p.validate_with_context(ctx, path.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
}
}
}
impl ValidateWithContext<Spec> for InQuery {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
InQuery::String(p) => {
p.validate_with_context(ctx, path);
}
InQuery::Integer(p) => {
p.validate_with_context(ctx, path);
}
InQuery::Number(p) => {
p.validate_with_context(ctx, path);
}
InQuery::Boolean(p) => {
p.validate_with_context(ctx, path);
}
InQuery::Array(p) => {
p.validate_with_context(ctx, path);
}
}
}
}
impl ValidateWithContext<Spec> for InPath {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
InPath::String(p) => {
must_be_required(&p.required, ctx, path.clone(), p.name.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InPath::Integer(p) => {
must_be_required(&p.required, ctx, path.clone(), p.name.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InPath::Number(p) => {
must_be_required(&p.required, ctx, path.clone(), p.name.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InPath::Boolean(p) => {
must_be_required(&p.required, ctx, path.clone(), p.name.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
InPath::Array(p) => {
must_be_required(&p.required, ctx, path.clone(), p.name.clone());
must_not_allow_empty_value(&p.allow_empty_value, ctx, path.clone(), p.name.clone());
}
}
}
}
impl ValidateWithContext<Spec> for InFormData {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
InFormData::String(p) => {
p.validate_with_context(ctx, path);
}
InFormData::Integer(p) => {
p.validate_with_context(ctx, path);
}
InFormData::Number(p) => {
p.validate_with_context(ctx, path);
}
InFormData::Boolean(p) => {
p.validate_with_context(ctx, path);
}
InFormData::Array(p) => {
p.validate_with_context(ctx, path);
}
InFormData::File(p) => {
p.validate_with_context(ctx, path);
}
}
}
}
impl ValidateWithContext<Spec> for StringParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
impl ValidateWithContext<Spec> for IntegerParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
impl ValidateWithContext<Spec> for NumberParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
impl ValidateWithContext<Spec> for BooleanParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
impl ValidateWithContext<Spec> for ArrayParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
impl ValidateWithContext<Spec> for FileParameter {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
}
}
fn must_be_required(p: &Option<bool>, ctx: &mut Context<Spec>, path: String, name: String) {
if !p.is_some_and(|x| x) {
ctx.errors.push(format!("{path}.{name}: must be required"));
}
}
fn must_not_allow_empty_value(
p: &Option<bool>,
ctx: &mut Context<Spec>,
path: String,
name: String,
) {
if p.is_some_and(|x| x) {
ctx.errors
.push(format!("{path}.{name}: must not allow empty value"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::helpers::Context;
use crate::v2::items::Items;
use crate::v2::schema::{Schema, StringSchema};
use crate::validation::Options;
use serde_json::json;
fn ctx() -> Context<'static, Spec> {
let spec: &'static Spec = Box::leak(Box::new(Spec::default()));
Context::new(spec, Options::new())
}
#[test]
fn body_parameter_roundtrip_and_validate() {
let raw = json!({
"in": "body",
"name": "user",
"description": "the user",
"required": true,
"schema": {"type": "string"},
"x-foo": "bar",
});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
match &p {
Parameter::Body(b) => {
assert_eq!(b.name, "user");
assert_eq!(b.required, Some(true));
}
_ => panic!("expected body"),
}
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v, raw);
let mut c = ctx();
p.validate_with_context(&mut c, "x".into());
assert!(c.errors.is_empty(), "errors: {:?}", c.errors);
let bad = Parameter::Body(Box::new(InBody {
name: String::new(),
description: None,
required: None,
schema: RefOr::new_item(Schema::from(StringSchema::default())),
x_examples: None,
extensions: None,
}));
let mut c = ctx();
bad.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("must not be empty")),
"errors: {:?}",
c.errors
);
}
#[test]
fn header_parameter_all_variants_roundtrip_and_validate() {
let raw = json!({"in": "header", "type": "string", "name": "h"});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert!(matches!(&p, Parameter::Header(h) if matches!(**h, InHeader::String(_))));
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty(), "errors: {:?}", c.errors);
let raw = json!({"in": "header", "type": "integer", "name": "h"});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty());
let raw = json!({"in": "header", "type": "number", "name": "h"});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty());
let raw = json!({"in": "header", "type": "boolean", "name": "h"});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty());
let raw =
json!({"in": "header", "type": "array", "name": "h", "items": {"type": "string"}});
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty());
let p = Parameter::Header(Box::new(InHeader::String(StringParameter {
name: "h".into(),
allow_empty_value: Some(true),
..Default::default()
})));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors
.iter()
.any(|e| e.contains("must not allow empty value")),
"errors: {:?}",
c.errors
);
let p = Parameter::Header(Box::new(InHeader::String(StringParameter {
name: "h".into(),
pattern: Some("[".into()),
..Default::default()
})));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("pattern")),
"errors: {:?}",
c.errors
);
for variant in [
Parameter::Header(Box::new(InHeader::Integer(IntegerParameter {
name: "h".into(),
allow_empty_value: Some(true),
..Default::default()
}))),
Parameter::Header(Box::new(InHeader::Number(NumberParameter {
name: "h".into(),
allow_empty_value: Some(true),
..Default::default()
}))),
Parameter::Header(Box::new(InHeader::Boolean(BooleanParameter {
name: "h".into(),
allow_empty_value: Some(true),
..Default::default()
}))),
Parameter::Header(Box::new(InHeader::Array(ArrayParameter {
name: "h".into(),
allow_empty_value: Some(true),
items: Items::String(Box::default()),
..Default::default()
}))),
] {
let mut c = ctx();
variant.validate_with_context(&mut c, "p".into());
assert!(
c.errors
.iter()
.any(|e| e.contains("must not allow empty value")),
"errors: {:?}",
c.errors
);
}
}
#[test]
fn query_parameter_all_variants_roundtrip_and_validate() {
for raw in [
json!({"in": "query", "type": "string", "name": "q"}),
json!({"in": "query", "type": "integer", "name": "q"}),
json!({"in": "query", "type": "number", "name": "q"}),
json!({"in": "query", "type": "boolean", "name": "q"}),
json!({"in": "query", "type": "array", "name": "q", "items": {"type": "string"}}),
] {
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty(), "errors: {:?}", c.errors);
}
for inner in [
InQuery::String(StringParameter::default()),
InQuery::Integer(IntegerParameter::default()),
InQuery::Number(NumberParameter::default()),
InQuery::Boolean(BooleanParameter::default()),
InQuery::Array(ArrayParameter {
items: Items::String(Box::default()),
..Default::default()
}),
] {
let p = Parameter::Query(Box::new(inner));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("must not be empty")),
"errors: {:?}",
c.errors
);
}
}
#[test]
fn path_parameter_all_variants_validate() {
let cases = [
Parameter::Path(Box::new(InPath::String(StringParameter {
name: "id".into(),
required: Some(true),
..Default::default()
}))),
Parameter::Path(Box::new(InPath::Integer(IntegerParameter {
name: "id".into(),
required: Some(true),
..Default::default()
}))),
Parameter::Path(Box::new(InPath::Number(NumberParameter {
name: "id".into(),
required: Some(true),
..Default::default()
}))),
Parameter::Path(Box::new(InPath::Boolean(BooleanParameter {
name: "id".into(),
required: Some(true),
..Default::default()
}))),
Parameter::Path(Box::new(InPath::Array(ArrayParameter {
name: "id".into(),
required: Some(true),
items: Items::String(Box::default()),
..Default::default()
}))),
];
for p in &cases {
let raw = serde_json::to_value(p).unwrap();
let p2: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(p, &p2);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty(), "errors: {:?}", c.errors);
}
let p = Parameter::Path(Box::new(InPath::String(StringParameter {
name: "id".into(),
required: None,
..Default::default()
})));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("must be required")),
"errors: {:?}",
c.errors
);
let p = Parameter::Path(Box::new(InPath::String(StringParameter {
name: "id".into(),
required: Some(true),
allow_empty_value: Some(true),
..Default::default()
})));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors
.iter()
.any(|e| e.contains("must not allow empty value")),
"errors: {:?}",
c.errors
);
for inner in [
InPath::Integer(IntegerParameter::default()),
InPath::Number(NumberParameter::default()),
InPath::Boolean(BooleanParameter::default()),
InPath::Array(ArrayParameter {
items: Items::String(Box::default()),
..Default::default()
}),
] {
let p = Parameter::Path(Box::new(inner));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("must be required")),
"errors: {:?}",
c.errors
);
}
}
#[test]
fn formdata_parameter_all_variants_roundtrip_and_validate() {
for raw in [
json!({"in": "formData", "type": "string", "name": "f"}),
json!({"in": "formData", "type": "integer", "name": "f"}),
json!({"in": "formData", "type": "number", "name": "f"}),
json!({"in": "formData", "type": "boolean", "name": "f"}),
json!({"in": "formData", "type": "array", "name": "f", "items": {"type": "string"}}),
json!({"in": "formData", "type": "file", "name": "f"}),
] {
let p: Parameter = serde_json::from_value(raw.clone()).unwrap();
assert_eq!(serde_json::to_value(&p).unwrap(), raw);
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(c.errors.is_empty(), "errors: {:?}", c.errors);
}
for inner in [
InFormData::String(StringParameter::default()),
InFormData::Integer(IntegerParameter::default()),
InFormData::Number(NumberParameter::default()),
InFormData::Boolean(BooleanParameter::default()),
InFormData::Array(ArrayParameter {
items: Items::String(Box::default()),
..Default::default()
}),
InFormData::File(FileParameter::default()),
] {
let p = Parameter::FormData(Box::new(inner));
let mut c = ctx();
p.validate_with_context(&mut c, "p".into());
assert!(
c.errors.iter().any(|e| e.contains("must not be empty")),
"errors: {:?}",
c.errors
);
}
}
#[test]
fn x_examples_round_trip() {
let body = json!({
"in": "body",
"name": "payload",
"schema": { "type": "string" },
"x-examples": {
"application/json": { "value": "demo" }
}
});
let p: Parameter = serde_json::from_value(body.clone()).unwrap();
match &p {
Parameter::Body(body) => assert_eq!(
body.x_examples,
Some(BTreeMap::from_iter([(
"application/json".to_owned(),
serde_json::json!({ "value": "demo" })
)]))
),
_ => panic!("expected body parameter"),
}
assert_eq!(serde_json::to_value(&p).unwrap(), body);
let query = json!({
"in": "query",
"type": "string",
"name": "q",
"x-examples": {
"default": "demo"
}
});
let p: Parameter = serde_json::from_value(query.clone()).unwrap();
match &p {
Parameter::Query(query) => match query.as_ref() {
InQuery::String(query) => assert_eq!(
query.x_examples,
Some(BTreeMap::from_iter([(
"default".to_owned(),
serde_json::json!("demo")
)]))
),
_ => panic!("expected string query parameter"),
},
_ => panic!("expected query parameter"),
}
assert_eq!(serde_json::to_value(&p).unwrap(), query);
}
}