use std::{borrow::Cow, collections::BTreeMap};
use schemars::Schema;
use serde::Serialize;
use serde_json::Value;
use topcoat_core::{context::Cx, error::Result};
use topcoat_router::{
Method,
content::Json,
response::{IntoResponse, Response},
};
use crate::{SecurityScheme, parameter::ParameterLocation};
#[derive(Debug, Clone, Serialize)]
pub struct Document {
pub(crate) openapi: &'static str,
pub(crate) info: Info,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) servers: Vec<Server>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) tags: Vec<Tag>,
pub(crate) paths: BTreeMap<String, PathItem>,
#[serde(skip_serializing_if = "Components::is_empty")]
pub(crate) components: Components,
}
impl Document {
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("an OpenAPI document serializes to JSON")
}
#[must_use]
pub fn to_json_pretty(&self) -> String {
serde_json::to_string_pretty(self).expect("an OpenAPI document serializes to JSON")
}
}
impl IntoResponse for &Document {
fn into_response(self, cx: &Cx) -> Result<Response> {
Json(self).into_response(cx)
}
}
impl IntoResponse for Document {
fn into_response(self, cx: &Cx) -> Result<Response> {
(&self).into_response(cx)
}
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Info {
pub(crate) title: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) description: Option<Cow<'static, str>>,
pub(crate) version: Cow<'static, str>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Server {
pub(crate) url: Cow<'static, str>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Tag {
pub(crate) name: Cow<'static, str>,
pub(crate) description: Cow<'static, str>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub(crate) struct PathItem {
#[serde(skip_serializing_if = "Option::is_none")]
get: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
put: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
post: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
delete: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
head: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
patch: Option<OperationObject>,
#[serde(skip_serializing_if = "Option::is_none")]
trace: Option<OperationObject>,
}
impl PathItem {
pub(crate) const METHODS: [Method; 8] = [
Method::GET,
Method::PUT,
Method::POST,
Method::DELETE,
Method::OPTIONS,
Method::HEAD,
Method::PATCH,
Method::TRACE,
];
pub(crate) fn describes(method: &Method) -> bool {
Self::METHODS.contains(method)
}
pub(crate) fn operation_mut(
&mut self,
method: &Method,
) -> Option<&mut Option<OperationObject>> {
match method.as_str() {
"GET" => Some(&mut self.get),
"PUT" => Some(&mut self.put),
"POST" => Some(&mut self.post),
"DELETE" => Some(&mut self.delete),
"OPTIONS" => Some(&mut self.options),
"HEAD" => Some(&mut self.head),
"PATCH" => Some(&mut self.patch),
"TRACE" => Some(&mut self.trace),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct OperationObject {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) tags: Vec<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) summary: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) description: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) operation_id: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) parameters: Vec<ParameterObject>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) request_body: Option<RequestBodyObject>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) responses: BTreeMap<u16, ResponseObject>,
#[serde(skip_serializing_if = "is_false")]
pub(crate) deprecated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) security: Option<Vec<SecurityRequirement>>,
}
pub(crate) type SecurityRequirement = BTreeMap<Cow<'static, str>, Vec<Cow<'static, str>>>;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ParameterObject {
pub(crate) name: Cow<'static, str>,
#[serde(rename = "in")]
pub(crate) location: ParameterLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) description: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "is_false")]
pub(crate) required: bool,
pub(crate) schema: Schema,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RequestBodyObject {
#[serde(skip_serializing_if = "is_false")]
pub(crate) required: bool,
pub(crate) content: BTreeMap<&'static str, MediaType>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ResponseObject {
pub(crate) description: Cow<'static, str>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) content: BTreeMap<&'static str, MediaType>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct MediaType {
pub(crate) schema: Schema,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Components {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) schemas: BTreeMap<String, Value>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) security_schemes: BTreeMap<Cow<'static, str>, SecurityScheme>,
}
impl Components {
fn is_empty(&self) -> bool {
self.schemas.is_empty() && self.security_schemes.is_empty()
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(flag: &bool) -> bool {
!flag
}