use crate::common::helpers::{validate_email, validate_optional_uri, validate_required_string};
use crate::v3_2::spec::Spec;
use crate::validation::Options;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Info {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "termsOfService")]
pub terms_of_service: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<License>,
pub version: 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)]
#[serde(rename_all = "camelCase")]
pub struct Contact {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: 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)]
#[serde(rename_all = "camelCase")]
pub struct License {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[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 Info {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if !ctx.is_option(Options::IgnoreEmptyInfoTitle) {
validate_required_string(&self.title, ctx, format!("{path}.title"));
}
if !ctx.is_option(Options::IgnoreEmptyInfoVersion) {
validate_required_string(&self.version, ctx, format!("{path}.version"));
}
if let Some(contact) = &self.contact {
contact.validate_with_context(ctx, format!("{path}.contact"));
}
if let Some(license) = &self.license {
license.validate_with_context(ctx, format!("{path}.license"));
}
}
}
impl ValidateWithContext<Spec> for Contact {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_optional_uri(&self.url, ctx, format!("{path}.url"));
validate_email(&self.email, ctx, format!("{path}.email"));
}
}
impl ValidateWithContext<Spec> for License {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.name, ctx, format!("{path}.name"));
if self.identifier.is_some() && self.url.is_some() {
ctx.error(
path.clone(),
"`identifier` and `url` are mutually exclusive (OAS 3.1)",
);
}
if let Some(id) = &self.identifier {
validate_required_string(id, ctx, format!("{path}.identifier"));
}
validate_optional_uri(&self.url, ctx, format!("{path}.url"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_info_deserialize() {
assert_eq!(
serde_json::from_value::<Info>(json!({
"title": "Swagger Sample App",
"description": "This is a sample server Petstore server.",
"termsOfService": "https://example.com/terms/",
"contact": {
"name": "API Support",
"url": "https://www.example.com/support",
"email": "support@example.com"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.1"
}))
.unwrap(),
Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
contact: Some(Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
}),
license: Some(License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html"
)),
..Default::default()
}),
version: "1.0.1".to_owned(),
..Default::default()
},
"deserialize",
);
assert_eq!(
serde_json::from_value::<Info>(json!({
"title": "Swagger Sample App",
"description": "This is a sample server Petstore server.",
"termsOfService": "https://example.com/terms/",
"version": "1.0.1"
}))
.unwrap(),
Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
version: "1.0.1".to_owned(),
..Default::default()
},
"deserialize",
);
assert_eq!(
serde_json::from_value::<Info>(json!({
"title": "Swagger Sample App",
"version": "1.0.1"
}))
.unwrap(),
Info {
title: String::from("Swagger Sample App"),
version: "1.0.1".to_owned(),
..Default::default()
},
"deserialize",
);
assert_eq!(
serde_json::from_value::<Info>(json!({
"title": "",
"version": ""
}))
.unwrap(),
Info::default(),
"deserialize",
);
}
#[test]
fn test_info_serialize() {
assert_eq!(
serde_json::to_value(Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
contact: Some(Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
}),
license: Some(License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html"
)),
..Default::default()
}),
version: "1.0.1".to_owned(),
..Default::default()
})
.unwrap(),
json!({
"title": "Swagger Sample App",
"description": "This is a sample server Petstore server.",
"termsOfService": "https://example.com/terms/",
"contact": {
"name": "API Support",
"url": "https://www.example.com/support",
"email": "support@example.com"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.1"
}),
"serialize",
);
assert_eq!(
serde_json::to_value(Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
version: "1.0.1".to_owned(),
..Default::default()
})
.unwrap(),
json!({
"title": "Swagger Sample App",
"description": "This is a sample server Petstore server.",
"termsOfService": "https://example.com/terms/",
"version": "1.0.1"
}),
"serialize",
);
assert_eq!(
serde_json::to_value(Info {
title: String::from("Swagger Sample App"),
version: "1.0.1".to_owned(),
..Default::default()
})
.unwrap(),
json!({
"title": "Swagger Sample App",
"version": "1.0.1"
}),
"serialize",
);
assert_eq!(
serde_json::to_value(Info::default()).unwrap(),
json!({
"title": "",
"version": ""
}),
"serialize",
);
}
#[test]
fn test_contact_deserialize() {
assert_eq!(
serde_json::from_value::<Contact>(json!({
"name": "API Support",
"url": "https://www.example.com/support",
"email": "support@example.com"
}))
.unwrap(),
Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
},
"deserialize",
);
}
#[test]
fn test_contact_serialize() {
assert_eq!(
serde_json::to_value(Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
})
.unwrap(),
json!({
"name": "API Support",
"url": "https://www.example.com/support",
"email": "support@example.com"
}),
"serialize",
);
}
#[test]
fn test_license_deserialize() {
assert_eq!(
serde_json::from_value::<License>(json!({
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}))
.unwrap(),
License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html"
)),
..Default::default()
},
"deserialize",
);
}
#[test]
fn test_license_serialize() {
assert_eq!(
serde_json::to_value(License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html"
)),
..Default::default()
})
.unwrap(),
json!({
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}),
"serialize",
);
}
#[test]
fn test_contact_validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("contact"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Contact {
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("contact"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Contact {
url: Some(String::from("foo - bar")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("contact"));
assert_eq!(ctx.errors.len(), 1, "incorrect url: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
Contact {
email: Some(String::from("foo - bar")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("contact"));
assert_eq!(ctx.errors.len(), 1, "incorrect email: {:?}", ctx.errors);
}
#[test]
fn test_license_validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html",
)),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("license"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
License {
name: String::from("Apache 2.0"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("license"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
License {
name: String::from(""),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("license"));
assert_eq!(ctx.errors.len(), 1, "empty name: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
License {
name: String::from("Apache 2.0"),
url: Some(String::from("foo - bar")),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("license"));
assert_eq!(ctx.errors.len(), 1, "incorrect url: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
License {
name: String::from("Apache 2.0"),
identifier: Some(String::new()),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("license"));
assert!(
ctx.errors
.iter()
.any(|e| e.contains("license.identifier") && e.contains("must not be empty")),
"empty identifier: {:?}",
ctx.errors
);
}
#[test]
fn test_info_validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
contact: Some(Contact {
name: Some(String::from("API Support")),
url: Some(String::from("https://www.example.com/support")),
email: Some(String::from("support@example.com")),
..Default::default()
}),
license: Some(License {
name: String::from("Apache 2.0"),
url: Some(String::from(
"https://www.apache.org/licenses/LICENSE-2.0.html",
)),
..Default::default()
}),
version: "1.0.1".to_owned(),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Info {
title: String::from("Swagger Sample App"),
description: Some(String::from("This is a sample server Petstore server.")),
terms_of_service: Some(String::from("https://example.com/terms/")),
version: "1.0.1".to_owned(),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Info {
title: String::from("Swagger Sample App"),
version: "1.0.1".to_owned(),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
Info {
title: String::from("Swagger Sample App"),
version: String::from(""),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert_eq!(ctx.errors.len(), 1, "empty version: {:?}", ctx.errors);
ctx = Context::new(&spec, Default::default());
Info {
title: String::from(""),
version: String::from("1.0.1"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert_eq!(ctx.errors.len(), 1, "empty title: {:?}", ctx.errors);
ctx = Context::new(&spec, Options::only(&Options::IgnoreEmptyInfoTitle));
Info {
title: String::from(""),
version: String::from("1.0.1"),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
ctx = Context::new(&spec, Options::only(&Options::IgnoreEmptyInfoVersion));
Info {
title: String::from("Swagger Sample App"),
version: String::from(""),
..Default::default()
}
.validate_with_context(&mut ctx, String::from("info"));
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
#[test]
fn x_logo_round_trip_via_generic_extensions() {
let logo_value = json!({
"url": "https://example.com/logo.png",
"altText": "Example",
"href": "https://example.com"
});
let info: Info = serde_json::from_value(json!({
"title": "Swagger Sample App",
"version": "1.0.1",
"x-logo": logo_value.clone(),
}))
.unwrap();
assert_eq!(
info.extensions.as_ref().and_then(|m| m.get("x-logo")),
Some(&logo_value)
);
assert_eq!(
serde_json::to_value(&info).unwrap(),
json!({
"title": "Swagger Sample App",
"version": "1.0.1",
"x-logo": logo_value,
})
);
let spec = Spec::default();
let mut ctx = Context::new(&spec, Default::default());
info.validate_with_context(&mut ctx, "info".to_owned());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
}