use crate::common::helpers::{
Context, PushError, ValidateWithContext, validate_not_visited, validate_optional_string_matches,
};
use crate::common::reference::ResolveReference;
use crate::v2::external_documentation::ExternalDocumentation;
use crate::v2::info::Info;
use crate::v2::parameter::Parameter;
use crate::v2::path_item::PathItem;
use crate::v2::response::Response;
use crate::v2::schema::{ObjectSchema, Schema};
use crate::v2::security_scheme::SecurityScheme;
use crate::v2::tag::Tag;
use crate::validation::{Error, Options, Validate};
use enumset::EnumSet;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Spec {
pub swagger: Version,
pub info: Info,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schemes: Option<Vec<Scheme>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub produces: Option<Vec<String>>,
pub paths: BTreeMap<String, PathItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub definitions: Option<BTreeMap<String, Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<BTreeMap<String, Parameter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub responses: Option<BTreeMap<String, Response>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "securityDefinitions")]
pub security_definitions: Option<BTreeMap<String, SecurityScheme>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<Vec<BTreeMap<String, Vec<String>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Tag>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocumentation>,
#[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 enum Version {
#[default]
#[serde(rename = "2.0")]
V2_0,
}
impl Display for Version {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::V2_0 => write!(f, "2.0"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub enum Scheme {
#[serde(rename = "http")]
HTTP,
#[default]
#[serde(rename = "https")]
HTTPS,
#[serde(rename = "ws")]
WS,
#[serde(rename = "wss")]
WSS,
}
impl Display for Scheme {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::HTTP => write!(f, "http"),
Self::HTTPS => write!(f, "https"),
Self::WS => write!(f, "ws"),
Self::WSS => write!(f, "wss"),
}
}
}
impl ResolveReference<Schema> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Schema> {
self.definitions
.as_ref()
.and_then(|x| x.get(reference.trim_start_matches("#/definitions/")))
}
}
impl ResolveReference<ObjectSchema> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&ObjectSchema> {
if let Schema::Object(schema) = self
.definitions
.as_ref()
.and_then(|x| x.get(reference.trim_start_matches("#/definitions/")))?
{
Some(schema)
} else {
None
}
}
}
impl ResolveReference<Parameter> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Parameter> {
self.parameters
.as_ref()
.and_then(|x| x.get(reference.trim_start_matches("#/parameters/")))
}
}
impl ResolveReference<Response> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Response> {
self.responses
.as_ref()
.and_then(|x| x.get(reference.trim_start_matches("#/responses/")))
}
}
impl ResolveReference<SecurityScheme> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&SecurityScheme> {
self.security_definitions
.as_ref()
.and_then(|x| x.get(reference.trim_start_matches("#/securityDefinitions/")))
}
}
impl ResolveReference<Tag> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Tag> {
self.tags
.iter()
.flatten()
.find(|tag| tag.name == reference.trim_start_matches("#/tags/"))
}
}
impl Validate for Spec {
fn validate(&self, options: EnumSet<Options>) -> Result<(), Error> {
let mut ctx = Context::new(self, options);
self.info
.validate_with_context(&mut ctx, "#.info".to_owned());
let re = Regex::new(r"^[^{}/ :\\]+(?::\d+)?$").unwrap();
validate_optional_string_matches(&self.host, &re, &mut ctx, "#.host".to_owned());
if let Some(base_path) = &self.base_path
&& !base_path.starts_with('/')
{
ctx.error(
"#.basePath".to_owned(),
format_args!("must start with `/`, found `{base_path}`"),
);
}
for (name, item) in self.paths.iter() {
let path = format!("#.paths[{name}]");
if !name.starts_with('/') {
ctx.error(path.clone(), "must start with `/`");
}
item.validate_with_context(&mut ctx, path);
}
if let Some(docs) = &self.external_docs {
docs.validate_with_context(&mut ctx, "#.externalDocs".to_owned())
}
if let Some(tags) = &self.tags {
for tag in tags.iter() {
let path = format!("#/tags/{}", tag.name);
validate_not_visited(tag, &mut ctx, Options::IgnoreUnusedTags, path);
}
}
if let Some(definitions) = &self.definitions {
for (name, definition) in definitions.iter() {
let path = format!("#/definitions/{name}");
validate_not_visited(definition, &mut ctx, Options::IgnoreUnusedSchemas, path);
}
}
if let Some(parameters) = &self.parameters {
for (name, parameter) in parameters.iter() {
let path = format!("#/parameters/{name}");
validate_not_visited(parameter, &mut ctx, Options::IgnoreUnusedParameters, path);
}
}
if let Some(responses) = &self.responses {
for (name, response) in responses.iter() {
let path = format!("#/responses/{name}");
validate_not_visited(response, &mut ctx, Options::IgnoreUnusedResponses, path);
}
}
ctx.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swagger_version_deserialize() {
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap(),
Spec {
swagger: Version::V2_0,
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
},
"correct swagger version",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap_err()
.to_string(),
"unknown variant ``, expected `2.0`",
"empty swagger version",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "foo",
"info": {
"title": "foo",
"version":"1",
}
}))
.unwrap_err()
.to_string(),
"unknown variant `foo`, expected `2.0`",
"foo as swagger version",
);
}
#[test]
fn test_swagger_version_serialize() {
#[derive(Deserialize)]
struct TestVersion {
pub swagger: String,
}
assert_eq!(
serde_json::from_str::<TestVersion>(
serde_json::to_string(&Spec {
swagger: Version::V2_0,
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
})
.unwrap()
.as_str(),
)
.unwrap()
.swagger,
"2.0",
);
assert_eq!(
serde_json::from_str::<TestVersion>(
serde_json::to_string(&Spec {
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
})
.unwrap()
.as_str(),
)
.unwrap()
.swagger,
"2.0",
);
}
#[test]
fn test_scheme_deserialize() {
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap(),
Spec {
schemes: None,
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
},
"no scheme",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths":{},
"schemes":null,
}))
.unwrap(),
Spec {
schemes: None,
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
},
"null scheme",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
"schemes": [],
}))
.unwrap(),
Spec {
schemes: Some(vec![]),
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
},
"empty schemes array",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
"schemes": ["http", "wss", "https", "ws"],
}))
.unwrap(),
Spec {
schemes: Some(vec![Scheme::HTTP, Scheme::WSS, Scheme::HTTPS, Scheme::WS]),
info: Info {
title: String::from("foo"),
version: String::from("1"),
..Default::default()
},
..Default::default()
},
"correct schemes",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
"schemes": "foo",
}))
.unwrap_err()
.to_string(),
r#"invalid type: string "foo", expected a sequence"#,
"foo string as schemes"
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"swagger": "2.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
"schemes": ["foo"],
}))
.unwrap_err()
.to_string(),
r#"unknown variant `foo`, expected one of `http`, `https`, `ws`, `wss`"#,
"foo string as scheme",
);
}
}