1use std::{borrow::Cow, collections::BTreeMap};
2
3use schemars::Schema;
4use serde::Serialize;
5use serde_json::Value;
6use topcoat_core::{context::Cx, error::Result};
7use topcoat_router::{
8 Method,
9 content::Json,
10 response::{IntoResponse, Response},
11};
12
13use crate::{SecurityScheme, parameter::ParameterLocation};
14
15#[derive(Debug, Clone, Serialize)]
22pub struct Document {
23 pub(crate) openapi: &'static str,
24 pub(crate) info: Info,
25 #[serde(skip_serializing_if = "Vec::is_empty")]
26 pub(crate) servers: Vec<Server>,
27 #[serde(skip_serializing_if = "Vec::is_empty")]
28 pub(crate) tags: Vec<Tag>,
29 pub(crate) paths: BTreeMap<String, PathItem>,
30 #[serde(skip_serializing_if = "Components::is_empty")]
31 pub(crate) components: Components,
32}
33
34impl Document {
35 #[must_use]
41 pub fn to_json(&self) -> String {
42 serde_json::to_string(self).expect("an OpenAPI document serializes to JSON")
43 }
44
45 #[must_use]
52 pub fn to_json_pretty(&self) -> String {
53 serde_json::to_string_pretty(self).expect("an OpenAPI document serializes to JSON")
54 }
55}
56
57impl IntoResponse for &Document {
59 fn into_response(self, cx: &Cx) -> Result<Response> {
60 Json(self).into_response(cx)
61 }
62}
63
64impl IntoResponse for Document {
66 fn into_response(self, cx: &Cx) -> Result<Response> {
67 (&self).into_response(cx)
68 }
69}
70
71#[derive(Debug, Clone, Serialize)]
73pub(crate) struct Info {
74 pub(crate) title: Cow<'static, str>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub(crate) description: Option<Cow<'static, str>>,
77 pub(crate) version: Cow<'static, str>,
78}
79
80#[derive(Debug, Clone, Serialize)]
82pub(crate) struct Server {
83 pub(crate) url: Cow<'static, str>,
84}
85
86#[derive(Debug, Clone, Serialize)]
88pub(crate) struct Tag {
89 pub(crate) name: Cow<'static, str>,
90 pub(crate) description: Cow<'static, str>,
91}
92
93#[derive(Debug, Clone, Default, Serialize)]
95pub(crate) struct PathItem {
96 #[serde(skip_serializing_if = "Option::is_none")]
97 get: Option<OperationObject>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 put: Option<OperationObject>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 post: Option<OperationObject>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 delete: Option<OperationObject>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 options: Option<OperationObject>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 head: Option<OperationObject>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 patch: Option<OperationObject>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 trace: Option<OperationObject>,
112}
113
114impl PathItem {
115 pub(crate) const METHODS: [Method; 8] = [
117 Method::GET,
118 Method::PUT,
119 Method::POST,
120 Method::DELETE,
121 Method::OPTIONS,
122 Method::HEAD,
123 Method::PATCH,
124 Method::TRACE,
125 ];
126
127 pub(crate) fn describes(method: &Method) -> bool {
129 Self::METHODS.contains(method)
130 }
131
132 pub(crate) fn operation_mut(
135 &mut self,
136 method: &Method,
137 ) -> Option<&mut Option<OperationObject>> {
138 match method.as_str() {
139 "GET" => Some(&mut self.get),
140 "PUT" => Some(&mut self.put),
141 "POST" => Some(&mut self.post),
142 "DELETE" => Some(&mut self.delete),
143 "OPTIONS" => Some(&mut self.options),
144 "HEAD" => Some(&mut self.head),
145 "PATCH" => Some(&mut self.patch),
146 "TRACE" => Some(&mut self.trace),
147 _ => None,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub(crate) struct OperationObject {
156 #[serde(skip_serializing_if = "Vec::is_empty")]
157 pub(crate) tags: Vec<Cow<'static, str>>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub(crate) summary: Option<Cow<'static, str>>,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub(crate) description: Option<Cow<'static, str>>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub(crate) operation_id: Option<Cow<'static, str>>,
164 #[serde(skip_serializing_if = "Vec::is_empty")]
165 pub(crate) parameters: Vec<ParameterObject>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub(crate) request_body: Option<RequestBodyObject>,
168 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
169 pub(crate) responses: BTreeMap<u16, ResponseObject>,
170 #[serde(skip_serializing_if = "is_false")]
171 pub(crate) deprecated: bool,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub(crate) security: Option<Vec<SecurityRequirement>>,
174}
175
176pub(crate) type SecurityRequirement = BTreeMap<Cow<'static, str>, Vec<Cow<'static, str>>>;
179
180#[derive(Debug, Clone, Serialize)]
182pub(crate) struct ParameterObject {
183 pub(crate) name: Cow<'static, str>,
184 #[serde(rename = "in")]
185 pub(crate) location: ParameterLocation,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub(crate) description: Option<Cow<'static, str>>,
188 #[serde(skip_serializing_if = "is_false")]
189 pub(crate) required: bool,
190 pub(crate) schema: Schema,
191}
192
193#[derive(Debug, Clone, Serialize)]
195pub(crate) struct RequestBodyObject {
196 #[serde(skip_serializing_if = "is_false")]
197 pub(crate) required: bool,
198 pub(crate) content: BTreeMap<&'static str, MediaType>,
199}
200
201#[derive(Debug, Clone, Serialize)]
203pub(crate) struct ResponseObject {
204 pub(crate) description: Cow<'static, str>,
205 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
206 pub(crate) content: BTreeMap<&'static str, MediaType>,
207}
208
209#[derive(Debug, Clone, Serialize)]
211pub(crate) struct MediaType {
212 pub(crate) schema: Schema,
213}
214
215#[derive(Debug, Clone, Default, Serialize)]
217#[serde(rename_all = "camelCase")]
218pub(crate) struct Components {
219 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
220 pub(crate) schemas: BTreeMap<String, Value>,
221 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
222 pub(crate) security_schemes: BTreeMap<Cow<'static, str>, SecurityScheme>,
223}
224
225impl Components {
226 fn is_empty(&self) -> bool {
228 self.schemas.is_empty() && self.security_schemes.is_empty()
229 }
230}
231
232#[allow(clippy::trivially_copy_pass_by_ref)]
234fn is_false(flag: &bool) -> bool {
235 !flag
236}