openapiv3_1/
lib.rs

1//! Rust implementation of OpenAPI Spec v3.1.x
2//!
3//! A lof the code was taken from [`utoipa`](https://crates.io/crates/utoipa).
4//!
5//! The main difference is the full JSON Schema 2020-12 Definitions.
6#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
7#![cfg_attr(feature = "docs", doc = "## Feature flags")]
8#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
9//! ## Alternatives
10//!
11//! - [`openapiv3`](https://crates.io/crates/openapiv3): Implements the openapi v3.0.x spec, does not implement full json schema draft 2020-12 spec.
12//! - [`utoipa`](https://crates.io/crates/utoipa): A fully fletched openapi-type-generator implementing some of the v3.1.x spec.
13//! - [`schemars`](https://crates.io/crates/schemars): A fully fletched jsonschema-type-generator implementing some of the json schema draft 2020-12 spec.
14//!
15//! ## License
16//!
17//! This project is licensed under the MIT or Apache-2.0 license.
18//! You can choose between one of them if you use this work.
19//!
20//! `SPDX-License-Identifier: MIT OR Apache-2.0`
21#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
22#![cfg_attr(docsrs, feature(doc_cfg))]
23#![deny(missing_docs)]
24#![deny(unsafe_code)]
25#![deny(unreachable_pub)]
26
27use std::fmt::Formatter;
28
29use indexmap::IndexMap;
30use serde::de::{Error, Expected, Visitor};
31use serde::{Deserializer, Serializer};
32use serde_derive::{Deserialize, Serialize};
33
34pub use self::content::{Content, ContentBuilder};
35pub use self::external_docs::ExternalDocs;
36pub use self::header::{Header, HeaderBuilder};
37pub use self::info::{Contact, ContactBuilder, Info, InfoBuilder, License, LicenseBuilder};
38pub use self::path::{HttpMethod, PathItem, Paths, PathsBuilder};
39pub use self::response::{Response, ResponseBuilder, Responses, ResponsesBuilder};
40pub use self::schema::{Components, ComponentsBuilder, Discriminator, Object, Ref, Schema, Type};
41pub use self::security::SecurityRequirement;
42pub use self::server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder};
43pub use self::tag::Tag;
44
45pub mod content;
46pub mod encoding;
47pub mod example;
48pub mod extensions;
49pub mod external_docs;
50pub mod header;
51pub mod info;
52pub mod link;
53pub mod path;
54pub mod request_body;
55pub mod response;
56pub mod schema;
57pub mod security;
58pub mod server;
59pub mod tag;
60pub mod xml;
61
62/// Root object of the OpenAPI document.
63///
64/// You can use [`OpenApi::new`] function to construct a new [`OpenApi`] instance and then
65/// use the fields with mutable access to modify them. This is quite tedious if you are not simply
66/// just changing one thing thus you can also use the [`OpenApi::builder`] to use builder to
67/// construct a new [`OpenApi`] object.
68///
69/// See more details at <https://spec.openapis.org/oas/latest.html#openapi-object>.
70#[non_exhaustive]
71#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default, Clone, PartialEq, bon::Builder)]
72#[cfg_attr(feature = "debug", derive(Debug))]
73#[serde(rename_all = "camelCase")]
74#[builder(on(_, into))]
75pub struct OpenApi {
76    /// OpenAPI document version.
77    #[builder(default)]
78    pub openapi: OpenApiVersion,
79
80    /// Provides metadata about the API.
81    ///
82    /// See more details at <https://spec.openapis.org/oas/latest.html#info-object>.
83    #[builder(default)]
84    pub info: Info,
85
86    /// Optional list of servers that provides the connectivity information to target servers.
87    ///
88    /// This is implicitly one server with `url` set to `/`.
89    ///
90    /// See more details at <https://spec.openapis.org/oas/latest.html#server-object>.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub servers: Option<Vec<Server>>,
93
94    /// Available paths and operations for the API.
95    ///
96    /// See more details at <https://spec.openapis.org/oas/latest.html#paths-object>.
97    #[builder(default)]
98    pub paths: Paths,
99
100    /// Holds various reusable schemas for the OpenAPI document.
101    ///
102    /// Few of these elements are security schemas and object schemas.
103    ///
104    /// See more details at <https://spec.openapis.org/oas/latest.html#components-object>.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub components: Option<Components>,
107
108    /// Declaration of global security mechanisms that can be used across the API. The individual operations
109    /// can override the declarations. You can use `SecurityRequirement::default()` if you wish to make security
110    /// optional by adding it to the list of securities.
111    ///
112    /// See more details at <https://spec.openapis.org/oas/latest.html#security-requirement-object>.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub security: Option<Vec<SecurityRequirement>>,
115
116    /// Optional list of tags can be used to add additional documentation to matching tags of operations.
117    ///
118    /// See more details at <https://spec.openapis.org/oas/latest.html#tag-object>.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub tags: Option<Vec<Tag>>,
121
122    /// Optional global additional documentation reference.
123    ///
124    /// See more details at <https://spec.openapis.org/oas/latest.html#external-documentation-object>.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub external_docs: Option<ExternalDocs>,
127
128    /// Schema keyword can be used to override default _`$schema`_ dialect which is by default
129    /// “<https://spec.openapis.org/oas/3.1/dialect/base>”.
130    ///
131    /// All the references and individual files could use their own schema dialect.
132    #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
133    #[builder(default)]
134    pub schema: String,
135
136    /// Optional extensions "x-something".
137    #[serde(skip_serializing_if = "Option::is_none", flatten)]
138    pub extensions: Option<Extensions>,
139}
140
141impl OpenApi {
142    /// Construct a new [`OpenApi`] object.
143    ///
144    /// Function accepts two arguments one which is [`Info`] metadata of the API; two which is [`Paths`]
145    /// containing operations for the API.
146    ///
147    /// # Examples
148    ///
149    /// ```rust
150    /// # use openapiv3_1::{Info, Paths, OpenApi};
151    /// #
152    /// let openapi = OpenApi::new(Info::new("pet api", "0.1.0"), Paths::new());
153    /// ```
154    pub fn new(info: impl Into<Info>, paths: impl Into<Paths>) -> Self {
155        Self {
156            info: info.into(),
157            paths: paths.into(),
158            ..Default::default()
159        }
160    }
161
162    /// Converts this [`OpenApi`] to JSON String. This method essentially calls [`serde_json::to_string`] method.
163    pub fn to_json(&self) -> Result<String, serde_json::Error> {
164        serde_json::to_string(self)
165    }
166
167    /// Converts this [`OpenApi`] to pretty JSON String. This method essentially calls [`serde_json::to_string_pretty`] method.
168    pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
169        serde_json::to_string_pretty(self)
170    }
171
172    /// Converts this [`OpenApi`] to YAML String. This method essentially calls [`serde_norway::to_string`] method.
173    #[cfg(feature = "yaml")]
174    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
175    pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
176        serde_norway::to_string(self)
177    }
178
179    /// Merge `other` [`OpenApi`] moving `self` and returning combined [`OpenApi`].
180    ///
181    /// In functionality wise this is exactly same as calling [`OpenApi::merge`] but but provides
182    /// leaner API for chaining method calls.
183    pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
184        self.merge(other);
185        self
186    }
187
188    /// Merge `other` [`OpenApi`] consuming it and resuming it's content.
189    ///
190    /// Merge function will take all `self` nonexistent _`servers`, `paths`, `schemas`, `responses`,
191    /// `security_schemes`, `security_requirements` and `tags`_ from _`other`_ [`OpenApi`].
192    ///
193    /// This function performs a shallow comparison for `paths`, `schemas`, `responses` and
194    /// `security schemes` which means that only _`name`_ and _`path`_ is used for comparison. When
195    /// match occurs the whole item will be ignored from merged results. Only items not
196    /// found will be appended to `self`.
197    ///
198    /// For _`servers`_, _`tags`_ and _`security_requirements`_ the whole item will be used for
199    /// comparison. Items not found from `self` will be appended to `self`.
200    ///
201    /// **Note!** `info`, `openapi`, `external_docs` and `schema` will not be merged.
202    pub fn merge(&mut self, mut other: OpenApi) {
203        if let Some(other_servers) = &mut other.servers {
204            let servers = self.servers.get_or_insert(Vec::new());
205            other_servers.retain(|server| !servers.contains(server));
206            servers.append(other_servers);
207        }
208
209        if !other.paths.paths.is_empty() {
210            self.paths.merge(other.paths);
211        };
212
213        if let Some(other_components) = &mut other.components {
214            let components = self.components.get_or_insert(Components::default());
215
216            other_components
217                .schemas
218                .retain(|name, _| !components.schemas.contains_key(name));
219            components.schemas.append(&mut other_components.schemas);
220
221            other_components
222                .responses
223                .retain(|name, _| !components.responses.contains_key(name));
224            components.responses.append(&mut other_components.responses);
225
226            other_components
227                .security_schemes
228                .retain(|name, _| !components.security_schemes.contains_key(name));
229            components.security_schemes.append(&mut other_components.security_schemes);
230        }
231
232        if let Some(other_security) = &mut other.security {
233            let security = self.security.get_or_insert(Vec::new());
234            other_security.retain(|requirement| !security.contains(requirement));
235            security.append(other_security);
236        }
237
238        if let Some(other_tags) = &mut other.tags {
239            let tags = self.tags.get_or_insert(Vec::new());
240            other_tags.retain(|tag| !tags.contains(tag));
241            tags.append(other_tags);
242        }
243    }
244
245    /// Nest `other` [`OpenApi`] to this [`OpenApi`].
246    ///
247    /// Nesting performs custom [`OpenApi::merge`] where `other` [`OpenApi`] paths are prepended with given
248    /// `path` and then appended to _`paths`_ of this [`OpenApi`] instance. Rest of the  `other`
249    /// [`OpenApi`] instance is merged to this [`OpenApi`] with [`OpenApi::merge_from`] method.
250    ///
251    /// **If multiple** APIs are being nested with same `path` only the **last** one will be retained.
252    ///
253    /// Method accepts two arguments, first is the path to prepend .e.g. _`/user`_. Second argument
254    /// is the [`OpenApi`] to prepend paths for.
255    ///
256    /// # Examples
257    ///
258    /// _**Merge `user_api` to `api` nesting `user_api` paths under `/api/v1/user`**_
259    /// ```rust
260    ///  # use openapiv3_1::{OpenApi, OpenApiBuilder};
261    ///  # use openapiv3_1::path::{Paths, PathItem,
262    ///  # HttpMethod, Operation};
263    ///  let api = OpenApi::builder()
264    ///      .paths(
265    ///          Paths::builder().path(
266    ///              "/api/v1/status",
267    ///              PathItem::new(
268    ///                  HttpMethod::Get,
269    ///                  Operation::builder()
270    ///                      .description("Get status")
271    ///                      .build(),
272    ///              ),
273    ///          ),
274    ///      )
275    ///      .build();
276    ///  let user_api = OpenApi::builder()
277    ///     .paths(
278    ///         Paths::builder().path(
279    ///             "/",
280    ///             PathItem::new(HttpMethod::Post, Operation::builder().build()),
281    ///         )
282    ///     )
283    ///     .build();
284    ///  let nested = api.nest("/api/v1/user", user_api);
285    /// ```
286    pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
287        self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
288    }
289
290    /// Nest `other` [`OpenApi`] with custom path composer.
291    ///
292    /// In most cases you should use [`OpenApi::nest`] instead.
293    /// Only use this method if you need custom path composition for a specific use case.
294    ///
295    /// `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.
296    pub fn nest_with_path_composer<P: Into<String>, O: Into<OpenApi>, F: Fn(&str, &str) -> String>(
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.paths.paths.into_iter().map(|(item_path, item)| {
306            let path = composer(&path, &item_path);
307            (path, item)
308        });
309
310        self.paths.paths.extend(nested_paths);
311
312        // paths are already merged, thus we can ignore them
313        other_api.paths.paths = IndexMap::new();
314        self.merge_from(other_api)
315    }
316}
317
318/// Represents available [OpenAPI versions][version].
319///
320/// [version]: <https://spec.openapis.org/oas/latest.html#versions>
321#[derive(Serialize, Clone, PartialEq, Eq, Default)]
322#[cfg_attr(feature = "debug", derive(Debug))]
323pub enum OpenApiVersion {
324    /// Will serialize to `3.1.0` the latest released OpenAPI version.
325    #[serde(rename = "3.1.0")]
326    #[default]
327    Version31,
328}
329
330impl<'de> serde::Deserialize<'de> for OpenApiVersion {
331    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
332    where
333        D: Deserializer<'de>,
334    {
335        struct VersionVisitor;
336
337        impl<'v> Visitor<'v> for VersionVisitor {
338            type Value = OpenApiVersion;
339
340            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
341                formatter.write_str("a version string in 3.1.x format")
342            }
343
344            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
345            where
346                E: Error,
347            {
348                self.visit_string(v.to_string())
349            }
350
351            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
352            where
353                E: Error,
354            {
355                let version = v.split('.').flat_map(|digit| digit.parse::<i8>()).collect::<Vec<_>>();
356
357                if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
358                    Ok(OpenApiVersion::Version31)
359                } else {
360                    let expected: &dyn Expected = &"3.1.0";
361                    Err(Error::invalid_value(serde::de::Unexpected::Str(&v), expected))
362                }
363            }
364        }
365
366        deserializer.deserialize_string(VersionVisitor)
367    }
368}
369
370/// Value used to indicate whether reusable schema, parameter or operation is deprecated.
371///
372/// The value will serialize to boolean.
373#[derive(PartialEq, Eq, Clone, Default)]
374#[cfg_attr(feature = "debug", derive(Debug))]
375#[allow(missing_docs)]
376pub enum Deprecated {
377    True,
378    #[default]
379    False,
380}
381
382impl serde::Serialize for Deprecated {
383    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
384    where
385        S: Serializer,
386    {
387        serializer.serialize_bool(matches!(self, Self::True))
388    }
389}
390
391impl<'de> serde::Deserialize<'de> for Deprecated {
392    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
393    where
394        D: serde::Deserializer<'de>,
395    {
396        struct BoolVisitor;
397        impl<'de> Visitor<'de> for BoolVisitor {
398            type Value = Deprecated;
399
400            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
401                formatter.write_str("a bool true or false")
402            }
403
404            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
405            where
406                E: serde::de::Error,
407            {
408                match v {
409                    true => Ok(Deprecated::True),
410                    false => Ok(Deprecated::False),
411                }
412            }
413        }
414        deserializer.deserialize_bool(BoolVisitor)
415    }
416}
417
418/// A [`Ref`] or some other type `T`.
419///
420/// Typically used in combination with [`Components`] and is an union type between [`Ref`] and any
421/// other given type such as [`Schema`] or [`Response`].
422#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
423#[cfg_attr(feature = "debug", derive(Debug))]
424#[serde(untagged)]
425pub enum RefOr<T> {
426    /// Represents [`Ref`] reference to another OpenAPI object instance. e.g.
427    /// `$ref: #/components/schemas/Hello`
428    Ref(Ref),
429    /// Represents any value that can be added to the [`struct@Components`] e.g. [`enum@Schema`]
430    /// or [`struct@Response`].
431    T(T),
432}
433
434use crate::extensions::Extensions;
435
436/// Changelogs generated by [scuffle_changelog]
437#[cfg(feature = "docs")]
438#[scuffle_changelog::changelog]
439pub mod changelog {}
440
441#[cfg(test)]
442#[cfg_attr(coverage_nightly, coverage(off))]
443mod tests {
444    use insta::assert_json_snapshot;
445
446    use super::response::Response;
447    use super::*;
448    use crate::path::Operation;
449
450    #[test]
451    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
452        assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
453        Ok(())
454    }
455
456    #[test]
457    fn serialize_openapi_json_minimal_success() {
458        let openapi = OpenApi::new(
459            Info::builder()
460                .title("My api")
461                .version("1.0.0")
462                .description("My api description")
463                .license(License::builder().name("MIT").url("http://mit.licence")),
464            Paths::new(),
465        );
466
467        assert_json_snapshot!(openapi);
468    }
469
470    #[test]
471    fn serialize_openapi_json_with_paths_success() {
472        let openapi = OpenApi::new(
473            Info::new("My big api", "1.1.0"),
474            Paths::builder()
475                .path(
476                    "/api/v1/users",
477                    PathItem::new(
478                        HttpMethod::Get,
479                        Operation::builder().response("200", Response::new("Get users list")),
480                    ),
481                )
482                .path(
483                    "/api/v1/users",
484                    PathItem::new(
485                        HttpMethod::Post,
486                        Operation::builder().response("200", Response::new("Post new user")),
487                    ),
488                )
489                .path(
490                    "/api/v1/users/{id}",
491                    PathItem::new(
492                        HttpMethod::Get,
493                        Operation::builder().response("200", Response::new("Get user by id")),
494                    ),
495                ),
496        );
497
498        assert_json_snapshot!(openapi);
499    }
500
501    #[test]
502    fn merge_2_openapi_documents() {
503        let mut api_1 = OpenApi::new(
504            Info::new("Api", "v1"),
505            Paths::builder()
506                .path(
507                    "/api/v1/user",
508                    PathItem::new(
509                        HttpMethod::Get,
510                        Operation::builder().response("200", Response::new("Get user success")),
511                    ),
512                )
513                .build(),
514        );
515
516        let api_2 = OpenApi::builder()
517            .info(Info::new("Api", "v2"))
518            .paths(
519                Paths::builder()
520                    .path(
521                        "/api/v1/user",
522                        PathItem::new(
523                            HttpMethod::Get,
524                            Operation::builder().response("200", Response::new("This will not get added")),
525                        ),
526                    )
527                    .path(
528                        "/ap/v2/user",
529                        PathItem::new(
530                            HttpMethod::Get,
531                            Operation::builder().response("200", Response::new("Get user success 2")),
532                        ),
533                    )
534                    .path(
535                        "/api/v2/user",
536                        PathItem::new(
537                            HttpMethod::Post,
538                            Operation::builder().response("200", Response::new("Get user success")),
539                        ),
540                    )
541                    .build(),
542            )
543            .components(
544                Components::builder().schema(
545                    "User2",
546                    Object::builder()
547                        .schema_type(Type::Object)
548                        .property("name", Object::builder().schema_type(Type::String)),
549                ),
550            )
551            .build();
552
553        api_1.merge(api_2);
554
555        assert_json_snapshot!(api_1, {
556            ".paths" => insta::sorted_redaction()
557        });
558    }
559
560    #[test]
561    fn merge_same_path_diff_methods() {
562        let mut api_1 = OpenApi::new(
563            Info::new("Api", "v1"),
564            Paths::builder()
565                .path(
566                    "/api/v1/user",
567                    PathItem::new(
568                        HttpMethod::Get,
569                        Operation::builder().response("200", Response::new("Get user success 1")),
570                    ),
571                )
572                .extensions(Extensions::from_iter([("x-v1-api", true)]))
573                .build(),
574        );
575
576        let api_2 = OpenApi::builder()
577            .info(Info::new("Api", "v2"))
578            .paths(
579                Paths::builder()
580                    .path(
581                        "/api/v1/user",
582                        PathItem::new(
583                            HttpMethod::Get,
584                            Operation::builder().response("200", Response::new("This will not get added")),
585                        ),
586                    )
587                    .path(
588                        "/api/v1/user",
589                        PathItem::new(
590                            HttpMethod::Post,
591                            Operation::builder().response("200", Response::new("Post user success 1")),
592                        ),
593                    )
594                    .path(
595                        "/api/v2/user",
596                        PathItem::new(
597                            HttpMethod::Get,
598                            Operation::builder().response("200", Response::new("Get user success 2")),
599                        ),
600                    )
601                    .path(
602                        "/api/v2/user",
603                        PathItem::new(
604                            HttpMethod::Post,
605                            Operation::builder().response("200", Response::new("Post user success 2")),
606                        ),
607                    )
608                    .extensions(Extensions::from_iter([("x-random", "Value")])),
609            )
610            .components(
611                Components::builder().schema(
612                    "User2",
613                    Object::builder()
614                        .schema_type(Type::Object)
615                        .property("name", Object::builder().schema_type(Type::String)),
616                ),
617            )
618            .build();
619
620        api_1.merge(api_2);
621
622        assert_json_snapshot!(api_1, {
623            ".paths" => insta::sorted_redaction()
624        });
625    }
626
627    #[test]
628    fn test_nest_open_apis() {
629        let api = OpenApi::builder()
630            .paths(Paths::builder().path(
631                "/api/v1/status",
632                PathItem::new(HttpMethod::Get, Operation::builder().description("Get status")),
633            ))
634            .build();
635
636        let user_api = OpenApi::builder()
637            .paths(
638                Paths::builder()
639                    .path(
640                        "/",
641                        PathItem::new(HttpMethod::Get, Operation::builder().description("Get user details").build()),
642                    )
643                    .path("/foo", PathItem::new(HttpMethod::Post, Operation::builder().build())),
644            )
645            .build();
646
647        let nest_merged = api.nest("/api/v1/user", user_api);
648        let value = serde_json::to_value(nest_merged).expect("should serialize as json");
649        let paths = value.pointer("/paths").expect("paths should exits in openapi");
650
651        assert_json_snapshot!(paths);
652    }
653
654    #[test]
655    fn openapi_custom_extension() {
656        let mut api = OpenApi::builder().build();
657        let extensions = api.extensions.get_or_insert(Default::default());
658        extensions.insert(
659            String::from("x-tagGroup"),
660            String::from("anything that serializes to Json").into(),
661        );
662
663        assert_json_snapshot!(api);
664    }
665}