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