use crate::common::reference::RefOr;
use crate::v3_1::example::Example;
use crate::v3_1::media_type::MediaType;
use crate::v3_1::parameter::InHeaderStyle;
use crate::v3_1::schema::Schema;
use crate::v3_1::spec::Spec;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Header {
#[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 deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<InHeaderStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub explode: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<RefOr<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<BTreeMap<String, RefOr<Example>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<BTreeMap<String, MediaType>>,
#[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 Header {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if self.example.is_some() && self.examples.is_some() {
ctx.error(path.clone(), "example and examples are mutually exclusive");
}
match (self.schema.is_some(), self.content.is_some()) {
(true, true) => ctx.error(path.clone(), "schema and content are mutually exclusive"),
(false, false) => ctx.error(path.clone(), "must define either `schema` or `content`"),
_ => {}
}
if let Some(schema) = &self.schema {
schema.validate_with_context(ctx, format!("{path}.schema"));
}
if let Some(examples) = &self.examples {
for (k, v) in examples {
v.validate_with_context(ctx, format!("{path}.examples[{k}]"));
}
}
if let Some(content) = &self.content {
if content.len() != 1 {
ctx.error(
path.clone(),
format_args!(
".content: must contain exactly one media type entry, found {}",
content.len()
),
);
}
for (k, v) in content {
v.validate_with_context(ctx, format!("{path}.content[{k}]"));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::ValidationErrorsExt;
#[test]
fn test_header_deserialize() {
assert_eq!(
serde_json::from_value::<Header>(serde_json::json!({
"description": "A short description of the header.",
"required": true,
"deprecated": false,
"style": "simple",
"explode": false,
"x-extra": "extension",
}))
.unwrap(),
Header {
description: Some("A short description of the header.".to_owned()),
required: Some(true),
deprecated: Some(false),
style: Some(InHeaderStyle::Simple),
explode: Some(false),
extensions: Some({
let mut map = BTreeMap::new();
map.insert("x-extra".to_owned(), "extension".into());
map
}),
..Default::default()
},
"deserialize",
);
}
#[test]
fn test_header_serialize() {
assert_eq!(
serde_json::to_value(Header {
description: Some("A short description of the header.".to_owned()),
required: Some(true),
deprecated: Some(false),
style: Some(InHeaderStyle::Simple),
explode: Some(false),
extensions: Some({
let mut map = BTreeMap::new();
map.insert("x-extra".to_owned(), "extension".into());
map
}),
..Default::default()
})
.unwrap(),
serde_json::json!({
"description": "A short description of the header.",
"required": true,
"deprecated": false,
"style": "simple",
"explode": false,
"x-extra": "extension",
}),
"serialize string",
);
}
#[test]
fn header_must_define_schema_or_content() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Header::default().validate_with_context(&mut ctx, "h".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must define either `schema` or `content`")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn header_walks_schema() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
Header {
schema: Some(RefOr::new_ref("")),
..Default::default()
}
.validate_with_context(&mut ctx, "h".into());
assert!(
ctx.errors.mentions("h.schema"),
"expected h.schema error: {:?}",
ctx.errors
);
}
#[test]
fn header_content_size_must_be_one() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
let mut content = BTreeMap::new();
content.insert("application/json".to_owned(), MediaType::default());
content.insert("text/plain".to_owned(), MediaType::default());
Header {
content: Some(content),
..Default::default()
}
.validate_with_context(&mut ctx, "h".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must contain exactly one media type entry")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn header_schema_and_content_mutex() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, crate::validation::Options::new());
let mut content = BTreeMap::new();
content.insert("application/json".to_owned(), MediaType::default());
Header {
schema: Some(RefOr::new_item(crate::v3_1::schema::Schema::Single(
Box::new(crate::v3_1::schema::SingleSchema::String(
crate::v3_1::schema::StringSchema::default(),
)),
))),
content: Some(content),
..Default::default()
}
.validate_with_context(&mut ctx, "h".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("schema and content are mutually exclusive")),
"errors: {:?}",
ctx.errors
);
}
}