Skip to main content

salvo_oapi/openapi/
operation.rs

1//! Implements [OpenAPI Operation Object][operation] types.
2//!
3//! [operation]: https://spec.openapis.org/oas/latest.html#operation-object
4use std::ops::{Deref, DerefMut};
5
6use serde::{Deserialize, Serialize};
7
8use super::request_body::RequestBody;
9use super::response::{Response, Responses};
10use super::{Deprecated, ExternalDocs, RefOr, SecurityRequirement, Server};
11use crate::{Parameter, Parameters, PathItemType, PropMap, Servers};
12
13/// Collection for save [`Operation`]s.
14#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
15pub struct Operations(pub PropMap<PathItemType, Operation>);
16impl Deref for Operations {
17    type Target = PropMap<PathItemType, Operation>;
18
19    fn deref(&self) -> &Self::Target {
20        &self.0
21    }
22}
23impl DerefMut for Operations {
24    fn deref_mut(&mut self) -> &mut Self::Target {
25        &mut self.0
26    }
27}
28impl IntoIterator for Operations {
29    type Item = (PathItemType, Operation);
30    type IntoIter = <PropMap<PathItemType, Operation> as IntoIterator>::IntoIter;
31
32    fn into_iter(self) -> Self::IntoIter {
33        self.0.into_iter()
34    }
35}
36impl Operations {
37    /// Construct a new empty [`Operations`]. This is effectively same as calling
38    /// [`Operations::default`].
39    #[must_use]
40    pub fn new() -> Self {
41        Default::default()
42    }
43
44    /// Returns `true` if instance contains no elements.
45    #[must_use]
46    pub fn is_empty(&self) -> bool {
47        self.0.is_empty()
48    }
49    /// Add a new operation and returns `self`.
50    #[must_use]
51    pub fn operation<K: Into<PathItemType>, O: Into<Operation>>(
52        mut self,
53        item_type: K,
54        operation: O,
55    ) -> Self {
56        self.insert(item_type, operation);
57        self
58    }
59
60    /// Inserts a key-value pair into the instance.
61    pub fn insert<K: Into<PathItemType>, O: Into<Operation>>(
62        &mut self,
63        item_type: K,
64        operation: O,
65    ) {
66        self.0.insert(item_type.into(), operation.into());
67    }
68
69    /// Moves all elements from `other` into `self`, leaving `other` empty.
70    ///
71    /// If a key from `other` is already present in `self`, the respective
72    /// value from `self` will be overwritten with the respective value from `other`.
73    pub fn append(&mut self, other: &mut Self) {
74        self.0.append(&mut other.0);
75    }
76    /// Extends a collection with the contents of an iterator.
77    pub fn extend<I>(&mut self, iter: I)
78    where
79        I: IntoIterator<Item = (PathItemType, Operation)>,
80    {
81        for (item_type, operation) in iter {
82            self.insert(item_type, operation);
83        }
84    }
85}
86
87/// Implements [OpenAPI Operation Object][operation] object.
88///
89/// [operation]: https://spec.openapis.org/oas/latest.html#operation-object
90#[non_exhaustive]
91#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
92#[serde(rename_all = "camelCase")]
93pub struct Operation {
94    /// List of tags used for grouping operations.
95    ///
96    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
97    /// default value used will be resolved from handler path provided in
98    /// `#[openapi(paths(...))]` with [`#[derive(OpenApi)]`][derive_openapi] macro. If path
99    /// resolves to `None` value `crate` will be used by default.
100    ///
101    /// [derive_path]: ../../attr.path.html
102    /// [derive_openapi]: ../../derive.OpenApi.html
103    #[serde(skip_serializing_if = "Vec::is_empty")]
104    pub tags: Vec<String>,
105
106    /// Short summary what [`Operation`] does.
107    ///
108    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
109    /// value is taken from **first line** of doc comment.
110    ///
111    /// [derive_path]: ../../attr.path.html
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub summary: Option<String>,
114
115    /// Long explanation of [`Operation`] behaviour. Markdown syntax is supported.
116    ///
117    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
118    /// doc comment is used as value for description.
119    ///
120    /// [derive_path]: ../../attr.path.html
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub description: Option<String>,
123
124    /// Unique identifier for the API [`Operation`]. Most typically this is mapped to handler
125    /// function name.
126    ///
127    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
128    /// handler function name will be used by default.
129    ///
130    /// [derive_path]: ../../attr.path.html
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub operation_id: Option<String>,
133
134    /// Additional external documentation for this operation.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub external_docs: Option<ExternalDocs>,
137
138    /// List of applicable parameters for this [`Operation`].
139    #[serde(skip_serializing_if = "Parameters::is_empty")]
140    pub parameters: Parameters,
141
142    /// Optional request body for this [`Operation`].
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub request_body: Option<RequestBody>,
145
146    /// List of possible responses returned by the [`Operation`].
147    pub responses: Responses,
148
149    /// Callback information.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub callbacks: Option<String>,
152
153    /// Define whether the operation is deprecated or not and thus should be avoided consuming.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub deprecated: Option<Deprecated>,
156
157    /// Declaration which security mechanisms can be used for for the operation. Only one
158    /// [`SecurityRequirement`] must be met.
159    ///
160    /// Security for the [`Operation`] can be set to optional by adding empty security with
161    /// [`SecurityRequirement::default`].
162    #[serde(skip_serializing_if = "Vec::is_empty")]
163    #[serde(rename = "security")]
164    pub securities: Vec<SecurityRequirement>,
165
166    /// Alternative [`Server`]s for this [`Operation`].
167    #[serde(skip_serializing_if = "Servers::is_empty")]
168    pub servers: Servers,
169
170    /// Optional extensions "x-something"
171    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
172    pub extensions: PropMap<String, serde_json::Value>,
173}
174
175impl Operation {
176    /// Construct a new API [`Operation`].
177    #[must_use]
178    pub fn new() -> Self {
179        Default::default()
180    }
181
182    /// Add or change tags of the [`Operation`].
183    #[must_use]
184    pub fn tags<I, T>(mut self, tags: I) -> Self
185    where
186        I: IntoIterator<Item = T>,
187        T: Into<String>,
188    {
189        self.tags = tags.into_iter().map(|t| t.into()).collect();
190        self
191    }
192    /// Append tag to [`Operation`] tags and returns `Self`.
193    #[must_use]
194    pub fn add_tag<S: Into<String>>(mut self, tag: S) -> Self {
195        self.tags.push(tag.into());
196        self
197    }
198
199    /// Add or change short summary of the [`Operation`].
200    #[must_use]
201    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
202        self.summary = Some(summary.into());
203        self
204    }
205
206    /// Add or change description of the [`Operation`].
207    #[must_use]
208    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
209        self.description = Some(description.into());
210        self
211    }
212
213    /// Add or change operation id of the [`Operation`].
214    #[must_use]
215    pub fn operation_id<S: Into<String>>(mut self, operation_id: S) -> Self {
216        self.operation_id = Some(operation_id.into());
217        self
218    }
219
220    /// Add or change parameters of the [`Operation`].
221    #[must_use]
222    pub fn parameters<I: IntoIterator<Item = P>, P: Into<Parameter>>(
223        mut self,
224        parameters: I,
225    ) -> Self {
226        self.parameters
227            .extend(parameters.into_iter().map(|parameter| parameter.into()));
228        self
229    }
230    /// Append parameter to [`Operation`] parameters and returns `Self`.
231    #[must_use]
232    pub fn add_parameter<P: Into<Parameter>>(mut self, parameter: P) -> Self {
233        self.parameters.insert(parameter);
234        self
235    }
236
237    /// Add or change request body of the [`Operation`].
238    #[must_use]
239    pub fn request_body(mut self, request_body: RequestBody) -> Self {
240        self.request_body = Some(request_body);
241        self
242    }
243
244    /// Add or change responses of the [`Operation`].
245    #[must_use]
246    pub fn responses<R: Into<Responses>>(mut self, responses: R) -> Self {
247        self.responses = responses.into();
248        self
249    }
250    /// Append status code and a [`Response`] to the [`Operation`] responses map and returns `Self`.
251    ///
252    /// * `code` must be valid HTTP status code.
253    /// * `response` is instances of [`Response`].
254    #[must_use]
255    pub fn add_response<S: Into<String>, R: Into<RefOr<Response>>>(
256        mut self,
257        code: S,
258        response: R,
259    ) -> Self {
260        self.responses.insert(code, response);
261        self
262    }
263
264    /// Add or change deprecated status of the [`Operation`].
265    #[must_use]
266    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
267        self.deprecated = Some(deprecated.into());
268        self
269    }
270
271    /// Add or change list of [`SecurityRequirement`]s that are available for [`Operation`].
272    #[must_use]
273    pub fn securities<I: IntoIterator<Item = SecurityRequirement>>(
274        mut self,
275        securities: I,
276    ) -> Self {
277        self.securities = securities.into_iter().collect();
278        self
279    }
280    /// Append [`SecurityRequirement`] to [`Operation`] security requirements and returns `Self`.
281    #[must_use]
282    pub fn add_security(mut self, security: SecurityRequirement) -> Self {
283        self.securities.push(security);
284        self
285    }
286
287    /// Add or change list of [`Server`]s of the [`Operation`].
288    #[must_use]
289    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: I) -> Self {
290        self.servers = Servers(servers.into_iter().collect());
291        self
292    }
293    /// Append a new [`Server`] to the [`Operation`] servers and returns `Self`.
294    #[must_use]
295    pub fn add_server(mut self, server: Server) -> Self {
296        self.servers.insert(server);
297        self
298    }
299
300    /// For easy chaining of operations.
301    #[must_use]
302    pub fn then<F>(self, func: F) -> Self
303    where
304        F: FnOnce(Self) -> Self,
305    {
306        func(self)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use assert_json_diff::assert_json_eq;
313    use serde_json::json;
314
315    use super::{Operation, Operations};
316    use crate::security::SecurityRequirement;
317    use crate::server::Server;
318    use crate::{Deprecated, Parameter, PathItemType, RequestBody, Responses};
319
320    #[test]
321    fn operation_new() {
322        let operation = Operation::new();
323
324        assert!(operation.tags.is_empty());
325        assert!(operation.summary.is_none());
326        assert!(operation.description.is_none());
327        assert!(operation.operation_id.is_none());
328        assert!(operation.external_docs.is_none());
329        assert!(operation.parameters.is_empty());
330        assert!(operation.request_body.is_none());
331        assert!(operation.responses.is_empty());
332        assert!(operation.callbacks.is_none());
333        assert!(operation.deprecated.is_none());
334        assert!(operation.securities.is_empty());
335        assert!(operation.servers.is_empty());
336    }
337
338    #[test]
339    fn test_build_operation() {
340        let operation = Operation::new()
341            .tags(["tag1", "tag2"])
342            .add_tag("tag3")
343            .summary("summary")
344            .description("description")
345            .operation_id("operation_id")
346            .parameters([Parameter::new("param1")])
347            .add_parameter(Parameter::new("param2"))
348            .request_body(RequestBody::new())
349            .responses(Responses::new())
350            .deprecated(Deprecated::False)
351            .securities([SecurityRequirement::new("api_key", ["read:items"])])
352            .servers([Server::new("/api")]);
353
354        assert_json_eq!(
355            operation,
356            json!({
357                "responses": {},
358                "parameters": [
359                    {
360                        "name": "param1",
361                        "in": "path",
362                        "required": false
363                    },
364                    {
365                        "name": "param2",
366                        "in": "path",
367                        "required": false
368                    }
369                ],
370                "operationId": "operation_id",
371                "deprecated": false,
372                "security": [
373                    {
374                        "api_key": ["read:items"]
375                    }
376                ],
377                "servers": [{"url": "/api"}],
378                "summary": "summary",
379                "tags": ["tag1", "tag2", "tag3"],
380                "description": "description",
381                "requestBody": {
382                    "content": {}
383                }
384            })
385        );
386    }
387
388    #[test]
389    fn operation_security() {
390        let security_requirement1 =
391            SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
392        let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
393        let operation = Operation::new()
394            .add_security(security_requirement1)
395            .add_security(security_requirement2);
396
397        assert!(!operation.securities.is_empty());
398    }
399
400    #[test]
401    fn operation_server() {
402        let server1 = Server::new("/api");
403        let server2 = Server::new("/admin");
404        let operation = Operation::new().add_server(server1).add_server(server2);
405        assert!(!operation.servers.is_empty());
406    }
407
408    #[test]
409    fn test_operations() {
410        let operations = Operations::new();
411        assert!(operations.is_empty());
412
413        let mut operations = operations.operation(PathItemType::Get, Operation::new());
414        operations.insert(PathItemType::Post, Operation::new());
415        operations.extend([(PathItemType::Head, Operation::new())]);
416        assert_eq!(3, operations.len());
417    }
418
419    #[test]
420    fn test_operations_into_iter() {
421        let mut operations = Operations::new();
422        operations.insert(PathItemType::Get, Operation::new());
423        operations.insert(PathItemType::Post, Operation::new());
424        operations.insert(PathItemType::Head, Operation::new());
425
426        let mut iter = operations.into_iter();
427        assert_eq!((PathItemType::Get, Operation::new()), iter.next().unwrap());
428        assert_eq!((PathItemType::Post, Operation::new()), iter.next().unwrap());
429        assert_eq!((PathItemType::Head, Operation::new()), iter.next().unwrap());
430    }
431
432    #[test]
433    fn test_operations_then() {
434        let print_operation = |operation: Operation| {
435            println!("{operation:?}");
436            operation
437        };
438        let operation = Operation::new();
439
440        let _ = operation.then(print_operation);
441    }
442}