Skip to main content

utoipa/
openapi.rs

1//! Rust implementation of Openapi Spec V3.2.
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        /// Incoming requests that may be initiated by the API provider independently of an
95        /// incoming API request. For example, a subscription confirmation request sent to a
96        /// consumer-provided webhook URL after they register for notifications.
97        ///
98        /// Each key is a unique identifier for the webhook, e.g. `newPet`, and the value is a
99        /// [`PathItem`][path::PathItem] describing the request that the API provider will send.
100        ///
101        /// See more details at <https://spec.openapis.org/oas/latest.html#oas-webhooks>.
102        #[serde(skip_serializing_if = "Option::is_none")]
103        pub webhooks: Option<Paths>,
104
105        /// Holds various reusable schemas for the OpenAPI document.
106        ///
107        /// Few of these elements are security schemas and object schemas.
108        ///
109        /// See more details at <https://spec.openapis.org/oas/latest.html#components-object>.
110        #[serde(skip_serializing_if = "Option::is_none")]
111        pub components: Option<Components>,
112
113        /// Declaration of global security mechanisms that can be used across the API. The individual operations
114        /// can override the declarations. You can use `SecurityRequirement::default()` if you wish to make security
115        /// optional by adding it to the list of securities.
116        ///
117        /// See more details at <https://spec.openapis.org/oas/latest.html#security-requirement-object>.
118        #[serde(skip_serializing_if = "Option::is_none")]
119        pub security: Option<Vec<SecurityRequirement>>,
120
121        /// Optional list of tags can be used to add additional documentation to matching tags of operations.
122        ///
123        /// See more details at <https://spec.openapis.org/oas/latest.html#tag-object>.
124        #[serde(skip_serializing_if = "Option::is_none")]
125        pub tags: Option<Vec<Tag>>,
126
127        /// Optional global additional documentation reference.
128        ///
129        /// See more details at <https://spec.openapis.org/oas/latest.html#external-documentation-object>.
130        #[serde(skip_serializing_if = "Option::is_none")]
131        pub external_docs: Option<ExternalDocs>,
132
133        /// The default JSON Schema dialect used by Schema Objects contained within this OpenAPI
134        /// document. Individual [`Schema`]s may still override this by declaring their own
135        /// `$schema` keyword.
136        ///
137        /// See more details at <https://spec.openapis.org/oas/latest.html#schema-object>.
138        #[serde(skip_serializing_if = "Option::is_none")]
139        pub json_schema_dialect: Option<String>,
140
141        /// Schema keyword can be used to override default _`$schema`_ dialect which is by default
142        /// “<https://spec.openapis.org/oas/3.1/dialect/base>”.
143        ///
144        /// All the references and individual files could use their own schema dialect.
145        #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
146        pub schema: String,
147
148        /// URI identifying this OpenAPI document, used as the base URI for resolving relative
149        /// references (e.g. `$ref` values) within the document.
150        ///
151        /// See more details at <https://spec.openapis.org/oas/latest.html#fixed-fields>.
152        #[serde(rename = "$self", skip_serializing_if = "Option::is_none")]
153        pub self_uri: Option<String>,
154
155        /// Optional extensions "x-something".
156        #[serde(skip_serializing_if = "Option::is_none", flatten)]
157        pub extensions: Option<Extensions>,
158    }
159}
160
161impl OpenApi {
162    /// Construct a new [`OpenApi`] object.
163    ///
164    /// Function accepts two arguments one which is [`Info`] metadata of the API; two which is [`Paths`]
165    /// containing operations for the API.
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// # use utoipa::openapi::{Info, Paths, OpenApi};
171    /// #
172    /// let openapi = OpenApi::new(Info::new("pet api", "0.1.0"), Paths::new());
173    /// ```
174    pub fn new<P: Into<Paths>>(info: Info, paths: P) -> Self {
175        Self {
176            info,
177            paths: paths.into(),
178            ..Default::default()
179        }
180    }
181
182    /// Converts this [`OpenApi`] to JSON String. This method essentially calls [`serde_json::to_string`] method.
183    pub fn to_json(&self) -> Result<String, serde_json::Error> {
184        serde_json::to_string(self)
185    }
186
187    /// Converts this [`OpenApi`] to pretty JSON String. This method essentially calls [`serde_json::to_string_pretty`] method.
188    pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
189        serde_json::to_string_pretty(self)
190    }
191
192    /// Converts this [`OpenApi`] to YAML String. This method essentially calls [`yaml_serde::to_string`].
193    #[cfg(feature = "yaml")]
194    #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))]
195    pub fn to_yaml(&self) -> Result<String, yaml_serde::Error> {
196        yaml_serde::to_string(self)
197    }
198
199    /// Merge `other` [`OpenApi`] moving `self` and returning combined [`OpenApi`].
200    ///
201    /// In functionality wise this is exactly same as calling [`OpenApi::merge`] but but provides
202    /// leaner API for chaining method calls.
203    pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
204        self.merge(other);
205        self
206    }
207
208    /// Merge `other` [`OpenApi`] consuming it and resuming it's content.
209    ///
210    /// Merge function will take all `self` nonexistent _`servers`, `paths`, `schemas`, `responses`,
211    /// `security_schemes`, `security_requirements` and `tags`_ from _`other`_ [`OpenApi`].
212    ///
213    /// This function performs a shallow comparison for `paths`, `schemas`, `responses` and
214    /// `security schemes` which means that only _`name`_ and _`path`_ is used for comparison. When
215    /// match occurs the whole item will be ignored from merged results. Only items not
216    /// found will be appended to `self`.
217    ///
218    /// For _`servers`_, _`tags`_ and _`security_requirements`_ the whole item will be used for
219    /// comparison. Items not found from `self` will be appended to `self`.
220    ///
221    /// **Note!** `info`, `openapi`, `external_docs` and `schema` will not be merged.
222    pub fn merge(&mut self, mut other: OpenApi) {
223        if let Some(other_servers) = &mut other.servers {
224            let servers = self.servers.get_or_insert(Vec::new());
225            other_servers.retain(|server| !servers.contains(server));
226            servers.append(other_servers);
227        }
228
229        if !other.paths.paths.is_empty() {
230            self.paths.merge(other.paths);
231        };
232
233        if let Some(other_components) = &mut other.components {
234            let components = self.components.get_or_insert(Components::default());
235
236            other_components
237                .schemas
238                .retain(|name, _| !components.schemas.contains_key(name));
239            components.schemas.append(&mut other_components.schemas);
240
241            other_components
242                .responses
243                .retain(|name, _| !components.responses.contains_key(name));
244            components.responses.append(&mut other_components.responses);
245
246            other_components
247                .security_schemes
248                .retain(|name, _| !components.security_schemes.contains_key(name));
249            components
250                .security_schemes
251                .append(&mut other_components.security_schemes);
252        }
253
254        if let Some(other_security) = &mut other.security {
255            let security = self.security.get_or_insert(Vec::new());
256            other_security.retain(|requirement| !security.contains(requirement));
257            security.append(other_security);
258        }
259
260        if let Some(other_tags) = &mut other.tags {
261            let tags = self.tags.get_or_insert(Vec::new());
262            other_tags.retain(|tag| !tags.contains(tag));
263            tags.append(other_tags);
264        }
265    }
266
267    /// Nest `other` [`OpenApi`] to this [`OpenApi`].
268    ///
269    /// Nesting performs custom [`OpenApi::merge`] where `other` [`OpenApi`] paths are prepended with given
270    /// `path` and then appended to _`paths`_ of this [`OpenApi`] instance. Rest of the  `other`
271    /// [`OpenApi`] instance is merged to this [`OpenApi`] with [`OpenApi::merge_from`] method.
272    ///
273    /// **If multiple** APIs are being nested with same `path` only the **last** one will be retained.
274    ///
275    /// Method accepts two arguments, first is the path to prepend .e.g. _`/user`_. Second argument
276    /// is the [`OpenApi`] to prepend paths for.
277    ///
278    /// # Examples
279    ///
280    /// _**Merge `user_api` to `api` nesting `user_api` paths under `/api/v1/user`**_
281    /// ```rust
282    ///  # use utoipa::openapi::{OpenApi, OpenApiBuilder};
283    ///  # use utoipa::openapi::path::{PathsBuilder, PathItemBuilder, PathItem,
284    ///  # HttpMethod, OperationBuilder};
285    ///  let api = OpenApiBuilder::new()
286    ///      .paths(
287    ///          PathsBuilder::new().path(
288    ///              "/api/v1/status",
289    ///              PathItem::new(
290    ///                  HttpMethod::Get,
291    ///                  OperationBuilder::new()
292    ///                      .description(Some("Get status"))
293    ///                      .build(),
294    ///              ),
295    ///          ),
296    ///      )
297    ///      .build();
298    ///  let user_api = OpenApiBuilder::new()
299    ///     .paths(
300    ///         PathsBuilder::new().path(
301    ///             "/",
302    ///             PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
303    ///         )
304    ///     )
305    ///     .build();
306    ///  let nested = api.nest("/api/v1/user", user_api);
307    /// ```
308    pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
309        self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
310    }
311
312    /// Nest `other` [`OpenApi`] with custom path composer.
313    ///
314    /// In most cases you should use [`OpenApi::nest`] instead.
315    /// Only use this method if you need custom path composition for a specific use case.
316    ///
317    /// `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.
318    pub fn nest_with_path_composer<
319        P: Into<String>,
320        O: Into<OpenApi>,
321        F: Fn(&str, &str) -> String,
322    >(
323        mut self,
324        path: P,
325        other: O,
326        composer: F,
327    ) -> Self {
328        let path: String = path.into();
329        let mut other_api: OpenApi = other.into();
330
331        let nested_paths = other_api
332            .paths
333            .paths
334            .into_iter()
335            .map(|(item_path, item)| {
336                let path = composer(&path, &item_path);
337                (path, item)
338            })
339            .collect::<PathsMap<_, _>>();
340
341        self.paths.paths.extend(nested_paths);
342
343        // paths are already merged, thus we can ignore them
344        other_api.paths.paths = PathsMap::new();
345        self.merge_from(other_api)
346    }
347}
348
349impl OpenApiBuilder {
350    /// Set the [`OpenApiVersion`] the document serializes as.
351    ///
352    /// Defaults to [`OpenApiVersion::Version31`]. Set [`OpenApiVersion::Version32`] to opt in to
353    /// OpenAPI 3.2 output.
354    ///
355    /// # Examples
356    ///
357    /// Opt in to OpenAPI 3.2 output.
358    /// ```rust
359    /// # use utoipa::openapi::{OpenApiBuilder, OpenApiVersion, Info};
360    /// let openapi = OpenApiBuilder::new()
361    ///     .openapi(OpenApiVersion::Version32)
362    ///     .info(Info::new("my api", "0.1.0"))
363    ///     .build();
364    /// ```
365    pub fn openapi(mut self, openapi: OpenApiVersion) -> Self {
366        set_value!(self openapi openapi)
367    }
368
369    /// Add [`Info`] metadata of the API.
370    pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
371        set_value!(self info info.into())
372    }
373
374    /// Add iterator of [`Server`]s to configure target servers.
375    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
376        set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
377    }
378
379    /// Add [`Paths`] to configure operations and endpoints of the API.
380    pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
381        set_value!(self paths paths.into())
382    }
383
384    /// Add [`Paths`] to describe incoming requests that may be initiated by the API provider.
385    ///
386    /// # Examples
387    ///
388    /// Describe a `newPet` webhook that the API provider may send to a subscriber.
389    /// ```rust
390    /// # use utoipa::openapi::{
391    /// #     OpenApiBuilder, Info, Paths, PathsBuilder, PathItem, HttpMethod,
392    /// #     path::OperationBuilder, response::Response,
393    /// # };
394    /// let openapi = OpenApiBuilder::new()
395    ///     .info(Info::new("Events API", "1.0.0"))
396    ///     .paths(Paths::new())
397    ///     .webhooks(Some(PathsBuilder::new().path(
398    ///         "newPet",
399    ///         PathItem::new(
400    ///             HttpMethod::Post,
401    ///             OperationBuilder::new().response("200", Response::new("Webhook received")),
402    ///         ),
403    ///     )))
404    ///     .build();
405    /// ```
406    pub fn webhooks<P: Into<Paths>>(mut self, webhooks: Option<P>) -> Self {
407        set_value!(self webhooks webhooks.map(Into::into))
408    }
409
410    /// Add [`Components`] to configure reusable schemas.
411    pub fn components(mut self, components: Option<Components>) -> Self {
412        set_value!(self components components)
413    }
414
415    /// Add iterator of [`SecurityRequirement`]s that are globally available for all operations.
416    pub fn security<I: IntoIterator<Item = SecurityRequirement>>(
417        mut self,
418        security: Option<I>,
419    ) -> Self {
420        set_value!(self security security.map(|security| security.into_iter().collect()))
421    }
422
423    /// Add iterator of [`Tag`]s to add additional documentation for **operations** tags.
424    pub fn tags<I: IntoIterator<Item = Tag>>(mut self, tags: Option<I>) -> Self {
425        set_value!(self tags tags.map(|tags| tags.into_iter().collect()))
426    }
427
428    /// Add [`ExternalDocs`] for referring additional documentation.
429    pub fn external_docs(mut self, external_docs: Option<ExternalDocs>) -> Self {
430        set_value!(self external_docs external_docs)
431    }
432
433    /// Override default `$schema` dialect for the Open API doc.
434    ///
435    /// # Examples
436    ///
437    /// _**Override default schema dialect.**_
438    /// ```rust
439    /// # use utoipa::openapi::OpenApiBuilder;
440    /// let _ = OpenApiBuilder::new()
441    ///     .schema("http://json-schema.org/draft-07/schema#")
442    ///     .build();
443    /// ```
444    pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
445        set_value!(self schema schema.into())
446    }
447
448    /// Add or change the default JSON Schema dialect for Schema Objects.
449    ///
450    /// # Examples
451    ///
452    /// ```rust
453    /// # use utoipa::openapi::OpenApiBuilder;
454    /// let _ = OpenApiBuilder::new()
455    ///     .json_schema_dialect(Some("https://spec.openapis.org/oas/3.2/dialect/2025-09-17"))
456    ///     .build();
457    /// ```
458    pub fn json_schema_dialect<S: Into<String>>(mut self, json_schema_dialect: Option<S>) -> Self {
459        set_value!(self json_schema_dialect json_schema_dialect.map(Into::into))
460    }
461
462    /// Add or change the URI identifying this OpenAPI document.
463    ///
464    /// # Examples
465    ///
466    /// ```rust
467    /// # use utoipa::openapi::OpenApiBuilder;
468    /// let _ = OpenApiBuilder::new()
469    ///     .self_uri(Some("https://example.com/openapi.json"))
470    ///     .build();
471    /// ```
472    pub fn self_uri<S: Into<String>>(mut self, self_uri: Option<S>) -> Self {
473        set_value!(self self_uri self_uri.map(Into::into))
474    }
475}
476
477/// Represents available [OpenAPI versions][version].
478///
479/// [version]: <https://spec.openapis.org/oas/latest.html#versions>
480#[derive(Serialize, Clone, PartialEq, Eq, Default)]
481#[cfg_attr(feature = "debug", derive(Debug))]
482pub enum OpenApiVersion {
483    /// Will serialize to `3.1.0`, the current default OpenAPI version.
484    #[serde(rename = "3.1.0")]
485    #[default]
486    Version31,
487    /// Will serialize to `3.2.0`. Opt-in until OpenAPI 3.2 tooling support is widespread.
488    #[serde(rename = "3.2.0")]
489    Version32,
490}
491
492impl<'de> Deserialize<'de> for OpenApiVersion {
493    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
494    where
495        D: Deserializer<'de>,
496    {
497        struct VersionVisitor;
498
499        impl<'v> Visitor<'v> for VersionVisitor {
500            type Value = OpenApiVersion;
501
502            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
503                formatter.write_str("a version string in 3.1.x or 3.2.x format")
504            }
505
506            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
507            where
508                E: Error,
509            {
510                self.visit_string(v.to_string())
511            }
512
513            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
514            where
515                E: Error,
516            {
517                let version = v
518                    .split('.')
519                    .flat_map(|digit| digit.parse::<i8>())
520                    .collect::<Vec<_>>();
521
522                if matches!(version.as_slice(), [3, 1, _]) {
523                    Ok(OpenApiVersion::Version31)
524                } else if matches!(version.as_slice(), [3, 2, _]) {
525                    Ok(OpenApiVersion::Version32)
526                } else {
527                    let expected: &dyn Expected = &"3.1.0 or 3.2.0";
528                    Err(Error::invalid_value(
529                        serde::de::Unexpected::Str(&v),
530                        expected,
531                    ))
532                }
533            }
534        }
535
536        deserializer.deserialize_string(VersionVisitor)
537    }
538}
539
540/// Value used to indicate whether reusable schema, parameter or operation is deprecated.
541///
542/// The value will serialize to boolean.
543#[derive(PartialEq, Eq, Clone, Default)]
544#[cfg_attr(feature = "debug", derive(Debug))]
545#[allow(missing_docs)]
546pub enum Deprecated {
547    True,
548    #[default]
549    False,
550}
551
552impl Serialize for Deprecated {
553    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
554    where
555        S: Serializer,
556    {
557        serializer.serialize_bool(matches!(self, Self::True))
558    }
559}
560
561impl<'de> Deserialize<'de> for Deprecated {
562    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
563    where
564        D: serde::Deserializer<'de>,
565    {
566        struct BoolVisitor;
567        impl<'de> Visitor<'de> for BoolVisitor {
568            type Value = Deprecated;
569
570            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
571                formatter.write_str("a bool true or false")
572            }
573
574            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
575            where
576                E: serde::de::Error,
577            {
578                match v {
579                    true => Ok(Deprecated::True),
580                    false => Ok(Deprecated::False),
581                }
582            }
583        }
584        deserializer.deserialize_bool(BoolVisitor)
585    }
586}
587
588/// Value used to indicate whether parameter or property is required.
589///
590/// The value will serialize to boolean.
591#[derive(PartialEq, Eq, Clone, Default)]
592#[allow(missing_docs)]
593#[cfg_attr(feature = "debug", derive(Debug))]
594pub enum Required {
595    True,
596    #[default]
597    False,
598}
599
600impl Serialize for Required {
601    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
602    where
603        S: Serializer,
604    {
605        serializer.serialize_bool(matches!(self, Self::True))
606    }
607}
608
609impl<'de> Deserialize<'de> for Required {
610    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
611    where
612        D: serde::Deserializer<'de>,
613    {
614        struct BoolVisitor;
615        impl<'de> Visitor<'de> for BoolVisitor {
616            type Value = Required;
617
618            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
619                formatter.write_str("a bool true or false")
620            }
621
622            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
623            where
624                E: serde::de::Error,
625            {
626                match v {
627                    true => Ok(Required::True),
628                    false => Ok(Required::False),
629                }
630            }
631        }
632        deserializer.deserialize_bool(BoolVisitor)
633    }
634}
635
636/// A [`Ref`] or some other type `T`.
637///
638/// Typically used in combination with [`Components`] and is an union type between [`Ref`] and any
639/// other given type such as [`Schema`] or [`Response`].
640#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
641#[cfg_attr(feature = "debug", derive(Debug))]
642#[serde(untagged)]
643pub enum RefOr<T> {
644    /// Represents [`Ref`] reference to another OpenAPI object instance. e.g.
645    /// `$ref: #/components/schemas/Hello`
646    Ref(Ref),
647    /// Represents any value that can be added to the [`struct@Components`] e.g. [`enum@Schema`]
648    /// or [`struct@Response`].
649    T(T),
650}
651
652macro_rules! build_fn {
653    ( $vis:vis $name:ident $( $field:ident ),+ ) => {
654        #[doc = concat!("Constructs a new [`", stringify!($name),"`] taking all fields values from this object.")]
655        $vis fn build(self) -> $name {
656            $name {
657                $(
658                    $field: self.$field,
659                )*
660            }
661        }
662    };
663}
664pub(crate) use build_fn;
665
666macro_rules! set_value {
667    ( $self:ident $field:ident $value:expr ) => {{
668        $self.$field = $value;
669
670        $self
671    }};
672}
673pub(crate) use set_value;
674
675macro_rules! new {
676    ( $vis:vis $name:ident ) => {
677        #[doc = concat!("Constructs a new [`", stringify!($name),"`].")]
678        $vis fn new() -> $name {
679            $name {
680                ..Default::default()
681            }
682        }
683    };
684}
685pub(crate) use new;
686
687macro_rules! from {
688    ( $name:ident $to:ident $( $field:ident ),+ ) => {
689        impl From<$name> for $to {
690            fn from(value: $name) -> Self {
691                Self {
692                    $( $field: value.$field, )*
693                }
694            }
695        }
696
697        impl From<$to> for $name {
698            fn from(value: $to) -> Self {
699                value.build()
700            }
701        }
702    };
703}
704pub(crate) use from;
705
706macro_rules! builder {
707    ( $( #[$builder_meta:meta] )* $builder_name:ident; $(#[$meta:meta])* $vis:vis $key:ident $name:ident $( $tt:tt )* ) => {
708        builder!( @type_impl $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
709        builder!( @builder_impl $( #[$builder_meta] )* $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
710    };
711
712    ( @type_impl $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
713        { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
714    ) => {
715        $( #[$meta] )*
716        $vis $key $name {
717            $( $( #[$field_meta] )* $field_vis $field: $field_ty, )*
718        }
719
720        impl $name {
721            #[doc = concat!("Construct a new ", stringify!($builder_name), ".")]
722            #[doc = ""]
723            #[doc = concat!("This is effectively same as calling [`", stringify!($builder_name), "::new`]")]
724            $vis fn builder() -> $builder_name {
725                $builder_name::new()
726            }
727        }
728    };
729
730    ( @builder_impl $( #[$builder_meta:meta] )* $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
731        { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
732    ) => {
733        #[doc = concat!("Builder for [`", stringify!($name),
734            "`] with chainable configuration methods to create a new [`", stringify!($name) , "`].")]
735        $( #[$builder_meta] )*
736        #[cfg_attr(feature = "debug", derive(Debug))]
737        $vis $key $builder_name {
738            $( $field: $field_ty, )*
739        }
740
741        impl Default for $builder_name {
742            fn default() -> Self {
743                let meta_default: $name = $name::default();
744                Self {
745                    $( $field: meta_default.$field, )*
746                }
747            }
748        }
749
750        impl $builder_name {
751            crate::openapi::new!($vis $builder_name);
752            crate::openapi::build_fn!($vis $name $( $field ),* );
753        }
754
755        crate::openapi::from!($name $builder_name $( $field ),* );
756    };
757}
758use crate::openapi::extensions::Extensions;
759pub(crate) use builder;
760
761#[cfg(test)]
762mod tests {
763    use crate::openapi::{
764        encoding::EncodingBuilder,
765        example::ExampleBuilder,
766        info::InfoBuilder,
767        link::LinkBuilder,
768        path::{
769            OperationBuilder, Parameter, ParameterBuilder, ParameterIn, ParameterStyle,
770            PathItemBuilder, PathsBuilder,
771        },
772        request_body::RequestBodyBuilder,
773        security::{DeviceAuthorization, Flow, OAuth2, Scopes, SecurityScheme},
774        tag::TagBuilder,
775        xml::XmlBuilder,
776    };
777    use insta::assert_json_snapshot;
778    use serde_json::json;
779    use std::collections::BTreeMap;
780
781    use super::{response::Response, *};
782
783    #[test]
784    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
785        assert_eq!(serde_json::to_value(&OpenApiVersion::Version32)?, "3.2.0");
786        assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
787        assert_eq!(
788            serde_json::from_str::<OpenApiVersion>("\"3.1.1\"")?,
789            OpenApiVersion::Version31
790        );
791        assert_eq!(
792            serde_json::from_str::<OpenApiVersion>("\"3.2.0\"")?,
793            OpenApiVersion::Version32
794        );
795        Ok(())
796    }
797
798    #[test]
799    fn openapi_32_root_self_and_webhooks_serialize() {
800        let openapi = OpenApiBuilder::new()
801            .openapi(OpenApiVersion::Version32)
802            .info(Info::new("Events API", "1.0.0"))
803            .json_schema_dialect(Some("https://spec.openapis.org/oas/3.2/dialect/2025-09-17"))
804            .self_uri(Some("https://example.com/openapi.json"))
805            .paths(Paths::new())
806            .webhooks(Some(PathsBuilder::new().path(
807                "newPet",
808                PathItem::new(
809                    HttpMethod::Post,
810                    OperationBuilder::new().response("200", Response::new("Webhook received")),
811                ),
812            )))
813            .build();
814
815        assert_eq!(
816            serde_json::to_value(openapi).unwrap(),
817            json!({
818                "openapi": "3.2.0",
819                "$self": "https://example.com/openapi.json",
820                "jsonSchemaDialect": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17",
821                "info": {
822                    "title": "Events API",
823                    "version": "1.0.0"
824                },
825                "paths": {},
826                "webhooks": {
827                    "newPet": {
828                        "post": {
829                            "responses": {
830                                "200": {
831                                    "description": "Webhook received"
832                                }
833                            }
834                        }
835                    }
836                }
837            })
838        );
839    }
840
841    #[test]
842    fn openapi_32_components_and_callbacks_use_spec_data_model() {
843        let callback_path = PathItemBuilder::new()
844            .query(Some(
845                OperationBuilder::new().response("200", ResponseBuilder::new()),
846            ))
847            .build();
848        let operation = OperationBuilder::new()
849            .callback(
850                "onEvent",
851                BTreeMap::from([(
852                    "{$request.body#/callbackUrl}".to_string(),
853                    callback_path.clone().into(),
854                )]),
855            )
856            .response("202", ResponseBuilder::new().summary(Some("Accepted")))
857            .build();
858        let components = ComponentsBuilder::new()
859            .parameter(
860                "Limit",
861                ParameterBuilder::new()
862                    .name("limit")
863                    .parameter_in(ParameterIn::Query)
864                    .schema(Some(ObjectBuilder::new().schema_type(Type::Integer)))
865                    .build(),
866            )
867            .example(
868                "Accepted",
869                ExampleBuilder::new().summary("Accepted").build(),
870            )
871            .request_body(
872                "EventRequest",
873                RequestBodyBuilder::new()
874                    .content(
875                        "application/json",
876                        ContentBuilder::new()
877                            .schema(Some(Ref::from_schema_name("EventPayload")))
878                            .build(),
879                    )
880                    .build(),
881            )
882            .header(
883                "RateLimit",
884                HeaderBuilder::new()
885                    .schema(Some(ObjectBuilder::new().schema_type(Type::Integer)))
886                    .build(),
887            )
888            .link(
889                "GetEvent",
890                LinkBuilder::new().operation_id("getEvent").build(),
891            )
892            .callback(
893                "EventCallback",
894                BTreeMap::from([(
895                    "{$request.body#/callbackUrl}".to_string(),
896                    callback_path.clone().into(),
897                )]),
898            )
899            .path_item("EventPath", callback_path)
900            .media_type(
901                "SseEvent",
902                ContentBuilder::new()
903                    .description(Some("Server-sent event item"))
904                    .item_schema(Some(Ref::from_schema_name("ServerEvent")))
905                    .item_encoding(Some(
906                        EncodingBuilder::new().content_type(Some("application/json")),
907                    ))
908                    .prefix_encoding([EncodingBuilder::new().content_type(Some("text/plain"))])
909                    .build(),
910            )
911            .security_scheme("ApiKey", RefOr::Ref(Ref::from_schema_name("ApiKey")))
912            .build();
913
914        assert_eq!(
915            serde_json::to_value(operation).unwrap(),
916            json!({
917                "responses": {
918                    "202": {
919                        "summary": "Accepted"
920                    }
921                },
922                "callbacks": {
923                    "onEvent": {
924                        "{$request.body#/callbackUrl}": {
925                            "query": {
926                                "responses": {
927                                    "200": {}
928                                }
929                            }
930                        }
931                    }
932                }
933            })
934        );
935        assert_eq!(
936            serde_json::to_value(components).unwrap(),
937            json!({
938                "parameters": {
939                    "Limit": {
940                        "name": "limit",
941                        "in": "query",
942                        "required": false,
943                        "schema": {
944                            "type": "integer"
945                        }
946                    }
947                },
948                "examples": {
949                    "Accepted": {
950                        "summary": "Accepted"
951                    }
952                },
953                "requestBodies": {
954                    "EventRequest": {
955                        "content": {
956                            "application/json": {
957                                "schema": {
958                                    "$ref": "#/components/schemas/EventPayload"
959                                }
960                            }
961                        }
962                    }
963                },
964                "headers": {
965                    "RateLimit": {
966                        "schema": {
967                            "type": "integer"
968                        }
969                    }
970                },
971                "links": {
972                    "GetEvent": {
973                        "operationId": "getEvent"
974                    }
975                },
976                "callbacks": {
977                    "EventCallback": {
978                        "{$request.body#/callbackUrl}": {
979                            "query": {
980                                "responses": {
981                                    "200": {}
982                                }
983                            }
984                        }
985                    }
986                },
987                "pathItems": {
988                    "EventPath": {
989                        "query": {
990                            "responses": {
991                                "200": {}
992                            }
993                        }
994                    }
995                },
996                "mediaTypes": {
997                    "SseEvent": {
998                        "description": "Server-sent event item",
999                        "itemSchema": {
1000                            "$ref": "#/components/schemas/ServerEvent"
1001                        },
1002                        "prefixEncoding": [
1003                            {
1004                                "contentType": "text/plain"
1005                            }
1006                        ],
1007                        "itemEncoding": {
1008                            "contentType": "application/json"
1009                        }
1010                    }
1011                },
1012                "securitySchemes": {
1013                    "ApiKey": {
1014                        "$ref": "#/components/schemas/ApiKey"
1015                    }
1016                }
1017            })
1018        );
1019    }
1020
1021    #[test]
1022    fn openapi_32_tags_server_xml_and_examples_serialize() {
1023        let tag = TagBuilder::new()
1024            .name("partner")
1025            .summary(Some("Partner"))
1026            .description(Some("Operations available to partners"))
1027            .parent(Some("external"))
1028            .kind(Some("audience"))
1029            .build();
1030        let server = ServerBuilder::new()
1031            .url("https://api.example.com")
1032            .name(Some("production"))
1033            .build();
1034        let xml = XmlBuilder::new()
1035            .name(Some("animal"))
1036            .node_type(Some("element"))
1037            .build();
1038        let example = ExampleBuilder::new()
1039            .summary("Serialized query")
1040            .data_value(Some(json!({"flag": true})))
1041            .serialized_value(Some("flag=true"))
1042            .build();
1043
1044        assert_eq!(
1045            serde_json::to_value(tag).unwrap(),
1046            json!({
1047                "name": "partner",
1048                "summary": "Partner",
1049                "description": "Operations available to partners",
1050                "parent": "external",
1051                "kind": "audience"
1052            })
1053        );
1054        assert_eq!(
1055            serde_json::to_value(server).unwrap(),
1056            json!({
1057                "url": "https://api.example.com",
1058                "name": "production"
1059            })
1060        );
1061        assert_eq!(
1062            serde_json::to_value(xml).unwrap(),
1063            json!({
1064                "name": "animal",
1065                "nodeType": "element"
1066            })
1067        );
1068        assert_eq!(
1069            serde_json::to_value(example).unwrap(),
1070            json!({
1071                "summary": "Serialized query",
1072                "dataValue": {
1073                    "flag": true
1074                },
1075                "serializedValue": "flag=true"
1076            })
1077        );
1078    }
1079
1080    #[test]
1081    fn openapi_32_query_and_additional_operations_serialize() {
1082        let path_item = PathItemBuilder::new()
1083            .query(Some(
1084                OperationBuilder::new()
1085                    .operation_id(Some("searchProducts"))
1086                    .request_body(Some(
1087                        request_body::RequestBodyBuilder::new()
1088                            .content(
1089                                "application/json",
1090                                ContentBuilder::new()
1091                                    .schema(Some(Ref::from_schema_name("SearchCriteria")))
1092                                    .build(),
1093                            )
1094                            .build(),
1095                    ))
1096                    .response("200", Response::new("Search results")),
1097            ))
1098            .additional_operation(
1099                "COPY",
1100                OperationBuilder::new()
1101                    .operation_id(Some("copyPet"))
1102                    .response("200", Response::new("Copied")),
1103            )
1104            .build();
1105
1106        assert_eq!(
1107            serde_json::to_value(path_item).unwrap(),
1108            json!({
1109                "query": {
1110                    "operationId": "searchProducts",
1111                    "requestBody": {
1112                        "content": {
1113                            "application/json": {
1114                                "schema": {
1115                                    "$ref": "#/components/schemas/SearchCriteria"
1116                                }
1117                            }
1118                        }
1119                    },
1120                    "responses": {
1121                        "200": {
1122                            "description": "Search results"
1123                        }
1124                    }
1125                },
1126                "additionalOperations": {
1127                    "COPY": {
1128                        "operationId": "copyPet",
1129                        "responses": {
1130                            "200": {
1131                                "description": "Copied"
1132                            }
1133                        }
1134                    }
1135                }
1136            })
1137        );
1138    }
1139
1140    #[test]
1141    fn openapi_32_querystring_parameter_and_cookie_style_serialize() {
1142        let querystring = ParameterBuilder::from(Parameter::new("advancedQuery"))
1143            .parameter_in(ParameterIn::QueryString)
1144            .required(Required::False)
1145            .content(
1146                "application/x-www-form-urlencoded",
1147                ContentBuilder::new()
1148                    .schema(Some(
1149                        ObjectBuilder::new()
1150                            .schema_type(Type::Object)
1151                            .property("foo", ObjectBuilder::new().schema_type(Type::String))
1152                            .property("bar", ObjectBuilder::new().schema_type(Type::Boolean)),
1153                    ))
1154                    .examples_from_iter([(
1155                        "spacesAndPluses",
1156                        ExampleBuilder::new()
1157                            .description("Form-encoded query string")
1158                            .data_value(Some(json!({
1159                                "foo": "a + b",
1160                                "bar": true
1161                            })))
1162                            .serialized_value(Some("foo=a+%2B+b&bar=true")),
1163                    )])
1164                    .build(),
1165            )
1166            .build();
1167        let cookie = ParameterBuilder::from(Parameter::new("greeting"))
1168            .parameter_in(ParameterIn::Cookie)
1169            .style(Some(ParameterStyle::Cookie))
1170            .example(Some(json!("Hello, world!")))
1171            .build();
1172
1173        assert_eq!(
1174            serde_json::to_value(querystring).unwrap(),
1175            json!({
1176                "name": "advancedQuery",
1177                "in": "querystring",
1178                "required": false,
1179                "content": {
1180                    "application/x-www-form-urlencoded": {
1181                        "schema": {
1182                            "type": "object",
1183                            "properties": {
1184                                "bar": {
1185                                    "type": "boolean"
1186                                },
1187                                "foo": {
1188                                    "type": "string"
1189                                }
1190                            }
1191                        },
1192                        "examples": {
1193                            "spacesAndPluses": {
1194                                "description": "Form-encoded query string",
1195                                "dataValue": {
1196                                    "foo": "a + b",
1197                                    "bar": true
1198                                },
1199                                "serializedValue": "foo=a+%2B+b&bar=true"
1200                            }
1201                        }
1202                    }
1203                }
1204            })
1205        );
1206        assert_eq!(
1207            serde_json::to_value(cookie).unwrap(),
1208            json!({
1209                "name": "greeting",
1210                "in": "cookie",
1211                "required": true,
1212                "style": "cookie",
1213                "example": "Hello, world!"
1214            })
1215        );
1216    }
1217
1218    #[test]
1219    fn openapi_32_components_media_types_streaming_and_response_summary_serialize() {
1220        let event_payload = ObjectBuilder::new()
1221            .schema_type(Type::Object)
1222            .property("pet_id", ObjectBuilder::new().schema_type(Type::Integer))
1223            .property("status", ObjectBuilder::new().schema_type(Type::String));
1224        let server_event = ObjectBuilder::new()
1225            .schema_type(Type::Object)
1226            .property("event", ObjectBuilder::new().schema_type(Type::String))
1227            .property(
1228                "data",
1229                ObjectBuilder::new()
1230                    .schema_type(Type::String)
1231                    .content_media_type("application/json")
1232                    .content_schema(Some(Ref::from_schema_name("PetEventPayload"))),
1233            )
1234            .property("id", ObjectBuilder::new().schema_type(Type::String))
1235            .property("retry", ObjectBuilder::new().schema_type(Type::Integer));
1236        let components = ComponentsBuilder::new()
1237            .schema("PetEventPayload", event_payload)
1238            .schema("ServerEvent", server_event)
1239            .media_type(
1240                "ServerSentEvents",
1241                ContentBuilder::new()
1242                    .item_schema(Some(Ref::from_schema_name("ServerEvent")))
1243                    .build(),
1244            )
1245            .media_type(
1246                "JsonLines",
1247                ContentBuilder::new()
1248                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1249                    .build(),
1250            )
1251            .media_type(
1252                "JsonTextSequences",
1253                ContentBuilder::new()
1254                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1255                    .build(),
1256            )
1257            .media_type(
1258                "MultipartMixed",
1259                ContentBuilder::new()
1260                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1261                    .build(),
1262            )
1263            .build();
1264        let response = ResponseBuilder::new()
1265            .summary(Some("Streaming response"))
1266            .content(
1267                "text/event-stream",
1268                ContentBuilder::new()
1269                    .item_schema(Some(Ref::from_schema_name("ServerEvent")))
1270                    .build(),
1271            )
1272            .content(
1273                "application/jsonl",
1274                ContentBuilder::new()
1275                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1276                    .build(),
1277            )
1278            .content(
1279                "application/json-seq",
1280                ContentBuilder::new()
1281                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1282                    .build(),
1283            )
1284            .content(
1285                "multipart/mixed",
1286                ContentBuilder::new()
1287                    .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1288                    .build(),
1289            )
1290            .build();
1291
1292        assert_eq!(
1293            serde_json::to_value(components).unwrap(),
1294            json!({
1295                "schemas": {
1296                    "PetEventPayload": {
1297                        "type": "object",
1298                        "properties": {
1299                            "pet_id": {
1300                                "type": "integer"
1301                            },
1302                            "status": {
1303                                "type": "string"
1304                            }
1305                        }
1306                    },
1307                    "ServerEvent": {
1308                        "type": "object",
1309                        "properties": {
1310                            "data": {
1311                                "type": "string",
1312                                "contentMediaType": "application/json",
1313                                "contentSchema": {
1314                                    "$ref": "#/components/schemas/PetEventPayload"
1315                                }
1316                            },
1317                            "event": {
1318                                "type": "string"
1319                            },
1320                            "id": {
1321                                "type": "string"
1322                            },
1323                            "retry": {
1324                                "type": "integer"
1325                            }
1326                        }
1327                    }
1328                },
1329                "mediaTypes": {
1330                    "JsonLines": {
1331                        "itemSchema": {
1332                            "$ref": "#/components/schemas/PetEventPayload"
1333                        }
1334                    },
1335                    "JsonTextSequences": {
1336                        "itemSchema": {
1337                            "$ref": "#/components/schemas/PetEventPayload"
1338                        }
1339                    },
1340                    "MultipartMixed": {
1341                        "itemSchema": {
1342                            "$ref": "#/components/schemas/PetEventPayload"
1343                        }
1344                    },
1345                    "ServerSentEvents": {
1346                        "itemSchema": {
1347                            "$ref": "#/components/schemas/ServerEvent"
1348                        }
1349                    }
1350                }
1351            })
1352        );
1353        assert_eq!(
1354            serde_json::to_value(response).unwrap(),
1355            json!({
1356                "summary": "Streaming response",
1357                "content": {
1358                    "application/json-seq": {
1359                        "itemSchema": {
1360                            "$ref": "#/components/schemas/PetEventPayload"
1361                        }
1362                    },
1363                    "application/jsonl": {
1364                        "itemSchema": {
1365                            "$ref": "#/components/schemas/PetEventPayload"
1366                        }
1367                    },
1368                    "multipart/mixed": {
1369                        "itemSchema": {
1370                            "$ref": "#/components/schemas/PetEventPayload"
1371                        }
1372                    },
1373                    "text/event-stream": {
1374                        "itemSchema": {
1375                            "$ref": "#/components/schemas/ServerEvent"
1376                        }
1377                    }
1378                }
1379            })
1380        );
1381    }
1382
1383    #[test]
1384    fn openapi_32_oauth_device_authorization_and_deprecated_security_serialize() {
1385        let oauth = SecurityScheme::OAuth2(
1386            OAuth2::new([Flow::DeviceAuthorization(DeviceAuthorization::new(
1387                "https://example.com/device",
1388                "https://example.com/token",
1389                Scopes::from_iter([("read:pets", "read pets")]),
1390            ))])
1391            .with_metadata_url("https://example.com/.well-known/oauth-authorization-server")
1392            .deprecated(Some(Deprecated::True)),
1393        );
1394
1395        assert_eq!(
1396            serde_json::to_value(oauth).unwrap(),
1397            json!({
1398                "type": "oauth2",
1399                "flows": {
1400                    "deviceAuthorization": {
1401                        "deviceAuthorizationUrl": "https://example.com/device",
1402                        "tokenUrl": "https://example.com/token",
1403                        "scopes": {
1404                            "read:pets": "read pets"
1405                        }
1406                    }
1407                },
1408                "oauth2MetadataUrl": "https://example.com/.well-known/oauth-authorization-server",
1409                "deprecated": true
1410            })
1411        );
1412    }
1413
1414    #[test]
1415    fn serialize_openapi_json_minimal_success() {
1416        let openapi = OpenApi::new(
1417            InfoBuilder::new()
1418                .title("My api")
1419                .version("1.0.0")
1420                .description(Some("My api description"))
1421                .license(Some(
1422                    LicenseBuilder::new()
1423                        .name("MIT")
1424                        .url(Some("http://mit.licence"))
1425                        .build(),
1426                ))
1427                .build(),
1428            Paths::new(),
1429        );
1430
1431        assert_json_snapshot!(openapi);
1432    }
1433
1434    #[test]
1435    fn serialize_openapi_json_with_paths_success() {
1436        let openapi = OpenApi::new(
1437            Info::new("My big api", "1.1.0"),
1438            PathsBuilder::new()
1439                .path(
1440                    "/api/v1/users",
1441                    PathItem::new(
1442                        HttpMethod::Get,
1443                        OperationBuilder::new().response("200", Response::new("Get users list")),
1444                    ),
1445                )
1446                .path(
1447                    "/api/v1/users",
1448                    PathItem::new(
1449                        HttpMethod::Post,
1450                        OperationBuilder::new().response("200", Response::new("Post new user")),
1451                    ),
1452                )
1453                .path(
1454                    "/api/v1/users/{id}",
1455                    PathItem::new(
1456                        HttpMethod::Get,
1457                        OperationBuilder::new().response("200", Response::new("Get user by id")),
1458                    ),
1459                ),
1460        );
1461
1462        assert_json_snapshot!(openapi);
1463    }
1464
1465    #[test]
1466    fn merge_2_openapi_documents() {
1467        let mut api_1 = OpenApi::new(
1468            Info::new("Api", "v1"),
1469            PathsBuilder::new()
1470                .path(
1471                    "/api/v1/user",
1472                    PathItem::new(
1473                        HttpMethod::Get,
1474                        OperationBuilder::new().response("200", Response::new("Get user success")),
1475                    ),
1476                )
1477                .build(),
1478        );
1479
1480        let api_2 = OpenApiBuilder::new()
1481            .info(Info::new("Api", "v2"))
1482            .paths(
1483                PathsBuilder::new()
1484                    .path(
1485                        "/api/v1/user",
1486                        PathItem::new(
1487                            HttpMethod::Get,
1488                            OperationBuilder::new()
1489                                .response("200", Response::new("This will not get added")),
1490                        ),
1491                    )
1492                    .path(
1493                        "/ap/v2/user",
1494                        PathItem::new(
1495                            HttpMethod::Get,
1496                            OperationBuilder::new()
1497                                .response("200", Response::new("Get user success 2")),
1498                        ),
1499                    )
1500                    .path(
1501                        "/api/v2/user",
1502                        PathItem::new(
1503                            HttpMethod::Post,
1504                            OperationBuilder::new()
1505                                .response("200", Response::new("Get user success")),
1506                        ),
1507                    )
1508                    .build(),
1509            )
1510            .components(Some(
1511                ComponentsBuilder::new()
1512                    .schema(
1513                        "User2",
1514                        ObjectBuilder::new().schema_type(Type::Object).property(
1515                            "name",
1516                            ObjectBuilder::new().schema_type(Type::String).build(),
1517                        ),
1518                    )
1519                    .build(),
1520            ))
1521            .build();
1522
1523        api_1.merge(api_2);
1524
1525        assert_json_snapshot!(api_1, {
1526            ".paths" => insta::sorted_redaction()
1527        });
1528    }
1529
1530    #[test]
1531    fn merge_same_path_diff_methods() {
1532        let mut api_1 = OpenApi::new(
1533            Info::new("Api", "v1"),
1534            PathsBuilder::new()
1535                .path(
1536                    "/api/v1/user",
1537                    PathItem::new(
1538                        HttpMethod::Get,
1539                        OperationBuilder::new()
1540                            .response("200", Response::new("Get user success 1")),
1541                    ),
1542                )
1543                .extensions(Some(Extensions::from_iter([("x-v1-api", true)])))
1544                .build(),
1545        );
1546
1547        let api_2 = OpenApiBuilder::new()
1548            .info(Info::new("Api", "v2"))
1549            .paths(
1550                PathsBuilder::new()
1551                    .path(
1552                        "/api/v1/user",
1553                        PathItem::new(
1554                            HttpMethod::Get,
1555                            OperationBuilder::new()
1556                                .response("200", Response::new("This will not get added")),
1557                        ),
1558                    )
1559                    .path(
1560                        "/api/v1/user",
1561                        PathItem::new(
1562                            HttpMethod::Post,
1563                            OperationBuilder::new()
1564                                .response("200", Response::new("Post user success 1")),
1565                        ),
1566                    )
1567                    .path(
1568                        "/api/v2/user",
1569                        PathItem::new(
1570                            HttpMethod::Get,
1571                            OperationBuilder::new()
1572                                .response("200", Response::new("Get user success 2")),
1573                        ),
1574                    )
1575                    .path(
1576                        "/api/v2/user",
1577                        PathItem::new(
1578                            HttpMethod::Post,
1579                            OperationBuilder::new()
1580                                .response("200", Response::new("Post user success 2")),
1581                        ),
1582                    )
1583                    .extensions(Some(Extensions::from_iter([("x-random", "Value")])))
1584                    .build(),
1585            )
1586            .components(Some(
1587                ComponentsBuilder::new()
1588                    .schema(
1589                        "User2",
1590                        ObjectBuilder::new().schema_type(Type::Object).property(
1591                            "name",
1592                            ObjectBuilder::new().schema_type(Type::String).build(),
1593                        ),
1594                    )
1595                    .build(),
1596            ))
1597            .build();
1598
1599        api_1.merge(api_2);
1600
1601        assert_json_snapshot!(api_1, {
1602            ".paths" => insta::sorted_redaction()
1603        });
1604    }
1605
1606    #[test]
1607    fn test_nest_open_apis() {
1608        let api = OpenApiBuilder::new()
1609            .paths(
1610                PathsBuilder::new().path(
1611                    "/api/v1/status",
1612                    PathItem::new(
1613                        HttpMethod::Get,
1614                        OperationBuilder::new()
1615                            .description(Some("Get status"))
1616                            .build(),
1617                    ),
1618                ),
1619            )
1620            .build();
1621
1622        let user_api = OpenApiBuilder::new()
1623            .paths(
1624                PathsBuilder::new()
1625                    .path(
1626                        "/",
1627                        PathItem::new(
1628                            HttpMethod::Get,
1629                            OperationBuilder::new()
1630                                .description(Some("Get user details"))
1631                                .build(),
1632                        ),
1633                    )
1634                    .path(
1635                        "/foo",
1636                        PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
1637                    ),
1638            )
1639            .build();
1640
1641        let nest_merged = api.nest("/api/v1/user", user_api);
1642        let value = serde_json::to_value(nest_merged).expect("should serialize as json");
1643        let paths = value
1644            .pointer("/paths")
1645            .expect("paths should exits in openapi");
1646
1647        assert_json_snapshot!(paths);
1648    }
1649
1650    #[test]
1651    fn openapi_custom_extension() {
1652        let mut api = OpenApiBuilder::new().build();
1653        let extensions = api.extensions.get_or_insert(Default::default());
1654        extensions.insert(
1655            String::from("x-tagGroup"),
1656            String::from("anything that serializes to Json").into(),
1657        );
1658
1659        assert_json_snapshot!(api);
1660    }
1661}