use crate::common::helpers::{Context, PushError, ValidateWithContext, validate_not_visited};
use crate::common::reference::{ResolveReference, resolve_in_map};
use crate::v3_0::callback::Callback;
use crate::v3_0::components::Components;
use crate::v3_0::example::Example;
use crate::v3_0::external_documentation::ExternalDocumentation;
use crate::v3_0::header::Header;
use crate::v3_0::info::Info;
use crate::v3_0::link::Link;
use crate::v3_0::parameter::Parameter;
use crate::v3_0::path_item::PathItem;
use crate::v3_0::request_body::RequestBody;
use crate::v3_0::response::Response;
use crate::v3_0::schema::Schema;
use crate::v3_0::security_scheme::SecurityScheme;
use crate::v3_0::server::Server;
use crate::v3_0::tag::Tag;
use crate::validation::{Error, Options, Validate};
use enumset::EnumSet;
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 openapi: Version,
pub info: Info,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
pub paths: BTreeMap<String, PathItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[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 {
#[serde(rename = "3.0.0")]
V3_0_0,
#[serde(rename = "3.0.1")]
V3_0_1,
#[serde(rename = "3.0.2")]
V3_0_2,
#[serde(rename = "3.0.3")]
V3_0_3,
#[default]
#[serde(rename = "3.0.4", alias = "3.0")]
V3_0_4,
}
impl Display for Version {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::V3_0_0 => write!(f, "3.0.0"),
Self::V3_0_1 => write!(f, "3.0.1"),
Self::V3_0_2 => write!(f, "3.0.2"),
Self::V3_0_3 => write!(f, "3.0.3"),
Self::V3_0_4 => write!(f, "3.0.4"),
}
}
}
impl ResolveReference<Response> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Response> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/responses/", &x.responses))
}
}
impl ResolveReference<Parameter> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Parameter> {
self.components.as_ref().and_then(|x| {
resolve_in_map(self, reference, "#/components/parameters/", &x.parameters)
})
}
}
impl ResolveReference<RequestBody> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&RequestBody> {
self.components.as_ref().and_then(|x| {
resolve_in_map(
self,
reference,
"#/components/requestBodies/",
&x.request_bodies,
)
})
}
}
impl ResolveReference<Header> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Header> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/headers/", &x.headers))
}
}
impl ResolveReference<Schema> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Schema> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/schemas/", &x.schemas))
}
}
impl ResolveReference<Example> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Example> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/examples/", &x.examples))
}
}
impl ResolveReference<Callback> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Callback> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/callbacks/", &x.callbacks))
}
}
impl ResolveReference<Link> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Link> {
self.components
.as_ref()
.and_then(|x| resolve_in_map(self, reference, "#/components/links/", &x.links))
}
}
impl ResolveReference<SecurityScheme> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&SecurityScheme> {
self.components.as_ref().and_then(|x| {
resolve_in_map(
self,
reference,
"#/components/securitySchemes/",
&x.security_schemes,
)
})
}
}
impl ResolveReference<Tag> for Spec {
fn resolve_reference(&self, reference: &str) -> Option<&Tag> {
self.tags.as_ref().and_then(|x| {
x.iter()
.find(|x| x.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());
if let Some(servers) = &self.servers {
for (i, server) in servers.iter().enumerate() {
server.validate_with_context(&mut ctx, format!("#.servers[{i}]"))
}
}
for (name, item) in self.paths.iter() {
if let Some(operations) = &item.operations {
for (method, operation) in operations.iter() {
if let Some(operation_id) = &operation.operation_id
&& !ctx
.visited
.insert(format!("#/paths/operations/{operation_id}"))
{
ctx.error(
"#".to_owned(),
format!(
".paths[{name}].{method}.operationId: `{operation_id}` already in use"
),
);
}
}
}
}
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(components) = &self.components {
components.validate_with_context(&mut ctx, "{}.components".to_owned());
}
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);
}
}
ctx.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_deserialize() {
assert_eq!(
serde_json::from_value::<Version>(serde_json::json!("3.0.0")).unwrap(),
Version::V3_0_0,
"correct openapi version",
);
assert_eq!(
serde_json::from_value::<Version>(serde_json::json!("3.0")).unwrap(),
Version::V3_0_4,
"3.0 openapi version",
);
assert_eq!(
serde_json::from_value::<Version>(serde_json::json!("foo"))
.unwrap_err()
.to_string(),
"unknown variant `foo`, expected one of `3.0.0`, `3.0.1`, `3.0.2`, `3.0.3`, `3.0`, `3.0.4`",
"foo as openapi version",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"openapi": "3.0.4",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap()
.openapi,
Version::V3_0_4,
"3.0.4 spec.openapi",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"openapi": "3.0",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap()
.openapi,
Version::V3_0_4,
"3.0 spec.openapi",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"openapi": "",
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap_err()
.to_string(),
"unknown variant ``, expected one of `3.0.0`, `3.0.1`, `3.0.2`, `3.0.3`, `3.0`, `3.0.4`",
"empty spec.openapi",
);
assert_eq!(
serde_json::from_value::<Spec>(serde_json::json!({
"info": {
"title": "foo",
"version": "1",
},
"paths": {},
}))
.unwrap_err()
.to_string(),
"missing field `openapi`",
"missing spec.openapi",
);
}
#[test]
fn test_version_serialize() {
assert_eq!(
serde_json::to_string(&Version::V3_0_0).unwrap(),
r#""3.0.0""#,
);
assert_eq!(
serde_json::to_string(&Version::default()).unwrap(),
r#""3.0.4""#,
);
}
}