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::{
9    Deprecated, ExternalDocs, RefOr, SecurityRequirement, Server,
10    request_body::RequestBody,
11    response::{Response, Responses},
12};
13use crate::{Parameter, Parameters, PathItemType, PropMap, Servers};
14
15/// Collection for save [`Operation`]s.
16#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
17pub struct Operations(pub PropMap<PathItemType, Operation>);
18impl Deref for Operations {
19    type Target = PropMap<PathItemType, Operation>;
20
21    fn deref(&self) -> &Self::Target {
22        &self.0
23    }
24}
25impl DerefMut for Operations {
26    fn deref_mut(&mut self) -> &mut Self::Target {
27        &mut self.0
28    }
29}
30impl IntoIterator for Operations {
31    type Item = (PathItemType, Operation);
32    type IntoIter = <PropMap<PathItemType, Operation> as IntoIterator>::IntoIter;
33
34    fn into_iter(self) -> Self::IntoIter {
35        self.0.into_iter()
36    }
37}
38impl Operations {
39    /// Construct a new empty [`Operations`]. This is effectively same as calling [`Operations::default`].
40    #[must_use]
41    pub fn new() -> Self {
42        Default::default()
43    }
44
45    /// Returns `true` if instance contains no elements.
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50    /// Add a new operation and returns `self`.
51    #[must_use]
52    pub fn operation<K: Into<PathItemType>, O: Into<Operation>>(
53        mut self,
54        item_type: K,
55        operation: O,
56    ) -> Self {
57        self.insert(item_type, operation);
58        self
59    }
60
61    /// Inserts a key-value pair into the instance.
62    pub fn insert<K: Into<PathItemType>, O: Into<Operation>>(
63        &mut self,
64        item_type: K,
65        operation: O,
66    ) {
67        self.0.insert(item_type.into(), operation.into());
68    }
69
70    /// Moves all elements from `other` into `self`, leaving `other` empty.
71    ///
72    /// If a key from `other` is already present in `self`, the respective
73    /// value from `self` will be overwritten with the respective value from `other`.
74    pub fn append(&mut self, other: &mut Self) {
75        self.0.append(&mut other.0);
76    }
77    /// Extends a collection with the contents of an iterator.
78    pub fn extend<I>(&mut self, iter: I)
79    where
80        I: IntoIterator<Item = (PathItemType, Operation)>,
81    {
82        for (item_type, operation) in iter {
83            self.insert(item_type, operation);
84        }
85    }
86}
87
88/// Implements [OpenAPI Operation Object][operation] object.
89///
90/// [operation]: https://spec.openapis.org/oas/latest.html#operation-object
91#[non_exhaustive]
92#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
93#[serde(rename_all = "camelCase")]
94pub struct Operation {
95    /// List of tags used for grouping operations.
96    ///
97    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the default
98    /// value used will be resolved from handler path provided in `#[openapi(paths(...))]` with
99    /// [`#[derive(OpenApi)]`][derive_openapi] macro. If path resolves to `None` value `crate` will
100    /// be used by default.
101    ///
102    /// [derive_path]: ../../attr.path.html
103    /// [derive_openapi]: ../../derive.OpenApi.html
104    #[serde(skip_serializing_if = "Vec::is_empty")]
105    pub tags: Vec<String>,
106
107    /// Short summary what [`Operation`] does.
108    ///
109    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the value
110    /// is taken from **first line** of doc comment.
111    ///
112    /// [derive_path]: ../../attr.path.html
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub summary: Option<String>,
115
116    /// Long explanation of [`Operation`] behaviour. Markdown syntax is supported.
117    ///
118    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
119    /// doc comment is used as value for description.
120    ///
121    /// [derive_path]: ../../attr.path.html
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub description: Option<String>,
124
125    /// Unique identifier for the API [`Operation`]. Most typically this is mapped to handler function name.
126    ///
127    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the handler function
128    /// 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::{
317        Deprecated, Parameter, PathItemType, RequestBody, Responses, security::SecurityRequirement,
318        server::Server,
319    };
320
321    #[test]
322    fn operation_new() {
323        let operation = Operation::new();
324
325        assert!(operation.tags.is_empty());
326        assert!(operation.summary.is_none());
327        assert!(operation.description.is_none());
328        assert!(operation.operation_id.is_none());
329        assert!(operation.external_docs.is_none());
330        assert!(operation.parameters.is_empty());
331        assert!(operation.request_body.is_none());
332        assert!(operation.responses.is_empty());
333        assert!(operation.callbacks.is_none());
334        assert!(operation.deprecated.is_none());
335        assert!(operation.securities.is_empty());
336        assert!(operation.servers.is_empty());
337    }
338
339    #[test]
340    fn test_build_operation() {
341        let operation = Operation::new()
342            .tags(["tag1", "tag2"])
343            .add_tag("tag3")
344            .summary("summary")
345            .description("description")
346            .operation_id("operation_id")
347            .parameters([Parameter::new("param1")])
348            .add_parameter(Parameter::new("param2"))
349            .request_body(RequestBody::new())
350            .responses(Responses::new())
351            .deprecated(Deprecated::False)
352            .securities([SecurityRequirement::new("api_key", ["read:items"])])
353            .servers([Server::new("/api")]);
354
355        assert_json_eq!(
356            operation,
357            json!({
358                "responses": {},
359                "parameters": [
360                    {
361                        "name": "param1",
362                        "in": "path",
363                        "required": false
364                    },
365                    {
366                        "name": "param2",
367                        "in": "path",
368                        "required": false
369                    }
370                ],
371                "operationId": "operation_id",
372                "deprecated": false,
373                "security": [
374                    {
375                        "api_key": ["read:items"]
376                    }
377                ],
378                "servers": [{"url": "/api"}],
379                "summary": "summary",
380                "tags": ["tag1", "tag2", "tag3"],
381                "description": "description",
382                "requestBody": {
383                    "content": {}
384                }
385            })
386        );
387    }
388
389    #[test]
390    fn operation_security() {
391        let security_requirement1 =
392            SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
393        let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
394        let operation = Operation::new()
395            .add_security(security_requirement1)
396            .add_security(security_requirement2);
397
398        assert!(!operation.securities.is_empty());
399    }
400
401    #[test]
402    fn operation_server() {
403        let server1 = Server::new("/api");
404        let server2 = Server::new("/admin");
405        let operation = Operation::new().add_server(server1).add_server(server2);
406        assert!(!operation.servers.is_empty());
407    }
408
409    #[test]
410    fn test_operations() {
411        let operations = Operations::new();
412        assert!(operations.is_empty());
413
414        let mut operations = operations.operation(PathItemType::Get, Operation::new());
415        operations.insert(PathItemType::Post, Operation::new());
416        operations.extend([(PathItemType::Head, Operation::new())]);
417        assert_eq!(3, operations.len());
418    }
419
420    #[test]
421    fn test_operations_into_iter() {
422        let mut operations = Operations::new();
423        operations.insert(PathItemType::Get, Operation::new());
424        operations.insert(PathItemType::Post, Operation::new());
425        operations.insert(PathItemType::Head, Operation::new());
426
427        let mut iter = operations.into_iter();
428        assert_eq!((PathItemType::Get, Operation::new()), iter.next().unwrap());
429        assert_eq!((PathItemType::Post, Operation::new()), iter.next().unwrap());
430        assert_eq!((PathItemType::Head, Operation::new()), iter.next().unwrap());
431    }
432
433    #[test]
434    fn test_operations_then() {
435        let print_operation = |operation: Operation| {
436            println!("{operation:?}");
437            operation
438        };
439        let operation = Operation::new();
440
441        let _ = operation.then(print_operation);
442    }
443}