Skip to main content

topcoat_openapi/
document.rs

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/// An OpenAPI document that describes the documented routes of a router.
16///
17/// Create it with [`OpenApi::generate`](crate::OpenApi::generate). Return it
18/// from a route to serve it as JSON, or write it to a file with
19/// [`to_json_pretty`](Self::to_json_pretty). It also implements
20/// [`Serialize`] for other formats.
21#[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    /// Returns the document as compact JSON.
36    ///
37    /// # Panics
38    ///
39    /// Panics if a schema in the document cannot be written as JSON.
40    #[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    /// Returns the document as indented JSON, which suits a file kept in
46    /// version control.
47    ///
48    /// # Panics
49    ///
50    /// Panics if a schema in the document cannot be written as JSON.
51    #[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
57/// Sends the document as JSON.
58impl IntoResponse for &Document {
59    fn into_response(self, cx: &Cx) -> Result<Response> {
60        Json(self).into_response(cx)
61    }
62}
63
64/// Sends the document as JSON.
65impl IntoResponse for Document {
66    fn into_response(self, cx: &Cx) -> Result<Response> {
67        (&self).into_response(cx)
68    }
69}
70
71/// General information about the API.
72#[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/// A base URL the API is reachable at.
81#[derive(Debug, Clone, Serialize)]
82pub(crate) struct Server {
83    pub(crate) url: Cow<'static, str>,
84}
85
86/// A description of a tag that operations use.
87#[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/// The operations served at one path, by method.
94#[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    /// The methods a path item can describe.
116    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    /// Returns whether a path item can describe an operation for `method`.
128    pub(crate) fn describes(method: &Method) -> bool {
129        Self::METHODS.contains(method)
130    }
131
132    /// Returns the place of the operation served for `method`, or `None` for
133    /// a method that a path item cannot describe.
134    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/// One operation: a method served at a path.
153#[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
176/// One accepted way to authenticate: the name of a security scheme, mapped
177/// to the scopes it needs.
178pub(crate) type SecurityRequirement = BTreeMap<Cow<'static, str>, Vec<Cow<'static, str>>>;
179
180/// A parameter of an operation.
181#[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/// The request body of an operation.
194#[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/// One response of an operation.
202#[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/// The payload of a body in one media type.
210#[derive(Debug, Clone, Serialize)]
211pub(crate) struct MediaType {
212    pub(crate) schema: Schema,
213}
214
215/// The definitions that the rest of the document refers to.
216#[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    /// Returns whether there is nothing to list.
227    fn is_empty(&self) -> bool {
228        self.schemas.is_empty() && self.security_schemes.is_empty()
229    }
230}
231
232/// Lets serde skip a flag that is not set.
233#[allow(clippy::trivially_copy_pass_by_ref)]
234fn is_false(flag: &bool) -> bool {
235    !flag
236}