utoipa/openapi/
path.rs

1//! Implements [OpenAPI Path Object][paths] types.
2//!
3//! [paths]: https://spec.openapis.org/oas/latest.html#paths-object
4use std::iter;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use super::{
10    builder,
11    request_body::RequestBody,
12    response::{Response, Responses},
13    set_value, Deprecated, ExternalDocs, RefOr, Required, Schema, SecurityRequirement, Server,
14};
15
16#[cfg(not(feature = "preserve_path_order"))]
17type PathsMap<K, V> = std::collections::BTreeMap<K, V>;
18#[cfg(feature = "preserve_path_order")]
19type PathsMap<K, V> = indexmap::IndexMap<K, V>;
20
21builder! {
22    PathsBuilder;
23
24    /// Implements [OpenAPI Paths Object][paths].
25    ///
26    /// Holds relative paths to matching endpoints and operations. The path is appended to the url
27    /// from [`Server`] object to construct a full url for endpoint.
28    ///
29    /// [paths]: https://spec.openapis.org/oas/latest.html#paths-object
30    #[non_exhaustive]
31    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
32    #[cfg_attr(feature = "debug", derive(Debug))]
33    pub struct Paths {
34        /// Map of relative paths with [`PathItem`]s holding [`Operation`]s matching
35        /// api endpoints.
36        pub paths: PathsMap<String, PathItem>,
37    }
38}
39
40impl Paths {
41    /// Construct a new [`Paths`] object.
42    pub fn new() -> Self {
43        Default::default()
44    }
45
46    /// Return _`Option`_ of reference to [`PathItem`] by given relative path _`P`_ if one exists
47    /// in [`Paths::paths`] map. Otherwise will return `None`.
48    ///
49    /// # Examples
50    ///
51    /// _**Get user path item.**_
52    /// ```rust
53    /// # use utoipa::openapi::path::{Paths, PathItemType};
54    /// # let paths = Paths::new();
55    /// let path_item = paths.get_path_item("/api/v1/user");
56    /// ```
57    pub fn get_path_item<P: AsRef<str>>(&self, path: P) -> Option<&PathItem> {
58        self.paths.get(path.as_ref())
59    }
60
61    /// Return _`Option`_ of reference to [`Operation`] from map of paths or `None` if not found.
62    ///
63    /// * First will try to find [`PathItem`] by given relative path _`P`_ e.g. `"/api/v1/user"`.
64    /// * Then tries to find [`Operation`] from [`PathItem`]'s operations by given [`PathItemType`].
65    ///
66    /// # Examples
67    ///
68    /// _**Get user operation from paths.**_
69    /// ```rust
70    /// # use utoipa::openapi::path::{Paths, PathItemType};
71    /// # let paths = Paths::new();
72    /// let operation = paths.get_path_operation("/api/v1/user", PathItemType::Get);
73    /// ```
74    pub fn get_path_operation<P: AsRef<str>>(
75        &self,
76        path: P,
77        item_type: PathItemType,
78    ) -> Option<&Operation> {
79        self.paths
80            .get(path.as_ref())
81            .and_then(|path| path.operations.get(&item_type))
82    }
83}
84
85impl PathsBuilder {
86    /// Append [`PathItem`] with path to map of paths. If path already exists it will merge [`Operation`]s of
87    /// [`PathItem`] with already found path item operations.
88    pub fn path<I: Into<String>>(mut self, path: I, item: PathItem) -> Self {
89        let path_string = path.into();
90        if let Some(existing_item) = self.paths.get_mut(&path_string) {
91            existing_item
92                .operations
93                .extend(&mut item.operations.into_iter());
94        } else {
95            self.paths.insert(path_string, item);
96        }
97
98        self
99    }
100}
101
102builder! {
103    PathItemBuilder;
104
105    /// Implements [OpenAPI Path Item Object][path_item] what describes [`Operation`]s available on
106    /// a single path.
107    ///
108    /// [path_item]: https://spec.openapis.org/oas/latest.html#path-item-object
109    #[non_exhaustive]
110    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
111    #[cfg_attr(feature = "debug", derive(Debug))]
112    #[serde(rename_all = "camelCase")]
113    pub struct PathItem {
114        /// Optional summary intended to apply all operations in this [`PathItem`].
115        #[serde(skip_serializing_if = "Option::is_none")]
116        pub summary: Option<String>,
117
118        /// Optional description intended to apply all operations in this [`PathItem`].
119        /// Description supports markdown syntax.
120        #[serde(skip_serializing_if = "Option::is_none")]
121        pub description: Option<String>,
122
123        /// Alternative [`Server`] array to serve all [`Operation`]s in this [`PathItem`] overriding
124        /// the global server array.
125        #[serde(skip_serializing_if = "Option::is_none")]
126        pub servers: Option<Vec<Server>>,
127
128        /// List of [`Parameter`]s common to all [`Operation`]s in this [`PathItem`]. Parameters cannot
129        /// contain duplicate parameters. They can be overridden in [`Operation`] level but cannot be
130        /// removed there.
131        #[serde(skip_serializing_if = "Option::is_none")]
132        pub parameters: Option<Vec<Parameter>>,
133
134        /// Map of operations in this [`PathItem`]. Operations can hold only one operation
135        /// per [`PathItemType`].
136        #[serde(flatten)]
137        pub operations: PathsMap<PathItemType, Operation>,
138    }
139}
140
141impl PathItem {
142    /// Construct a new [`PathItem`] with provided [`Operation`] mapped to given [`PathItemType`].
143    pub fn new<O: Into<Operation>>(path_item_type: PathItemType, operation: O) -> Self {
144        let operations = PathsMap::from_iter(iter::once((path_item_type, operation.into())));
145
146        Self {
147            operations,
148            ..Default::default()
149        }
150    }
151}
152
153impl PathItemBuilder {
154    /// Append a new [`Operation`] by [`PathItemType`] to this [`PathItem`]. Operations can
155    /// hold only one operation per [`PathItemType`].
156    pub fn operation<O: Into<Operation>>(
157        mut self,
158        path_item_type: PathItemType,
159        operation: O,
160    ) -> Self {
161        self.operations.insert(path_item_type, operation.into());
162
163        self
164    }
165
166    /// Add or change summary intended to apply all operations in this [`PathItem`].
167    pub fn summary<S: Into<String>>(mut self, summary: Option<S>) -> Self {
168        set_value!(self summary summary.map(|summary| summary.into()))
169    }
170
171    /// Add or change optional description intended to apply all operations in this [`PathItem`].
172    /// Description supports markdown syntax.
173    pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
174        set_value!(self description description.map(|description| description.into()))
175    }
176
177    /// Add list of alternative [`Server`]s to serve all [`Operation`]s in this [`PathItem`] overriding
178    /// the global server array.
179    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
180        set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
181    }
182
183    /// Append list of [`Parameter`]s common to all [`Operation`]s to this [`PathItem`].
184    pub fn parameters<I: IntoIterator<Item = Parameter>>(mut self, parameters: Option<I>) -> Self {
185        set_value!(self parameters parameters.map(|parameters| parameters.into_iter().collect()))
186    }
187}
188
189/// Path item operation type.
190#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)]
191#[serde(rename_all = "lowercase")]
192#[cfg_attr(feature = "debug", derive(Debug))]
193pub enum PathItemType {
194    /// Type mapping for HTTP _GET_ request.
195    Get,
196    /// Type mapping for HTTP _POST_ request.
197    Post,
198    /// Type mapping for HTTP _PUT_ request.
199    Put,
200    /// Type mapping for HTTP _DELETE_ request.
201    Delete,
202    /// Type mapping for HTTP _OPTIONS_ request.
203    Options,
204    /// Type mapping for HTTP _HEAD_ request.
205    Head,
206    /// Type mapping for HTTP _PATCH_ request.
207    Patch,
208    /// Type mapping for HTTP _TRACE_ request.
209    Trace,
210    /// Type mapping for HTTP _CONNECT_ request.
211    Connect,
212}
213
214builder! {
215    OperationBuilder;
216
217    /// Implements [OpenAPI Operation Object][operation] object.
218    ///
219    /// [operation]: https://spec.openapis.org/oas/latest.html#operation-object
220    #[non_exhaustive]
221    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
222    #[cfg_attr(feature = "debug", derive(Debug))]
223    #[serde(rename_all = "camelCase")]
224    pub struct Operation {
225        /// List of tags used for grouping operations.
226        ///
227        /// When used with derive [`#[utoipa::path(...)]`][derive_path] attribute macro the default
228        /// value used will be resolved from handler path provided in `#[openapi(paths(...))]` with
229        /// [`#[derive(OpenApi)]`][derive_openapi] macro. If path resolves to `None` value `crate` will
230        /// be used by default.
231        ///
232        /// [derive_path]: ../../attr.path.html
233        /// [derive_openapi]: ../../derive.OpenApi.html
234        #[serde(skip_serializing_if = "Option::is_none")]
235        pub tags: Option<Vec<String>>,
236
237        /// Short summary what [`Operation`] does.
238        ///
239        /// When used with derive [`#[utoipa::path(...)]`][derive_path] attribute macro the value
240        /// is taken from **first line** of doc comment.
241        ///
242        /// [derive_path]: ../../attr.path.html
243        #[serde(skip_serializing_if = "Option::is_none")]
244        pub summary: Option<String>,
245
246        /// Long explanation of [`Operation`] behaviour. Markdown syntax is supported.
247        ///
248        /// When used with derive [`#[utoipa::path(...)]`][derive_path] attribute macro the
249        /// doc comment is used as value for description.
250        ///
251        /// [derive_path]: ../../attr.path.html
252        #[serde(skip_serializing_if = "Option::is_none")]
253        pub description: Option<String>,
254
255        /// Unique identifier for the API [`Operation`]. Most typically this is mapped to handler function name.
256        ///
257        /// When used with derive [`#[utoipa::path(...)]`][derive_path] attribute macro the handler function
258        /// name will be used by default.
259        ///
260        /// [derive_path]: ../../attr.path.html
261        #[serde(skip_serializing_if = "Option::is_none")]
262        pub operation_id: Option<String>,
263
264        /// Additional external documentation for this operation.
265        #[serde(skip_serializing_if = "Option::is_none")]
266        pub external_docs: Option<ExternalDocs>,
267
268        /// List of applicable parameters for this [`Operation`].
269        #[serde(skip_serializing_if = "Option::is_none")]
270        pub parameters: Option<Vec<Parameter>>,
271
272        /// Optional request body for this [`Operation`].
273        #[serde(skip_serializing_if = "Option::is_none")]
274        pub request_body: Option<RequestBody>,
275
276        /// List of possible responses returned by the [`Operation`].
277        pub responses: Responses,
278
279        // TODO
280        #[serde(skip_serializing_if = "Option::is_none")]
281        pub callbacks: Option<String>,
282
283        /// Define whether the operation is deprecated or not and thus should be avoided consuming.
284        #[serde(skip_serializing_if = "Option::is_none")]
285        pub deprecated: Option<Deprecated>,
286
287        /// Declaration which security mechanisms can be used for for the operation. Only one
288        /// [`SecurityRequirement`] must be met.
289        ///
290        /// Security for the [`Operation`] can be set to optional by adding empty security with
291        /// [`SecurityRequirement::default`].
292        #[serde(skip_serializing_if = "Option::is_none")]
293        pub security: Option<Vec<SecurityRequirement>>,
294
295        /// Alternative [`Server`]s for this [`Operation`].
296        #[serde(skip_serializing_if = "Option::is_none")]
297        pub servers: Option<Vec<Server>>,
298    }
299}
300
301impl Operation {
302    /// Construct a new API [`Operation`].
303    pub fn new() -> Self {
304        Default::default()
305    }
306}
307
308impl OperationBuilder {
309    /// Add or change tags of the [`Operation`].
310    pub fn tags<I: IntoIterator<Item = String>>(mut self, tags: Option<I>) -> Self {
311        set_value!(self tags tags.map(|tags| tags.into_iter().collect()))
312    }
313
314    /// Append tag to [`Operation`] tags.
315    pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
316        let tag_string = tag.into();
317        match self.tags {
318            Some(ref mut tags) => tags.push(tag_string),
319            None => {
320                self.tags = Some(vec![tag_string]);
321            }
322        }
323
324        self
325    }
326
327    /// Add or change short summary of the [`Operation`].
328    pub fn summary<S: Into<String>>(mut self, summary: Option<S>) -> Self {
329        set_value!(self summary summary.map(|summary| summary.into()))
330    }
331
332    /// Add or change description of the [`Operation`].
333    pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
334        set_value!(self description description.map(|description| description.into()))
335    }
336
337    /// Add or change operation id of the [`Operation`].
338    pub fn operation_id<S: Into<String>>(mut self, operation_id: Option<S>) -> Self {
339        set_value!(self operation_id operation_id.map(|operation_id| operation_id.into()))
340    }
341
342    /// Add or change parameters of the [`Operation`].
343    pub fn parameters<I: IntoIterator<Item = P>, P: Into<Parameter>>(
344        mut self,
345        parameters: Option<I>,
346    ) -> Self {
347        self.parameters = parameters.map(|parameters| {
348            if let Some(mut params) = self.parameters {
349                params.extend(parameters.into_iter().map(|parameter| parameter.into()));
350                params
351            } else {
352                parameters
353                    .into_iter()
354                    .map(|parameter| parameter.into())
355                    .collect()
356            }
357        });
358
359        self
360    }
361
362    /// Append parameter to [`Operation`] parameters.
363    pub fn parameter<P: Into<Parameter>>(mut self, parameter: P) -> Self {
364        match self.parameters {
365            Some(ref mut parameters) => parameters.push(parameter.into()),
366            None => {
367                self.parameters = Some(vec![parameter.into()]);
368            }
369        }
370
371        self
372    }
373
374    /// Add or change request body of the [`Operation`].
375    pub fn request_body(mut self, request_body: Option<RequestBody>) -> Self {
376        set_value!(self request_body request_body)
377    }
378
379    /// Add or change responses of the [`Operation`].
380    pub fn responses<R: Into<Responses>>(mut self, responses: R) -> Self {
381        set_value!(self responses responses.into())
382    }
383
384    /// Append status code and a [`Response`] to the [`Operation`] responses map.
385    ///
386    /// * `code` must be valid HTTP status code.
387    /// * `response` is instances of [`Response`].
388    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
389        mut self,
390        code: S,
391        response: R,
392    ) -> Self {
393        self.responses
394            .responses
395            .insert(code.into(), response.into());
396
397        self
398    }
399
400    /// Add or change deprecated status of the [`Operation`].
401    pub fn deprecated(mut self, deprecated: Option<Deprecated>) -> Self {
402        set_value!(self deprecated deprecated)
403    }
404
405    /// Add or change list of [`SecurityRequirement`]s that are available for [`Operation`].
406    pub fn securities<I: IntoIterator<Item = SecurityRequirement>>(
407        mut self,
408        securities: Option<I>,
409    ) -> Self {
410        set_value!(self security securities.map(|securities| securities.into_iter().collect()))
411    }
412
413    /// Append [`SecurityRequirement`] to [`Operation`] security requirements.
414    pub fn security(mut self, security: SecurityRequirement) -> Self {
415        if let Some(ref mut securities) = self.security {
416            securities.push(security);
417        } else {
418            self.security = Some(vec![security]);
419        }
420
421        self
422    }
423
424    /// Add or change list of [`Server`]s of the [`Operation`].
425    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
426        set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
427    }
428
429    /// Append a new [`Server`] to the [`Operation`] servers.
430    pub fn server(mut self, server: Server) -> Self {
431        if let Some(ref mut servers) = self.servers {
432            servers.push(server);
433        } else {
434            self.servers = Some(vec![server]);
435        }
436
437        self
438    }
439}
440
441builder! {
442    ParameterBuilder;
443
444    /// Implements [OpenAPI Parameter Object][parameter] for [`Operation`].
445    ///
446    /// [parameter]: https://spec.openapis.org/oas/latest.html#parameter-object
447    #[non_exhaustive]
448    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
449    #[cfg_attr(feature = "debug", derive(Debug))]
450    #[serde(rename_all = "camelCase")]
451    pub struct Parameter {
452        /// Name of the parameter.
453        ///
454        /// * For [`ParameterIn::Path`] this must in accordance to path templating.
455        /// * For [`ParameterIn::Query`] `Content-Type` or `Authorization` value will be ignored.
456        pub name: String,
457
458        /// Parameter location.
459        #[serde(rename = "in")]
460        pub parameter_in: ParameterIn,
461
462        /// Markdown supported description of the parameter.
463        #[serde(skip_serializing_if = "Option::is_none")]
464        pub description: Option<String>,
465
466        /// Declares whether the parameter is required or not for api.
467        ///
468        /// * For [`ParameterIn::Path`] this must and will be [`Required::True`].
469        pub required: Required,
470
471        /// Declares the parameter deprecated status.
472        #[serde(skip_serializing_if = "Option::is_none")]
473        pub deprecated: Option<Deprecated>,
474        // pub allow_empty_value: bool, this is going to be removed from further open api spec releases
475        /// Schema of the parameter. Typically [`Schema::Object`] is used.
476        #[serde(skip_serializing_if = "Option::is_none")]
477        pub schema: Option<RefOr<Schema>>,
478
479        /// Describes how [`Parameter`] is being serialized depending on [`Parameter::schema`] (type of a content).
480        /// Default value is based on [`ParameterIn`].
481        #[serde(skip_serializing_if = "Option::is_none")]
482        pub style: Option<ParameterStyle>,
483
484        /// When _`true`_ it will generate separate parameter value for each parameter with _`array`_ and _`object`_ type.
485        /// This is also _`true`_ by default for [`ParameterStyle::Form`].
486        ///
487        /// With explode _`false`_:
488        /// ```text
489        ///color=blue,black,brown
490        /// ```
491        ///
492        /// With explode _`true`_:
493        /// ```text
494        ///color=blue&color=black&color=brown
495        /// ```
496        #[serde(skip_serializing_if = "Option::is_none")]
497        pub explode: Option<bool>,
498
499        /// Defines whether parameter should allow reserved characters defined by
500        /// [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) _`:/?#[]@!$&'()*+,;=`_.
501        /// This is only applicable with [`ParameterIn::Query`]. Default value is _`false`_.
502        #[serde(skip_serializing_if = "Option::is_none")]
503        pub allow_reserved: Option<bool>,
504
505        /// Example of [`Parameter`]'s potential value. This examples will override example
506        /// within [`Parameter::schema`] if defined.
507        #[serde(skip_serializing_if = "Option::is_none")]
508        example: Option<Value>,
509    }
510}
511
512impl Parameter {
513    /// Constructs a new required [`Parameter`] with given name.
514    pub fn new<S: Into<String>>(name: S) -> Self {
515        Self {
516            name: name.into(),
517            required: Required::True,
518            ..Default::default()
519        }
520    }
521}
522
523impl ParameterBuilder {
524    /// Add name of the [`Parameter`].
525    pub fn name<I: Into<String>>(mut self, name: I) -> Self {
526        set_value!(self name name.into())
527    }
528
529    /// Add in of the [`Parameter`].
530    pub fn parameter_in(mut self, parameter_in: ParameterIn) -> Self {
531        set_value!(self parameter_in parameter_in)
532    }
533
534    /// Add required declaration of the [`Parameter`]. If [`ParameterIn::Path`] is
535    /// defined this is always [`Required::True`].
536    pub fn required(mut self, required: Required) -> Self {
537        self.required = required;
538        // required must be true, if parameter_in is Path
539        if self.parameter_in == ParameterIn::Path {
540            self.required = Required::True;
541        }
542
543        self
544    }
545
546    /// Add or change description of the [`Parameter`].
547    pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
548        set_value!(self description description.map(|description| description.into()))
549    }
550
551    /// Add or change [`Parameter`] deprecated declaration.
552    pub fn deprecated(mut self, deprecated: Option<Deprecated>) -> Self {
553        set_value!(self deprecated deprecated)
554    }
555
556    /// Add or change [`Parameter`]s schema.
557    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: Option<I>) -> Self {
558        set_value!(self schema component.map(|component| component.into()))
559    }
560
561    /// Add or change serialization style of [`Parameter`].
562    pub fn style(mut self, style: Option<ParameterStyle>) -> Self {
563        set_value!(self style style)
564    }
565
566    /// Define whether [`Parameter`]s are exploded or not.
567    pub fn explode(mut self, explode: Option<bool>) -> Self {
568        set_value!(self explode explode)
569    }
570
571    /// Add or change whether [`Parameter`] should allow reserved characters.
572    pub fn allow_reserved(mut self, allow_reserved: Option<bool>) -> Self {
573        set_value!(self allow_reserved allow_reserved)
574    }
575
576    /// Add or change example of [`Parameter`]'s potential value.
577    pub fn example(mut self, example: Option<Value>) -> Self {
578        set_value!(self example example)
579    }
580}
581
582/// In definition of [`Parameter`].
583#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
584#[serde(rename_all = "lowercase")]
585#[cfg_attr(feature = "debug", derive(Debug))]
586pub enum ParameterIn {
587    /// Declares that parameter is used as query parameter.
588    Query,
589    /// Declares that parameter is used as path parameter.
590    Path,
591    /// Declares that parameter is used as header value.
592    Header,
593    /// Declares that parameter is used as cookie value.
594    Cookie,
595}
596
597impl Default for ParameterIn {
598    fn default() -> Self {
599        Self::Path
600    }
601}
602
603/// Defines how [`Parameter`] should be serialized.
604#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
605#[cfg_attr(feature = "debug", derive(Debug))]
606#[serde(rename_all = "camelCase")]
607pub enum ParameterStyle {
608    /// Path style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.7)
609    /// e.g _`;color=blue`_.
610    /// Allowed with [`ParameterIn::Path`].
611    Matrix,
612    /// Label style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.5)
613    /// e.g _`.color=blue`_.
614    /// Allowed with [`ParameterIn::Path`].
615    Label,
616    /// Form style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8)
617    /// e.g. _`color=blue`_. Default value for [`ParameterIn::Query`] [`ParameterIn::Cookie`].
618    /// Allowed with [`ParameterIn::Query`] or [`ParameterIn::Cookie`].
619    Form,
620    /// Default value for [`ParameterIn::Path`] [`ParameterIn::Header`]. e.g. _`blue`_.
621    /// Allowed with [`ParameterIn::Path`] or [`ParameterIn::Header`].
622    Simple,
623    /// Space separated array values e.g. _`blue%20black%20brown`_.
624    /// Allowed with [`ParameterIn::Query`].
625    SpaceDelimited,
626    /// Pipe separated array values e.g. _`blue|black|brown`_.
627    /// Allowed with [`ParameterIn::Query`].
628    PipeDelimited,
629    /// Simple way of rendering nested objects using form parameters .e.g. _`color[B]=150`_.
630    /// Allowed with [`ParameterIn::Query`].
631    DeepObject,
632}
633
634#[cfg(test)]
635mod tests {
636    use super::{Operation, OperationBuilder};
637    use crate::openapi::{
638        security::SecurityRequirement, server::Server, PathItem, PathItemType, PathsBuilder,
639    };
640
641    #[test]
642    fn test_path_order() {
643        let paths_list = PathsBuilder::new()
644            .path(
645                "/todo",
646                PathItem::new(PathItemType::Get, OperationBuilder::new()),
647            )
648            .path(
649                "/todo",
650                PathItem::new(PathItemType::Post, OperationBuilder::new()),
651            )
652            .path(
653                "/todo/{id}",
654                PathItem::new(PathItemType::Delete, OperationBuilder::new()),
655            )
656            .path(
657                "/todo/{id}",
658                PathItem::new(PathItemType::Get, OperationBuilder::new()),
659            )
660            .path(
661                "/todo/{id}",
662                PathItem::new(PathItemType::Put, OperationBuilder::new()),
663            )
664            .path(
665                "/todo/search",
666                PathItem::new(PathItemType::Get, OperationBuilder::new()),
667            )
668            .build();
669
670        let actual_value = paths_list
671            .paths
672            .iter()
673            .flat_map(|(path, path_item)| {
674                path_item.operations.iter().fold(
675                    Vec::<(&str, &PathItemType)>::with_capacity(paths_list.paths.len()),
676                    |mut acc, (method, _)| {
677                        acc.push((path.as_str(), method));
678                        acc
679                    },
680                )
681            })
682            .collect::<Vec<_>>();
683
684        let get = PathItemType::Get;
685        let post = PathItemType::Post;
686        let put = PathItemType::Put;
687        let delete = PathItemType::Delete;
688
689        #[cfg(not(feature = "preserve_path_order"))]
690        {
691            let expected_value = vec![
692                ("/todo", &get),
693                ("/todo", &post),
694                ("/todo/search", &get),
695                ("/todo/{id}", &get),
696                ("/todo/{id}", &put),
697                ("/todo/{id}", &delete),
698            ];
699            assert_eq!(actual_value, expected_value);
700        }
701
702        #[cfg(feature = "preserve_path_order")]
703        {
704            let expected_value = vec![
705                ("/todo", &get),
706                ("/todo", &post),
707                ("/todo/{id}", &delete),
708                ("/todo/{id}", &get),
709                ("/todo/{id}", &put),
710                ("/todo/search", &get),
711            ];
712            assert_eq!(actual_value, expected_value);
713        }
714    }
715
716    #[test]
717    fn operation_new() {
718        let operation = Operation::new();
719
720        assert!(operation.tags.is_none());
721        assert!(operation.summary.is_none());
722        assert!(operation.description.is_none());
723        assert!(operation.operation_id.is_none());
724        assert!(operation.external_docs.is_none());
725        assert!(operation.parameters.is_none());
726        assert!(operation.request_body.is_none());
727        assert!(operation.responses.responses.is_empty());
728        assert!(operation.callbacks.is_none());
729        assert!(operation.deprecated.is_none());
730        assert!(operation.security.is_none());
731        assert!(operation.servers.is_none());
732    }
733
734    #[test]
735    fn operation_builder_security() {
736        let security_requirement1 =
737            SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
738        let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
739        let operation = OperationBuilder::new()
740            .security(security_requirement1)
741            .security(security_requirement2)
742            .build();
743
744        assert!(operation.security.is_some());
745    }
746
747    #[test]
748    fn operation_builder_server() {
749        let server1 = Server::new("/api");
750        let server2 = Server::new("/admin");
751        let operation = OperationBuilder::new()
752            .server(server1)
753            .server(server2)
754            .build();
755
756        assert!(operation.servers.is_some());
757    }
758}