utoipa/
openapi.rs

1//! Rust implementation of Openapi Spec V3.1.
2
3use serde::{
4    de::{Error, Expected, Visitor},
5    Deserialize, Deserializer, Serialize, Serializer,
6};
7use std::fmt::Formatter;
8
9use self::path::PathsMap;
10pub use self::{
11    content::{Content, ContentBuilder},
12    external_docs::ExternalDocs,
13    header::{Header, HeaderBuilder},
14    info::{Contact, ContactBuilder, Info, InfoBuilder, License, LicenseBuilder},
15    path::{HttpMethod, PathItem, Paths, PathsBuilder},
16    response::{Response, ResponseBuilder, Responses, ResponsesBuilder},
17    schema::{
18        AllOf, AllOfBuilder, Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator,
19        KnownFormat, Object, ObjectBuilder, OneOf, OneOfBuilder, Ref, Schema, SchemaFormat,
20        ToArray, Type,
21    },
22    security::SecurityRequirement,
23    server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder},
24    tag::Tag,
25};
26
27pub mod content;
28pub mod encoding;
29pub mod example;
30pub mod extensions;
31pub mod external_docs;
32pub mod header;
33pub mod info;
34pub mod link;
35pub mod path;
36pub mod request_body;
37pub mod response;
38pub mod schema;
39pub mod security;
40pub mod server;
41pub mod tag;
42pub mod xml;
43
44builder! {
45    /// # Examples
46    ///
47    /// Create [`OpenApi`] using [`OpenApiBuilder`].
48    /// ```rust
49    /// # use utoipa::openapi::{Info, Paths, Components, OpenApiBuilder};
50    /// let openapi = OpenApiBuilder::new()
51    ///      .info(Info::new("My api", "1.0.0"))
52    ///      .paths(Paths::new())
53    ///      .components(Some(
54    ///          Components::new()
55    ///      ))
56    ///      .build();
57    /// ```
58    OpenApiBuilder;
59
60    /// Root object of the OpenAPI document.
61    ///
62    /// You can use [`OpenApi::new`] function to construct a new [`OpenApi`] instance and then
63    /// use the fields with mutable access to modify them. This is quite tedious if you are not simply
64    /// just changing one thing thus you can also use the [`OpenApiBuilder::new`] to use builder to
65    /// construct a new [`OpenApi`] object.
66    ///
67    /// See more details at <https://spec.openapis.org/oas/latest.html#openapi-object>.
68    #[non_exhaustive]
69    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
70    #[cfg_attr(feature = "debug", derive(Debug))]
71    #[serde(rename_all = "camelCase")]
72    pub struct OpenApi {
73        /// OpenAPI document version.
74        pub openapi: OpenApiVersion,
75
76        /// Provides metadata about the API.
77        ///
78        /// See more details at <https://spec.openapis.org/oas/latest.html#info-object>.
79        pub info: Info,
80
81        /// Optional list of servers that provides the connectivity information to target servers.
82        ///
83        /// This is implicitly one server with `url` set to `/`.
84        ///
85        /// See more details at <https://spec.openapis.org/oas/latest.html#server-object>.
86        #[serde(skip_serializing_if = "Option::is_none")]
87        pub servers: Option<Vec<Server>>,
88
89        /// Available paths and operations for the API.
90        ///
91        /// See more details at <https://spec.openapis.org/oas/latest.html#paths-object>.
92        pub paths: Paths,
93
94        /// Holds various reusable schemas for the OpenAPI document.
95        ///
96        /// Few of these elements are security schemas and object schemas.
97        ///
98        /// See more details at <https://spec.openapis.org/oas/latest.html#components-object>.
99        #[serde(skip_serializing_if = "Option::is_none")]
100        pub components: Option<Components>,
101
102        /// Declaration of global security mechanisms that can be used across the API. The individual operations
103        /// can override the declarations. You can use `SecurityRequirement::default()` if you wish to make security
104        /// optional by adding it to the list of securities.
105        ///
106        /// See more details at <https://spec.openapis.org/oas/latest.html#security-requirement-object>.
107        #[serde(skip_serializing_if = "Option::is_none")]
108        pub security: Option<Vec<SecurityRequirement>>,
109
110        /// Optional list of tags can be used to add additional documentation to matching tags of operations.
111        ///
112        /// See more details at <https://spec.openapis.org/oas/latest.html#tag-object>.
113        #[serde(skip_serializing_if = "Option::is_none")]
114        pub tags: Option<Vec<Tag>>,
115
116        /// Optional global additional documentation reference.
117        ///
118        /// See more details at <https://spec.openapis.org/oas/latest.html#external-documentation-object>.
119        #[serde(skip_serializing_if = "Option::is_none")]
120        pub external_docs: Option<ExternalDocs>,
121
122        /// Schema keyword can be used to override default _`$schema`_ dialect which is by default
123        /// “<https://spec.openapis.org/oas/3.1/dialect/base>”.
124        ///
125        /// All the references and individual files could use their own schema dialect.
126        #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
127        pub schema: String,
128
129        /// Optional extensions "x-something".
130        #[serde(skip_serializing_if = "Option::is_none", flatten)]
131        pub extensions: Option<Extensions>,
132    }
133}
134
135impl OpenApi {
136    /// Construct a new [`OpenApi`] object.
137    ///
138    /// Function accepts two arguments one which is [`Info`] metadata of the API; two which is [`Paths`]
139    /// containing operations for the API.
140    ///
141    /// # Examples
142    ///
143    /// ```rust
144    /// # use utoipa::openapi::{Info, Paths, OpenApi};
145    /// #
146    /// let openapi = OpenApi::new(Info::new("pet api", "0.1.0"), Paths::new());
147    /// ```
148    pub fn new<P: Into<Paths>>(info: Info, paths: P) -> Self {
149        Self {
150            info,
151            paths: paths.into(),
152            ..Default::default()
153        }
154    }
155
156    /// Converts this [`OpenApi`] to JSON String. This method essentially calls [`serde_json::to_string`] method.
157    pub fn to_json(&self) -> Result<String, serde_json::Error> {
158        serde_json::to_string(self)
159    }
160
161    /// Converts this [`OpenApi`] to pretty JSON String. This method essentially calls [`serde_json::to_string_pretty`] method.
162    pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
163        serde_json::to_string_pretty(self)
164    }
165
166    /// Converts this [`OpenApi`] to YAML String. This method essentially calls [`serde_norway::to_string`] method.
167    #[cfg(feature = "yaml")]
168    #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))]
169    pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
170        serde_norway::to_string(self)
171    }
172
173    /// Merge `other` [`OpenApi`] moving `self` and returning combined [`OpenApi`].
174    ///
175    /// In functionality wise this is exactly same as calling [`OpenApi::merge`] but but provides
176    /// leaner API for chaining method calls.
177    pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
178        self.merge(other);
179        self
180    }
181
182    /// Merge `other` [`OpenApi`] consuming it and resuming it's content.
183    ///
184    /// Merge function will take all `self` nonexistent _`servers`, `paths`, `schemas`, `responses`,
185    /// `security_schemes`, `security_requirements` and `tags`_ from _`other`_ [`OpenApi`].
186    ///
187    /// This function performs a shallow comparison for `paths`, `schemas`, `responses` and
188    /// `security schemes` which means that only _`name`_ and _`path`_ is used for comparison. When
189    /// match occurs the whole item will be ignored from merged results. Only items not
190    /// found will be appended to `self`.
191    ///
192    /// For _`servers`_, _`tags`_ and _`security_requirements`_ the whole item will be used for
193    /// comparison. Items not found from `self` will be appended to `self`.
194    ///
195    /// **Note!** `info`, `openapi`, `external_docs` and `schema` will not be merged.
196    pub fn merge(&mut self, mut other: OpenApi) {
197        if let Some(other_servers) = &mut other.servers {
198            let servers = self.servers.get_or_insert(Vec::new());
199            other_servers.retain(|server| !servers.contains(server));
200            servers.append(other_servers);
201        }
202
203        if !other.paths.paths.is_empty() {
204            self.paths.merge(other.paths);
205        };
206
207        if let Some(other_components) = &mut other.components {
208            let components = self.components.get_or_insert(Components::default());
209
210            other_components
211                .schemas
212                .retain(|name, _| !components.schemas.contains_key(name));
213            components.schemas.append(&mut other_components.schemas);
214
215            other_components
216                .responses
217                .retain(|name, _| !components.responses.contains_key(name));
218            components.responses.append(&mut other_components.responses);
219
220            other_components
221                .security_schemes
222                .retain(|name, _| !components.security_schemes.contains_key(name));
223            components
224                .security_schemes
225                .append(&mut other_components.security_schemes);
226        }
227
228        if let Some(other_security) = &mut other.security {
229            let security = self.security.get_or_insert(Vec::new());
230            other_security.retain(|requirement| !security.contains(requirement));
231            security.append(other_security);
232        }
233
234        if let Some(other_tags) = &mut other.tags {
235            let tags = self.tags.get_or_insert(Vec::new());
236            other_tags.retain(|tag| !tags.contains(tag));
237            tags.append(other_tags);
238        }
239    }
240
241    /// Nest `other` [`OpenApi`] to this [`OpenApi`].
242    ///
243    /// Nesting performs custom [`OpenApi::merge`] where `other` [`OpenApi`] paths are prepended with given
244    /// `path` and then appended to _`paths`_ of this [`OpenApi`] instance. Rest of the  `other`
245    /// [`OpenApi`] instance is merged to this [`OpenApi`] with [`OpenApi::merge_from`] method.
246    ///
247    /// **If multiple** APIs are being nested with same `path` only the **last** one will be retained.
248    ///
249    /// Method accepts two arguments, first is the path to prepend .e.g. _`/user`_. Second argument
250    /// is the [`OpenApi`] to prepend paths for.
251    ///
252    /// # Examples
253    ///
254    /// _**Merge `user_api` to `api` nesting `user_api` paths under `/api/v1/user`**_
255    /// ```rust
256    ///  # use utoipa::openapi::{OpenApi, OpenApiBuilder};
257    ///  # use utoipa::openapi::path::{PathsBuilder, PathItemBuilder, PathItem,
258    ///  # HttpMethod, OperationBuilder};
259    ///  let api = OpenApiBuilder::new()
260    ///      .paths(
261    ///          PathsBuilder::new().path(
262    ///              "/api/v1/status",
263    ///              PathItem::new(
264    ///                  HttpMethod::Get,
265    ///                  OperationBuilder::new()
266    ///                      .description(Some("Get status"))
267    ///                      .build(),
268    ///              ),
269    ///          ),
270    ///      )
271    ///      .build();
272    ///  let user_api = OpenApiBuilder::new()
273    ///     .paths(
274    ///         PathsBuilder::new().path(
275    ///             "/",
276    ///             PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
277    ///         )
278    ///     )
279    ///     .build();
280    ///  let nested = api.nest("/api/v1/user", user_api);
281    /// ```
282    pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
283        self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
284    }
285
286    /// Nest `other` [`OpenApi`] with custom path composer.
287    ///
288    /// In most cases you should use [`OpenApi::nest`] instead.
289    /// Only use this method if you need custom path composition for a specific use case.
290    ///
291    /// `composer` is a function that takes two strings, the base path and the path to nest, and returns the composed path for the API Specification.
292    pub fn nest_with_path_composer<
293        P: Into<String>,
294        O: Into<OpenApi>,
295        F: Fn(&str, &str) -> String,
296    >(
297        mut self,
298        path: P,
299        other: O,
300        composer: F,
301    ) -> Self {
302        let path: String = path.into();
303        let mut other_api: OpenApi = other.into();
304
305        let nested_paths = other_api
306            .paths
307            .paths
308            .into_iter()
309            .map(|(item_path, item)| {
310                let path = composer(&path, &item_path);
311                (path, item)
312            })
313            .collect::<PathsMap<_, _>>();
314
315        self.paths.paths.extend(nested_paths);
316
317        // paths are already merged, thus we can ignore them
318        other_api.paths.paths = PathsMap::new();
319        self.merge_from(other_api)
320    }
321}
322
323impl OpenApiBuilder {
324    /// Add [`Info`] metadata of the API.
325    pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
326        set_value!(self info info.into())
327    }
328
329    /// Add iterator of [`Server`]s to configure target servers.
330    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
331        set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
332    }
333
334    /// Add [`Paths`] to configure operations and endpoints of the API.
335    pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
336        set_value!(self paths paths.into())
337    }
338
339    /// Add [`Components`] to configure reusable schemas.
340    pub fn components(mut self, components: Option<Components>) -> Self {
341        set_value!(self components components)
342    }
343
344    /// Add iterator of [`SecurityRequirement`]s that are globally available for all operations.
345    pub fn security<I: IntoIterator<Item = SecurityRequirement>>(
346        mut self,
347        security: Option<I>,
348    ) -> Self {
349        set_value!(self security security.map(|security| security.into_iter().collect()))
350    }
351
352    /// Add iterator of [`Tag`]s to add additional documentation for **operations** tags.
353    pub fn tags<I: IntoIterator<Item = Tag>>(mut self, tags: Option<I>) -> Self {
354        set_value!(self tags tags.map(|tags| tags.into_iter().collect()))
355    }
356
357    /// Add [`ExternalDocs`] for referring additional documentation.
358    pub fn external_docs(mut self, external_docs: Option<ExternalDocs>) -> Self {
359        set_value!(self external_docs external_docs)
360    }
361
362    /// Override default `$schema` dialect for the Open API doc.
363    ///
364    /// # Examples
365    ///
366    /// _**Override default schema dialect.**_
367    /// ```rust
368    /// # use utoipa::openapi::OpenApiBuilder;
369    /// let _ = OpenApiBuilder::new()
370    ///     .schema("http://json-schema.org/draft-07/schema#")
371    ///     .build();
372    /// ```
373    pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
374        set_value!(self schema schema.into())
375    }
376}
377
378/// Represents available [OpenAPI versions][version].
379///
380/// [version]: <https://spec.openapis.org/oas/latest.html#versions>
381#[derive(Serialize, Clone, PartialEq, Eq, Default)]
382#[cfg_attr(feature = "debug", derive(Debug))]
383pub enum OpenApiVersion {
384    /// Will serialize to `3.1.0` the latest released OpenAPI version.
385    #[serde(rename = "3.1.0")]
386    #[default]
387    Version31,
388}
389
390impl<'de> Deserialize<'de> for OpenApiVersion {
391    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
392    where
393        D: Deserializer<'de>,
394    {
395        struct VersionVisitor;
396
397        impl<'v> Visitor<'v> for VersionVisitor {
398            type Value = OpenApiVersion;
399
400            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
401                formatter.write_str("a version string in 3.1.x format")
402            }
403
404            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
405            where
406                E: Error,
407            {
408                self.visit_string(v.to_string())
409            }
410
411            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
412            where
413                E: Error,
414            {
415                let version = v
416                    .split('.')
417                    .flat_map(|digit| digit.parse::<i8>())
418                    .collect::<Vec<_>>();
419
420                if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
421                    Ok(OpenApiVersion::Version31)
422                } else {
423                    let expected: &dyn Expected = &"3.1.0";
424                    Err(Error::invalid_value(
425                        serde::de::Unexpected::Str(&v),
426                        expected,
427                    ))
428                }
429            }
430        }
431
432        deserializer.deserialize_string(VersionVisitor)
433    }
434}
435
436/// Value used to indicate whether reusable schema, parameter or operation is deprecated.
437///
438/// The value will serialize to boolean.
439#[derive(PartialEq, Eq, Clone, Default)]
440#[cfg_attr(feature = "debug", derive(Debug))]
441#[allow(missing_docs)]
442pub enum Deprecated {
443    True,
444    #[default]
445    False,
446}
447
448impl Serialize for Deprecated {
449    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
450    where
451        S: Serializer,
452    {
453        serializer.serialize_bool(matches!(self, Self::True))
454    }
455}
456
457impl<'de> Deserialize<'de> for Deprecated {
458    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
459    where
460        D: serde::Deserializer<'de>,
461    {
462        struct BoolVisitor;
463        impl<'de> Visitor<'de> for BoolVisitor {
464            type Value = Deprecated;
465
466            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
467                formatter.write_str("a bool true or false")
468            }
469
470            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
471            where
472                E: serde::de::Error,
473            {
474                match v {
475                    true => Ok(Deprecated::True),
476                    false => Ok(Deprecated::False),
477                }
478            }
479        }
480        deserializer.deserialize_bool(BoolVisitor)
481    }
482}
483
484/// Value used to indicate whether parameter or property is required.
485///
486/// The value will serialize to boolean.
487#[derive(PartialEq, Eq, Clone, Default)]
488#[allow(missing_docs)]
489#[cfg_attr(feature = "debug", derive(Debug))]
490pub enum Required {
491    True,
492    #[default]
493    False,
494}
495
496impl Serialize for Required {
497    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
498    where
499        S: Serializer,
500    {
501        serializer.serialize_bool(matches!(self, Self::True))
502    }
503}
504
505impl<'de> Deserialize<'de> for Required {
506    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
507    where
508        D: serde::Deserializer<'de>,
509    {
510        struct BoolVisitor;
511        impl<'de> Visitor<'de> for BoolVisitor {
512            type Value = Required;
513
514            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
515                formatter.write_str("a bool true or false")
516            }
517
518            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
519            where
520                E: serde::de::Error,
521            {
522                match v {
523                    true => Ok(Required::True),
524                    false => Ok(Required::False),
525                }
526            }
527        }
528        deserializer.deserialize_bool(BoolVisitor)
529    }
530}
531
532/// A [`Ref`] or some other type `T`.
533///
534/// Typically used in combination with [`Components`] and is an union type between [`Ref`] and any
535/// other given type such as [`Schema`] or [`Response`].
536#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
537#[cfg_attr(feature = "debug", derive(Debug))]
538#[serde(untagged)]
539pub enum RefOr<T> {
540    /// Represents [`Ref`] reference to another OpenAPI object instance. e.g.
541    /// `$ref: #/components/schemas/Hello`
542    Ref(Ref),
543    /// Represents any value that can be added to the [`struct@Components`] e.g. [`enum@Schema`]
544    /// or [`struct@Response`].
545    T(T),
546}
547
548macro_rules! build_fn {
549    ( $vis:vis $name:ident $( $field:ident ),+ ) => {
550        #[doc = concat!("Constructs a new [`", stringify!($name),"`] taking all fields values from this object.")]
551        $vis fn build(self) -> $name {
552            $name {
553                $(
554                    $field: self.$field,
555                )*
556            }
557        }
558    };
559}
560pub(crate) use build_fn;
561
562macro_rules! set_value {
563    ( $self:ident $field:ident $value:expr ) => {{
564        $self.$field = $value;
565
566        $self
567    }};
568}
569pub(crate) use set_value;
570
571macro_rules! new {
572    ( $vis:vis $name:ident ) => {
573        #[doc = concat!("Constructs a new [`", stringify!($name),"`].")]
574        $vis fn new() -> $name {
575            $name {
576                ..Default::default()
577            }
578        }
579    };
580}
581pub(crate) use new;
582
583macro_rules! from {
584    ( $name:ident $to:ident $( $field:ident ),+ ) => {
585        impl From<$name> for $to {
586            fn from(value: $name) -> Self {
587                Self {
588                    $( $field: value.$field, )*
589                }
590            }
591        }
592
593        impl From<$to> for $name {
594            fn from(value: $to) -> Self {
595                value.build()
596            }
597        }
598    };
599}
600pub(crate) use from;
601
602macro_rules! builder {
603    ( $( #[$builder_meta:meta] )* $builder_name:ident; $(#[$meta:meta])* $vis:vis $key:ident $name:ident $( $tt:tt )* ) => {
604        builder!( @type_impl $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
605        builder!( @builder_impl $( #[$builder_meta] )* $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
606    };
607
608    ( @type_impl $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
609        { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
610    ) => {
611        $( #[$meta] )*
612        $vis $key $name {
613            $( $( #[$field_meta] )* $field_vis $field: $field_ty, )*
614        }
615
616        impl $name {
617            #[doc = concat!("Construct a new ", stringify!($builder_name), ".")]
618            #[doc = ""]
619            #[doc = concat!("This is effectively same as calling [`", stringify!($builder_name), "::new`]")]
620            $vis fn builder() -> $builder_name {
621                $builder_name::new()
622            }
623        }
624    };
625
626    ( @builder_impl $( #[$builder_meta:meta] )* $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
627        { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
628    ) => {
629        #[doc = concat!("Builder for [`", stringify!($name),
630            "`] with chainable configuration methods to create a new [`", stringify!($name) , "`].")]
631        $( #[$builder_meta] )*
632        #[cfg_attr(feature = "debug", derive(Debug))]
633        $vis $key $builder_name {
634            $( $field: $field_ty, )*
635        }
636
637        impl Default for $builder_name {
638            fn default() -> Self {
639                let meta_default: $name = $name::default();
640                Self {
641                    $( $field: meta_default.$field, )*
642                }
643            }
644        }
645
646        impl $builder_name {
647            crate::openapi::new!($vis $builder_name);
648            crate::openapi::build_fn!($vis $name $( $field ),* );
649        }
650
651        crate::openapi::from!($name $builder_name $( $field ),* );
652    };
653}
654use crate::openapi::extensions::Extensions;
655pub(crate) use builder;
656
657#[cfg(test)]
658mod tests {
659    use crate::openapi::{
660        info::InfoBuilder,
661        path::{OperationBuilder, PathsBuilder},
662    };
663    use insta::assert_json_snapshot;
664
665    use super::{response::Response, *};
666
667    #[test]
668    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
669        assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
670        Ok(())
671    }
672
673    #[test]
674    fn serialize_openapi_json_minimal_success() {
675        let openapi = OpenApi::new(
676            InfoBuilder::new()
677                .title("My api")
678                .version("1.0.0")
679                .description(Some("My api description"))
680                .license(Some(
681                    LicenseBuilder::new()
682                        .name("MIT")
683                        .url(Some("http://mit.licence"))
684                        .build(),
685                ))
686                .build(),
687            Paths::new(),
688        );
689
690        assert_json_snapshot!(openapi);
691    }
692
693    #[test]
694    fn serialize_openapi_json_with_paths_success() {
695        let openapi = OpenApi::new(
696            Info::new("My big api", "1.1.0"),
697            PathsBuilder::new()
698                .path(
699                    "/api/v1/users",
700                    PathItem::new(
701                        HttpMethod::Get,
702                        OperationBuilder::new().response("200", Response::new("Get users list")),
703                    ),
704                )
705                .path(
706                    "/api/v1/users",
707                    PathItem::new(
708                        HttpMethod::Post,
709                        OperationBuilder::new().response("200", Response::new("Post new user")),
710                    ),
711                )
712                .path(
713                    "/api/v1/users/{id}",
714                    PathItem::new(
715                        HttpMethod::Get,
716                        OperationBuilder::new().response("200", Response::new("Get user by id")),
717                    ),
718                ),
719        );
720
721        assert_json_snapshot!(openapi);
722    }
723
724    #[test]
725    fn merge_2_openapi_documents() {
726        let mut api_1 = OpenApi::new(
727            Info::new("Api", "v1"),
728            PathsBuilder::new()
729                .path(
730                    "/api/v1/user",
731                    PathItem::new(
732                        HttpMethod::Get,
733                        OperationBuilder::new().response("200", Response::new("Get user success")),
734                    ),
735                )
736                .build(),
737        );
738
739        let api_2 = OpenApiBuilder::new()
740            .info(Info::new("Api", "v2"))
741            .paths(
742                PathsBuilder::new()
743                    .path(
744                        "/api/v1/user",
745                        PathItem::new(
746                            HttpMethod::Get,
747                            OperationBuilder::new()
748                                .response("200", Response::new("This will not get added")),
749                        ),
750                    )
751                    .path(
752                        "/ap/v2/user",
753                        PathItem::new(
754                            HttpMethod::Get,
755                            OperationBuilder::new()
756                                .response("200", Response::new("Get user success 2")),
757                        ),
758                    )
759                    .path(
760                        "/api/v2/user",
761                        PathItem::new(
762                            HttpMethod::Post,
763                            OperationBuilder::new()
764                                .response("200", Response::new("Get user success")),
765                        ),
766                    )
767                    .build(),
768            )
769            .components(Some(
770                ComponentsBuilder::new()
771                    .schema(
772                        "User2",
773                        ObjectBuilder::new().schema_type(Type::Object).property(
774                            "name",
775                            ObjectBuilder::new().schema_type(Type::String).build(),
776                        ),
777                    )
778                    .build(),
779            ))
780            .build();
781
782        api_1.merge(api_2);
783
784        assert_json_snapshot!(api_1, {
785            ".paths" => insta::sorted_redaction()
786        });
787    }
788
789    #[test]
790    fn merge_same_path_diff_methods() {
791        let mut api_1 = OpenApi::new(
792            Info::new("Api", "v1"),
793            PathsBuilder::new()
794                .path(
795                    "/api/v1/user",
796                    PathItem::new(
797                        HttpMethod::Get,
798                        OperationBuilder::new()
799                            .response("200", Response::new("Get user success 1")),
800                    ),
801                )
802                .extensions(Some(Extensions::from_iter([("x-v1-api", true)])))
803                .build(),
804        );
805
806        let api_2 = OpenApiBuilder::new()
807            .info(Info::new("Api", "v2"))
808            .paths(
809                PathsBuilder::new()
810                    .path(
811                        "/api/v1/user",
812                        PathItem::new(
813                            HttpMethod::Get,
814                            OperationBuilder::new()
815                                .response("200", Response::new("This will not get added")),
816                        ),
817                    )
818                    .path(
819                        "/api/v1/user",
820                        PathItem::new(
821                            HttpMethod::Post,
822                            OperationBuilder::new()
823                                .response("200", Response::new("Post user success 1")),
824                        ),
825                    )
826                    .path(
827                        "/api/v2/user",
828                        PathItem::new(
829                            HttpMethod::Get,
830                            OperationBuilder::new()
831                                .response("200", Response::new("Get user success 2")),
832                        ),
833                    )
834                    .path(
835                        "/api/v2/user",
836                        PathItem::new(
837                            HttpMethod::Post,
838                            OperationBuilder::new()
839                                .response("200", Response::new("Post user success 2")),
840                        ),
841                    )
842                    .extensions(Some(Extensions::from_iter([("x-random", "Value")])))
843                    .build(),
844            )
845            .components(Some(
846                ComponentsBuilder::new()
847                    .schema(
848                        "User2",
849                        ObjectBuilder::new().schema_type(Type::Object).property(
850                            "name",
851                            ObjectBuilder::new().schema_type(Type::String).build(),
852                        ),
853                    )
854                    .build(),
855            ))
856            .build();
857
858        api_1.merge(api_2);
859
860        assert_json_snapshot!(api_1, {
861            ".paths" => insta::sorted_redaction()
862        });
863    }
864
865    #[test]
866    fn test_nest_open_apis() {
867        let api = OpenApiBuilder::new()
868            .paths(
869                PathsBuilder::new().path(
870                    "/api/v1/status",
871                    PathItem::new(
872                        HttpMethod::Get,
873                        OperationBuilder::new()
874                            .description(Some("Get status"))
875                            .build(),
876                    ),
877                ),
878            )
879            .build();
880
881        let user_api = OpenApiBuilder::new()
882            .paths(
883                PathsBuilder::new()
884                    .path(
885                        "/",
886                        PathItem::new(
887                            HttpMethod::Get,
888                            OperationBuilder::new()
889                                .description(Some("Get user details"))
890                                .build(),
891                        ),
892                    )
893                    .path(
894                        "/foo",
895                        PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
896                    ),
897            )
898            .build();
899
900        let nest_merged = api.nest("/api/v1/user", user_api);
901        let value = serde_json::to_value(nest_merged).expect("should serialize as json");
902        let paths = value
903            .pointer("/paths")
904            .expect("paths should exits in openapi");
905
906        assert_json_snapshot!(paths);
907    }
908
909    #[test]
910    fn openapi_custom_extension() {
911        let mut api = OpenApiBuilder::new().build();
912        let extensions = api.extensions.get_or_insert(Default::default());
913        extensions.insert(
914            String::from("x-tagGroup"),
915            String::from("anything that serializes to Json").into(),
916        );
917
918        assert_json_snapshot!(api);
919    }
920}