diff --git a/Cargo.toml b/Cargo.toml index 4aa9617..f8cda04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ description = "This crate provides data structures that represent the OpenAPI v3 serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" indexmap = {version = "1.0", features = ["serde-1"]} +schemars = "0.8.7" [dev-dependencies] newline-converter = "0.2.0" diff --git a/README.md b/README.md index 95104e8..659d166 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OpenAPI v3 ![example workflow](https://github.com/glademiller/openapiv3/actions/workflows/rust.yml/badge.svg) -This crate provides data structures that represent the [OpenAPI v3.0.x specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md). Note this does not cover OpenAPI v3.1 which was an incompatible change. +This crate provides data structures that represent the [OpenAPI v3.0.x](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) and [OpenAPI v3.1.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md) specifications. ## Example @@ -38,4 +38,4 @@ This crate is licensed under either of ## Contribution -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/fixtures/non-oauth-scopes.yaml b/fixtures/non-oauth-scopes.yaml new file mode 100644 index 0000000..e757452 --- /dev/null +++ b/fixtures/non-oauth-scopes.yaml @@ -0,0 +1,19 @@ +openapi: 3.1.0 +info: + title: Non-oAuth Scopes example + version: 1.0.0 +paths: + /users: + get: + security: + - bearerAuth: + - 'read:users' + - 'public' +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: jwt + description: 'note: non-oauth scopes are not defined at the securityScheme level' + diff --git a/fixtures/webhook-example.yaml b/fixtures/webhook-example.yaml new file mode 100644 index 0000000..44fc73a --- /dev/null +++ b/fixtures/webhook-example.yaml @@ -0,0 +1,34 @@ +openapi: 3.1.0 +info: + title: Webhook Example + version: 1.0.0 +# Since OAS 3.1.0 the paths element isn't necessary. Now a valid OpenAPI Document can describe only paths, webhooks, or even only reusable components +webhooks: + # Each webhook needs a name + newPet: + # This is a Path Item Object, the only difference is that the request is initiated by the API provider + post: + requestBody: + description: Information about a new pet in the system + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully + +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string diff --git a/src/lib.rs b/src/lib.rs index 611d974..cec4261 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,57 +1,17 @@ -mod callback; -mod components; -mod contact; -mod discriminator; -mod encoding; -mod example; -mod external_documentation; -mod header; -mod info; -mod license; -mod link; -mod media_type; -mod openapi; -mod operation; -mod parameter; -mod paths; -mod reference; -mod request_body; -mod responses; -mod schema; -mod security_requirement; -mod security_scheme; -mod server; -mod server_variable; -mod status_code; -mod tag; mod util; -mod variant_or; +pub mod v3_0; +pub mod v3_1; -pub use self::callback::*; -pub use self::components::*; -pub use self::contact::*; -pub use self::discriminator::*; -pub use self::encoding::*; -pub use self::example::*; -pub use self::external_documentation::*; -pub use self::header::*; -pub use self::info::*; -pub use self::license::*; -pub use self::link::*; -pub use self::media_type::*; -pub use self::openapi::*; -pub use self::operation::*; -pub use self::parameter::*; -pub use self::paths::*; -pub use self::reference::*; -pub use self::request_body::*; -pub use self::responses::*; -pub use self::schema::*; -pub use self::security_requirement::*; -pub use self::security_scheme::*; -pub use self::server::*; -pub use self::server_variable::*; -pub use self::status_code::*; -pub use self::tag::*; pub use self::util::*; -pub use self::variant_or::*; +pub use schemars; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "openapi")] +pub enum OpenAPI { + #[serde(rename = "3.0.0", alias = "3.0.1", alias = "3.0.2", alias = "3.0.3")] + Version30(v3_0::OpenAPI), + #[serde(rename = "3.1.0")] + Version31(v3_1::OpenAPI), +} diff --git a/src/callback.rs b/src/v3_0/callback.rs similarity index 58% rename from src/callback.rs rename to src/v3_0/callback.rs index 0e2ee8c..e3ed0b4 100644 --- a/src/callback.rs +++ b/src/v3_0/callback.rs @@ -1,9 +1,10 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; /// A map of possible out-of band callbacks related to the parent operation. /// Each value in the map is a Path Item Object that describes a set of -/// requests that may be initiated by the API provider and the expected responses. -/// The key value used to identify the callback object is an expression, -/// evaluated at runtime, that identifies a URL to use for the callback operation. +/// requests that may be initiated by the API provider and the expected +/// responses. The key value used to identify the callback object is an +/// expression, evaluated at runtime, that identifies a URL to use for the +/// callback operation. pub type Callback = IndexMap; diff --git a/src/components.rs b/src/v3_0/components.rs similarity index 99% rename from src/components.rs rename to src/v3_0/components.rs index 359682d..c1e06be 100644 --- a/src/components.rs +++ b/src/v3_0/components.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/contact.rs b/src/v3_0/contact.rs similarity index 100% rename from src/contact.rs rename to src/v3_0/contact.rs diff --git a/src/discriminator.rs b/src/v3_0/discriminator.rs similarity index 70% rename from src/discriminator.rs rename to src/v3_0/discriminator.rs index 638ca4c..f8174f4 100644 --- a/src/discriminator.rs +++ b/src/v3_0/discriminator.rs @@ -1,11 +1,11 @@ use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -/// When request bodies or response payloads may be one of a number of different schemas, -/// a discriminator object can be used to aid in serialization, deserialization, -/// and validation. The discriminator is a specific object in a schema which is -/// used to inform the consumer of the specification of an alternative schema based -/// on the value associated with it. +/// When request bodies or response payloads may be one of a number of different +/// schemas, a discriminator object can be used to aid in serialization, +/// deserialization, and validation. The discriminator is a specific object in a +/// schema which is used to inform the consumer of the specification of an +/// alternative schema based on the value associated with it. /// /// When using the discriminator, inline schemas will not be considered. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] @@ -14,7 +14,8 @@ pub struct Discriminator { /// REQUIRED. The name of the property in the payload that /// will hold the discriminator value. pub property_name: String, - /// An object to hold mappings between payload values and schema names or references. + /// An object to hold mappings between payload values and schema names or + /// references. #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub mapping: IndexMap, /// Inline extensions to this object. diff --git a/src/encoding.rs b/src/v3_0/encoding.rs similarity index 90% rename from src/encoding.rs rename to src/v3_0/encoding.rs index 0edb7fb..b886825 100644 --- a/src/encoding.rs +++ b/src/v3_0/encoding.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -13,7 +13,8 @@ pub struct Encoding { /// for object - application/json; /// for array – the default is defined based on the inner type. /// The value can be a specific media type (e.g. application/json), - /// a wildcard media type (e.g. image/*), or a comma-separated list of the two types. + /// a wildcard media type (e.g. image/*), or a comma-separated list of the + /// two types. pub content_type: Option, /// A map allowing additional information to be provided as headers, /// for example Content-Disposition. Content-Type is described separately @@ -36,13 +37,15 @@ pub struct Encoding { /// SHALL be ignored if the request body media type is /// not application/x-www-form-urlencoded. /// - /// In this Library this value defaults to false always despite the specification. + /// In this Library this value defaults to false always despite the + /// specification. #[serde(default, skip_serializing_if = "is_false")] pub explode: bool, /// Determines whether the parameter value SHOULD allow reserved characters, - /// as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding. - /// The default value is false. This property SHALL be ignored if the request - /// body media type is not application/x-www-form-urlencoded. + /// as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without + /// percent-encoding. The default value is false. This property SHALL be + /// ignored if the request body media type is not + /// application/x-www-form-urlencoded. #[serde(default, skip_serializing_if = "is_false")] pub allow_reserved: bool, /// Inline extensions to this object. diff --git a/src/example.rs b/src/v3_0/example.rs similarity index 100% rename from src/example.rs rename to src/v3_0/example.rs diff --git a/src/external_documentation.rs b/src/v3_0/external_documentation.rs similarity index 100% rename from src/external_documentation.rs rename to src/v3_0/external_documentation.rs diff --git a/src/header.rs b/src/v3_0/header.rs similarity index 93% rename from src/header.rs rename to src/v3_0/header.rs index dcb47aa..1650d34 100644 --- a/src/header.rs +++ b/src/v3_0/header.rs @@ -1,12 +1,14 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -/// The Header Object follows the structure of the Parameter Object with the following changes: +/// The Header Object follows the structure of the Parameter Object with the +/// following changes: /// /// 1) name MUST NOT be specified, it is given in the corresponding headers map. /// 2) in MUST NOT be specified, it is implicitly in header. -/// 3) All traits that are affected by the location MUST be applicable to a location of header (for example, style). +/// 3) All traits that are affected by the location MUST be applicable to a +/// location of header (for example, style). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Header { diff --git a/src/info.rs b/src/v3_0/info.rs similarity index 96% rename from src/info.rs rename to src/v3_0/info.rs index 6911440..667abe0 100644 --- a/src/info.rs +++ b/src/v3_0/info.rs @@ -1,10 +1,11 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; /// The object provides metadata about the API. /// The metadata MAY be used by the clients if needed, -/// and MAY be presented in editing or documentation generation tools for convenience. +/// and MAY be presented in editing or documentation generation tools for +/// convenience. #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct Info { /// REQUIRED. The title of the application. diff --git a/src/license.rs b/src/v3_0/license.rs similarity index 100% rename from src/license.rs rename to src/v3_0/license.rs diff --git a/src/link.rs b/src/v3_0/link.rs similarity index 98% rename from src/link.rs rename to src/v3_0/link.rs index 31cbc76..46e0283 100644 --- a/src/link.rs +++ b/src/v3_0/link.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize}; /// traversal mechanism between responses and other operations. /// /// Unlike dynamic links (i.e. links provided in the response payload), -/// the OAS linking mechanism does not require link information in the runtime response. +/// the OAS linking mechanism does not require link information in the runtime +/// response. /// /// For computing links, and providing instructions to execute them, /// a runtime expression is used for accessing values in an operation diff --git a/src/media_type.rs b/src/v3_0/media_type.rs similarity index 97% rename from src/media_type.rs rename to src/v3_0/media_type.rs index 76c7ba1..bc33b27 100644 --- a/src/media_type.rs +++ b/src/v3_0/media_type.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/v3_0/mod.rs b/src/v3_0/mod.rs new file mode 100644 index 0000000..12eea4f --- /dev/null +++ b/src/v3_0/mod.rs @@ -0,0 +1,55 @@ +mod callback; +mod components; +mod contact; +mod discriminator; +mod encoding; +mod example; +mod external_documentation; +mod header; +mod info; +mod license; +mod link; +mod media_type; +mod openapi; +mod operation; +mod parameter; +mod paths; +mod reference; +mod request_body; +mod responses; +mod schema; +mod security_requirement; +mod security_scheme; +mod server; +mod server_variable; +mod status_code; +mod tag; +mod variant_or; + +pub use self::callback::*; +pub use self::components::*; +pub use self::contact::*; +pub use self::discriminator::*; +pub use self::encoding::*; +pub use self::example::*; +pub use self::external_documentation::*; +pub use self::header::*; +pub use self::info::*; +pub use self::license::*; +pub use self::link::*; +pub use self::media_type::*; +pub use self::openapi::*; +pub use self::operation::*; +pub use self::parameter::*; +pub use self::paths::*; +pub use self::reference::*; +pub use self::request_body::*; +pub use self::responses::*; +pub use self::schema::*; +pub use self::security_requirement::*; +pub use self::security_scheme::*; +pub use self::server::*; +pub use self::server_variable::*; +pub use self::status_code::*; +pub use self::tag::*; +pub use self::variant_or::*; diff --git a/src/openapi.rs b/src/v3_0/openapi.rs similarity index 87% rename from src/openapi.rs rename to src/v3_0/openapi.rs index 2a9ad26..6d3d190 100644 --- a/src/openapi.rs +++ b/src/v3_0/openapi.rs @@ -1,15 +1,9 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct OpenAPI { - /// REQUIRED. This string MUST be the semantic version number of the - /// OpenAPI Specification version that the OpenAPI document uses. - /// The openapi field SHOULD be used by tooling specifications and - /// clients to interpret the OpenAPI document. This is not related to - /// the API info.version string. - pub openapi: String, /// REQUIRED. Provides metadata about the API. /// The metadata MAY be used by tooling as required. pub info: Info, @@ -28,8 +22,8 @@ pub struct OpenAPI { /// The list of values includes alternative security requirement objects /// that can be used. Only one of the security requirement objects need to /// be satisfied to authorize a request. Individual operations can override - /// this definition. Global security settings may be overridden on a per-path - /// basis. + /// this definition. Global security settings may be overridden on a + /// per-path basis. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub security: Option>, diff --git a/src/operation.rs b/src/v3_0/operation.rs similarity index 91% rename from src/operation.rs rename to src/v3_0/operation.rs index b0fd458..03c1c31 100644 --- a/src/operation.rs +++ b/src/v3_0/operation.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -52,11 +52,12 @@ pub struct Operation { /// Declares this operation to be deprecated.Default value is false. #[serde(default, skip_serializing_if = "is_false")] pub deprecated: bool, - /// A declaration of which security mechanisms can be used for this operation. - /// The list of values includes alternative security requirement objects that can - /// be used. Only one of the security requirement objects need to be satisfied to - /// authorize a request. This definition overrides any declared top-level security. - /// To remove a top-level security declaration, an empty array can be used. + /// A declaration of which security mechanisms can be used for this + /// operation. The list of values includes alternative security + /// requirement objects that can be used. Only one of the security + /// requirement objects need to be satisfied to authorize a request. + /// This definition overrides any declared top-level security. To remove + /// a top-level security declaration, an empty array can be used. #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub security: Option>, @@ -73,7 +74,7 @@ pub struct Operation { #[cfg(test)] mod tests { - use crate::{Operation, ReferenceOr, Responses, StatusCode}; + use crate::v3_0::{Operation, ReferenceOr, Responses, StatusCode}; use indexmap::IndexMap; use serde_yaml::from_str; diff --git a/src/parameter.rs b/src/v3_0/parameter.rs similarity index 99% rename from src/parameter.rs rename to src/v3_0/parameter.rs index be81732..fd1c71d 100644 --- a/src/parameter.rs +++ b/src/v3_0/parameter.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/paths.rs b/src/v3_0/paths.rs similarity index 99% rename from src/paths.rs rename to src/v3_0/paths.rs index 43513bb..e662b60 100644 --- a/src/paths.rs +++ b/src/v3_0/paths.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize}; diff --git a/src/reference.rs b/src/v3_0/reference.rs similarity index 81% rename from src/reference.rs rename to src/v3_0/reference.rs index cf24b78..1749bba 100644 --- a/src/reference.rs +++ b/src/v3_0/reference.rs @@ -22,12 +22,14 @@ impl ReferenceOr { /// Converts this [ReferenceOr] to the item inside, if it exists. /// - /// The return value will be [Option::Some] if this was a [ReferenceOr::Item] or [Option::None] if this was a [ReferenceOr::Reference]. + /// The return value will be [Option::Some] if this was a + /// [ReferenceOr::Item] or [Option::None] if this was a + /// [ReferenceOr::Reference]. /// /// # Examples /// /// ``` - /// # use openapiv3::ReferenceOr; + /// # use openapiv3::v3_0::ReferenceOr; /// /// let i = ReferenceOr::Item(1); /// assert_eq!(i.into_item(), Some(1)); @@ -42,14 +44,17 @@ impl ReferenceOr { } } - /// Returns a reference to to the item inside this [ReferenceOr], if it exists. + /// Returns a reference to to the item inside this [ReferenceOr], if it + /// exists. /// - /// The return value will be [Option::Some] if this was a [ReferenceOr::Item] or [Option::None] if this was a [ReferenceOr::Reference]. + /// The return value will be [Option::Some] if this was a + /// [ReferenceOr::Item] or [Option::None] if this was a + /// [ReferenceOr::Reference]. /// /// # Examples /// /// ``` - /// # use openapiv3::ReferenceOr; + /// # use openapiv3::v3_0::ReferenceOr; /// /// let i = ReferenceOr::Item(1); /// assert_eq!(i.as_item(), Some(&1)); diff --git a/src/request_body.rs b/src/v3_0/request_body.rs similarity index 97% rename from src/request_body.rs rename to src/v3_0/request_body.rs index 205ab4c..dd6cf3a 100644 --- a/src/request_body.rs +++ b/src/v3_0/request_body.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/responses.rs b/src/v3_0/responses.rs similarity index 97% rename from src/responses.rs rename to src/v3_0/responses.rs index 5abbe0e..07f0ed6 100644 --- a/src/responses.rs +++ b/src/v3_0/responses.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize}; @@ -79,7 +79,7 @@ where mod tests { use serde_json::json; - use crate::{ReferenceOr, Response, Responses, StatusCode}; + use crate::v3_0::{ReferenceOr, Response, Responses, StatusCode}; #[test] fn test_responses() { diff --git a/src/schema.rs b/src/v3_0/schema.rs similarity index 99% rename from src/schema.rs rename to src/v3_0/schema.rs index 75995d3..a021616 100644 --- a/src/schema.rs +++ b/src/v3_0/schema.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{util::*, v3_0::*}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -243,7 +243,7 @@ pub enum StringFormat { mod tests { use serde_json::json; - use crate::{AnySchema, Schema, SchemaData, SchemaKind}; + use crate::v3_0::{AnySchema, Schema, SchemaData, SchemaKind}; #[test] fn test_schema_with_extensions() { diff --git a/src/security_requirement.rs b/src/v3_0/security_requirement.rs similarity index 100% rename from src/security_requirement.rs rename to src/v3_0/security_requirement.rs diff --git a/src/security_scheme.rs b/src/v3_0/security_scheme.rs similarity index 99% rename from src/security_scheme.rs rename to src/v3_0/security_scheme.rs index f362cf6..e2bf6ae 100644 --- a/src/security_scheme.rs +++ b/src/v3_0/security_scheme.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; /// Defines a security scheme that can be used by the operations. /// Supported schemes are HTTP authentication, an API key (either as a /// header or as a query parameter), OAuth2's common flows (implicit, password, -/// application and access code) as defined in RFC6749, and OpenID Connect Discovery. +/// application and access code) as defined in RFC6749, and OpenID Connect +/// Discovery. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum SecurityScheme { diff --git a/src/server.rs b/src/v3_0/server.rs similarity index 98% rename from src/server.rs rename to src/v3_0/server.rs index 6a03858..5e01f59 100644 --- a/src/server.rs +++ b/src/v3_0/server.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/server_variable.rs b/src/v3_0/server_variable.rs similarity index 100% rename from src/server_variable.rs rename to src/v3_0/server_variable.rs diff --git a/src/status_code.rs b/src/v3_0/status_code.rs similarity index 100% rename from src/status_code.rs rename to src/v3_0/status_code.rs diff --git a/src/tag.rs b/src/v3_0/tag.rs similarity index 97% rename from src/tag.rs rename to src/v3_0/tag.rs index bae42bf..82c4101 100644 --- a/src/tag.rs +++ b/src/v3_0/tag.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::v3_0::*; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; diff --git a/src/variant_or.rs b/src/v3_0/variant_or.rs similarity index 100% rename from src/variant_or.rs rename to src/v3_0/variant_or.rs diff --git a/src/v3_1/callback.rs b/src/v3_1/callback.rs new file mode 100644 index 0000000..9e03e8a --- /dev/null +++ b/src/v3_1/callback.rs @@ -0,0 +1,10 @@ +use crate::v3_1::*; +use indexmap::IndexMap; + +/// A map of possible out-of band callbacks related to the parent operation. +/// Each value in the map is a Path Item Object that describes a set of +/// requests that may be initiated by the API provider and the expected +/// responses. The key value used to identify the callback object is an +/// expression, evaluated at runtime, that identifies a URL to use for the +/// callback operation. +pub type Callback = IndexMap>; diff --git a/src/v3_1/components.rs b/src/v3_1/components.rs new file mode 100644 index 0000000..a9d7d44 --- /dev/null +++ b/src/v3_1/components.rs @@ -0,0 +1,45 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Holds a set of reusable objects for different aspects of the OAS. +/// All objects defined within the components object will have no effect +/// on the API unless they are explicitly referenced from properties +/// outside the components object. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Components { + /// An object to hold reusable Security Scheme Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub security_schemes: IndexMap>, + /// An object to hold reusable Response Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub responses: IndexMap>, + /// An object to hold reusable Parameter Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub parameters: IndexMap>, + /// An object to hold reusable Example Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub examples: IndexMap>, + /// An object to hold reusable Request Body Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub request_bodies: IndexMap>, + /// An object to hold reusable Header Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub headers: IndexMap>, + /// An object to hold reusable Schema Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub schemas: IndexMap, + /// An object to hold reusable Link Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub links: IndexMap>, + /// An object to hold reusable Callback Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub callbacks: IndexMap>, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + /// An object to hold reusable Path Item Objects. + pub path_items: IndexMap>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/contact.rs b/src/v3_1/contact.rs new file mode 100644 index 0000000..fefa126 --- /dev/null +++ b/src/v3_1/contact.rs @@ -0,0 +1,21 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Contact information for the exposed API. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Contact { + /// The identifying name of the contact person/organization. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// The URL pointing to the contact information. + /// This MUST be in the format of a URL. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// The email address of the contact person/organization. + /// This MUST be in the format of an email address. + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/discriminator.rs b/src/v3_1/discriminator.rs new file mode 100644 index 0000000..f8174f4 --- /dev/null +++ b/src/v3_1/discriminator.rs @@ -0,0 +1,24 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// When request bodies or response payloads may be one of a number of different +/// schemas, a discriminator object can be used to aid in serialization, +/// deserialization, and validation. The discriminator is a specific object in a +/// schema which is used to inform the consumer of the specification of an +/// alternative schema based on the value associated with it. +/// +/// When using the discriminator, inline schemas will not be considered. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Discriminator { + /// REQUIRED. The name of the property in the payload that + /// will hold the discriminator value. + pub property_name: String, + /// An object to hold mappings between payload values and schema names or + /// references. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub mapping: IndexMap, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/encoding.rs b/src/v3_1/encoding.rs new file mode 100644 index 0000000..5b7da6f --- /dev/null +++ b/src/v3_1/encoding.rs @@ -0,0 +1,59 @@ +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// A single encoding definition applied to a single schema property. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Encoding { + /// The Content-Type for encoding a specific property. + /// Default value depends on the property type: + /// for object - application/json; + /// for array – the default is defined based on the inner type. + /// for all other cases the default is `application/octet-stream`. + /// The value can be a specific media type (e.g. application/json), + /// a wildcard media type (e.g. image/*), or a comma-separated list of the + /// two types. + pub content_type: Option, + /// A map allowing additional information to be provided as headers, + /// for example Content-Disposition. Content-Type is described separately + /// and SHALL be ignored in this section. This property SHALL be ignored + /// if the request body media type is not a multipart. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub headers: IndexMap>, + /// Describes how a specific property value will be serialized depending + /// on its type. See Parameter Object for details on the style property. + /// The behavior follows the same values as query parameters, including + /// default values. This property SHALL be ignored if the request body + /// media type is not application/x-www-form-urlencoded or + /// multipart/form-data. If a value is explicitly defined, then the value of + /// `contentType` (implicit or explicit) SHALL be ignored. + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + /// When this is true, property values of type array or object generate + /// separate parameters for each value of the array, or key-value-pair + /// of the map. For other types of properties this property has no effect. + /// When style is form, the default value is true. + /// For all other styles, the default value is false. This property + /// SHALL be ignored if the request body media type is + /// not application/x-www-form-urlencoded or multipart/form-data. If a value + /// is explicitly defined, then the value of `contentType` (implicit or + /// explicit) SHALL be ignored. + /// + /// In this Library this value defaults to false always despite the + /// specification. + #[serde(default, skip_serializing_if = "is_false")] + pub explode: bool, + /// Determines whether the parameter value SHOULD allow reserved characters, + /// as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without + /// percent-encoding. The default value is false. This property SHALL be + /// ignored if the request body media type is not + /// application/x-www-form-urlencoded or multipart/form-data. If a value is + /// explicitly defined, then the value of `contentType` (implicit or + /// explicit) SHALL be ignored. + #[serde(default, skip_serializing_if = "is_false")] + pub allow_reserved: bool, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/example.rs b/src/v3_1/example.rs new file mode 100644 index 0000000..ddac208 --- /dev/null +++ b/src/v3_1/example.rs @@ -0,0 +1,30 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Example { + /// Short description for the example. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// Long description for the example. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Embedded literal example. The `value` field and `externalValue` + /// field are mutually exclusive. To represent examples of + /// media types that cannot naturally represented in JSON or YAML, + /// use a string value to contain the example, escaping where necessary. + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// A URI that points to the literal example. + /// This provides the capability to reference examples that cannot + /// easily be included in JSON or YAML documents. The `value` field and + /// `externalValue` field are mutually exclusive. See the rules for + /// resolving Relative References. + #[serde(rename = "externalValue")] + #[serde(skip_serializing_if = "Option::is_none")] + pub external_value: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/external_documentation.rs b/src/v3_1/external_documentation.rs new file mode 100644 index 0000000..441866a --- /dev/null +++ b/src/v3_1/external_documentation.rs @@ -0,0 +1,17 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Allows referencing an external resource for extended documentation. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct ExternalDocumentation { + /// A description of the target documentation. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// REQUIRED. The URL for the target documentation. + /// This MUST be in the format of a URL. + pub url: String, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/header.rs b/src/v3_1/header.rs new file mode 100644 index 0000000..e1751e9 --- /dev/null +++ b/src/v3_1/header.rs @@ -0,0 +1,42 @@ +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// The Header Object follows the structure of the Parameter Object with the +/// following changes: +/// +/// 1) name MUST NOT be specified, it is given in the corresponding headers map. +/// 2) in MUST NOT be specified, it is implicitly in header. +/// 3) All traits that are affected by the location MUST be applicable to a +/// location of header (for example, style). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Header { + /// A brief description of the parameter. This could + /// contain examples of use. CommonMark syntax MAY be + /// used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default)] + pub style: HeaderStyle, + /// Determines whether this parameter is mandatory. + /// If the parameter location is "path", this property + /// is REQUIRED and its value MUST be true. Otherwise, + /// the property MAY be included and its default value + /// is false. + #[serde(default, skip_serializing_if = "is_false")] + pub required: bool, + /// Specifies that a parameter is deprecated and SHOULD + /// be transitioned out of usage. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + #[serde(flatten)] + pub format: ParameterSchemaOrContent, + #[serde(skip_serializing_if = "Option::is_none")] + pub example: Option, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub examples: IndexMap>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/info.rs b/src/v3_1/info.rs new file mode 100644 index 0000000..b47ea67 --- /dev/null +++ b/src/v3_1/info.rs @@ -0,0 +1,36 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// The object provides metadata about the API. +/// The metadata MAY be used by the clients if needed, +/// and MAY be presented in editing or documentation generation tools for +/// convenience. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Info { + /// REQUIRED. The title of the application. + pub title: String, + /// A short summary of the API. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// A description of the API. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A URL to the Terms of Service for the API. + /// This MUST be in the format of a URL. + #[serde(rename = "termsOfService", skip_serializing_if = "Option::is_none")] + pub terms_of_service: Option, + /// The contact information for the exposed API. + #[serde(skip_serializing_if = "Option::is_none")] + pub contact: Option, + /// The license information for the exposed API. + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, + /// REQUIRED. The version of the OpenAPI document (which is distinct from + /// the OpenAPI Specification version or the API implementation version). + pub version: String, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/license.rs b/src/v3_1/license.rs new file mode 100644 index 0000000..c959cde --- /dev/null +++ b/src/v3_1/license.rs @@ -0,0 +1,19 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// License information for the exposed API. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct License { + /// REQUIRED. The license name used for the API. + pub name: String, + /// An [SPDX](https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60) license expression for the API. The `identifier` field is mutually exclusive of the `url` field. + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier: Option, + /// A URL to the license used for the API. This MUST be in the form of a + /// URL. The `url` field is mutually exclusive of the `identifier` field. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/link.rs b/src/v3_1/link.rs new file mode 100644 index 0000000..921dafc --- /dev/null +++ b/src/v3_1/link.rs @@ -0,0 +1,62 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// The Link object represents a possible design-time link for a response. +/// The presence of a link does not guarantee the caller's ability to +/// successfully invoke it, rather it provides a known relationship and +/// traversal mechanism between responses and other operations. +/// +/// Unlike dynamic links (i.e. links provided in the response payload), +/// the OAS linking mechanism does not require link information in the runtime +/// response. +/// +/// For computing links, and providing instructions to execute them, +/// a runtime expression is used for accessing values in an operation +/// and using them as parameters while invoking the linked operation. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Link { + /// A description of the link. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Either a operationRef or operationId + #[serde(flatten)] + pub operation: LinkOperation, + /// A literal value or {expression} to use as a request body + /// when calling the target operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_body: Option, + /// A map representing parameters to pass to an operation + /// as specified with operationId or identified via operationRef. + /// The key is the parameter name to be used, whereas the value + /// can be a constant or an expression to be evaluated and passed + /// to the linked operation. The parameter name can be qualified + /// using the parameter location [{in}.]{name} for operations + /// that use the same parameter name in different locations (e.g. path.id). + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub parameters: IndexMap, + /// A server object to be used by the target operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub server: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum LinkOperation { + /// A relative or absolute reference to an OAS operation. + /// This field is mutually exclusive of the operationId field, + /// and MUST point to an Operation Object. Relative operationRef + /// values MAY be used to locate an existing Operation Object + /// in the OpenAPI definition. See the rules for resolving Relative + /// References. + OperationRef(String), + /// The name of an existing, resolvable OAS operation, + /// as defined with a unique operationId. This field is + /// mutually exclusive of the operationRef field. + OperationId(String), +} diff --git a/src/v3_1/media_type.rs b/src/v3_1/media_type.rs new file mode 100644 index 0000000..59f0e0e --- /dev/null +++ b/src/v3_1/media_type.rs @@ -0,0 +1,18 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct MediaType { + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub example: Option, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub examples: IndexMap>, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub encoding: IndexMap, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/mod.rs b/src/v3_1/mod.rs new file mode 100644 index 0000000..12eea4f --- /dev/null +++ b/src/v3_1/mod.rs @@ -0,0 +1,55 @@ +mod callback; +mod components; +mod contact; +mod discriminator; +mod encoding; +mod example; +mod external_documentation; +mod header; +mod info; +mod license; +mod link; +mod media_type; +mod openapi; +mod operation; +mod parameter; +mod paths; +mod reference; +mod request_body; +mod responses; +mod schema; +mod security_requirement; +mod security_scheme; +mod server; +mod server_variable; +mod status_code; +mod tag; +mod variant_or; + +pub use self::callback::*; +pub use self::components::*; +pub use self::contact::*; +pub use self::discriminator::*; +pub use self::encoding::*; +pub use self::example::*; +pub use self::external_documentation::*; +pub use self::header::*; +pub use self::info::*; +pub use self::license::*; +pub use self::link::*; +pub use self::media_type::*; +pub use self::openapi::*; +pub use self::operation::*; +pub use self::parameter::*; +pub use self::paths::*; +pub use self::reference::*; +pub use self::request_body::*; +pub use self::responses::*; +pub use self::schema::*; +pub use self::security_requirement::*; +pub use self::security_scheme::*; +pub use self::server::*; +pub use self::server_variable::*; +pub use self::status_code::*; +pub use self::tag::*; +pub use self::variant_or::*; diff --git a/src/v3_1/openapi.rs b/src/v3_1/openapi.rs new file mode 100644 index 0000000..bd058f0 --- /dev/null +++ b/src/v3_1/openapi.rs @@ -0,0 +1,81 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct OpenAPI { + /// REQUIRED. Provides metadata about the API. + /// The metadata MAY be used by tooling as required. + pub info: Info, + /// The default value for the `$schema` keyword within Schema Objects + /// contained within this OAS document. This MUST be in the form of a URI. + #[serde(skip_serializing_if = "Option::is_none")] + pub json_schema_dialect: Option, + /// An array of Server Objects, which provide connectivity information to a + /// target server. If the servers property is not provided, or is an empty + /// array, the default value would be a Server Object with a url value of /. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub servers: Vec, + /// The available paths and operations for the API. + #[serde(skip_serializing_if = "Option::is_none")] + pub paths: Option, + /// The incoming webhooks that MAY be received as part of this API and that + /// the API consumer MAY choose to implement. Closely related to the + /// `callbacks` feature, this section describes requests initiated other + /// than by an API call, for example by an out of band registration. The key + /// name is a unique string to refer to each webhook, while the (optionally + /// referenced) Path Item Object describes a request that may be initiated + /// by the API provider and the expected responses. + #[serde(default)] + #[serde(skip_serializing_if = "IndexMap::is_empty")] + pub webhooks: IndexMap>, + /// An element to hold various schemas for the document. + #[serde(skip_serializing_if = "Option::is_none")] + pub components: Option, + /// A declaration of which security mechanisms can be used across the API. + /// The list of values includes alternative security requirement objects + /// that can be used. Only one of the security requirement objects need to + /// be satisfied to authorize a request. Individual operations can override + /// this definition. Global security settings may be overridden on a + /// per-path basis. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub security: Option>, + /// A list of tags used by the document with additional metadata. + /// The order of the tags can be used to reflect on their order by the + /// parsing tools. Not all tags that are used by the Operation Object + /// must be declared. The tags that are not declared MAY be organized + /// randomly or based on the tool's logic. Each tag name in the list + /// MUST be unique. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + /// Additional external documentation. + #[serde(rename = "externalDocs")] + #[serde(skip_serializing_if = "Option::is_none")] + pub external_docs: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +impl OpenAPI { + /// Iterates through all [Operation]s in this API. + /// + /// The iterated items are tuples of `(&str, &str, &Operation)` containing + /// the path, method, and the operation. + /// + /// Path items containing `$ref`s are skipped. + pub fn operations(&self) -> impl Iterator { + self.paths.iter().flat_map(|paths| { + paths + .iter() + .filter_map(|(path, item)| item.as_item().map(|i| (path, i))) + .flat_map(|(path, item)| { + item.iter() + .map(move |(method, op)| (path.as_str(), method, op)) + }) + }) + } +} diff --git a/src/v3_1/operation.rs b/src/v3_1/operation.rs new file mode 100644 index 0000000..5c21ca7 --- /dev/null +++ b/src/v3_1/operation.rs @@ -0,0 +1,145 @@ +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Describes a single API operation on a path. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Operation { + /// A list of tags for API documentation control. + /// Tags can be used for logical grouping of operations + /// by resources or any other qualifier. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + /// A short summary of what the operation does. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// A verbose explanation of the operation behavior. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional external documentation for this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub external_docs: Option, + /// Unique string used to identify the operation. + /// The id MUST be unique among all operations described in the API. + /// Tools and libraries MAY use the operationId to uniquely identify + /// an operation, therefore, it is RECOMMENDED to follow common + /// programming naming conventions. + #[serde(skip_serializing_if = "Option::is_none")] + pub operation_id: Option, + /// A list of parameters that are applicable for this operation. + /// If a parameter is already defined at the Path Item, the new + /// definition will override it but can never remove it. + /// The list MUST NOT include duplicated parameters. A unique + /// parameter is defined by a combination of a name and location. + /// The list can use the Reference Object to link to parameters + /// that are defined at the OpenAPI Object's components/parameters. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub parameters: Vec>, + /// The request body applicable for this operation. + /// The requestBody is fully supported in HTTP methods + /// where the HTTP 1.1 specification RFC7231 has explicitly + /// defined semantics for request bodies. In other cases where + /// the HTTP spec is vague (such as + /// [GET](https://tools.ietf.org/html/rfc7231#section-4.3.1), + /// [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) and + /// [DELETE](https://tools.ietf.org/html/rfc7231#section-4.3.5)), + /// requestBody is permitted but does not have well-defined semantics and + /// SHOULD be avoided if possible. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_body: Option>, + /// The list of possible responses as they are returned + /// from executing this operation. + #[serde(skip_serializing_if = "Option::is_none")] + pub responses: Option, + /// Declares this operation to be deprecated.Default value is false. + #[serde(default, skip_serializing_if = "is_false")] + pub deprecated: bool, + /// A declaration of which security mechanisms can be used for this + /// operation. The list of values includes alternative security + /// requirement objects that can be used. Only one of the security + /// requirement objects need to be satisfied to authorize a request. + /// This definition overrides any declared top-level security. To remove + /// a top-level security declaration, an empty array can be used. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub security: Option>, + /// An alternative server array to service this operation. + /// If an alternative server object is specified at the + /// Path Item Object or Root level, it will be overridden by this value. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub servers: Vec, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +#[cfg(test)] +mod tests { + use crate::v3_1::{Operation, ReferenceOr, Response, Responses, StatusCode}; + use indexmap::IndexMap; + use serde_yaml::from_str; + + fn response(desc: &str) -> ReferenceOr { + ReferenceOr::Item(Response { + description: desc.to_owned(), + ..Default::default() + }) + } + + #[test] + fn deserialize_responses() { + assert_eq!( + Operation { + responses: Some(Responses { + default: None, + responses: { + let mut map = IndexMap::new(); + map.insert(StatusCode::Code(200), response("test")); + map + }, + ..Default::default() + }), + ..Default::default() + }, + from_str("{ responses: { 200: { description: 'test' } } }").unwrap(), + ); + + assert_eq!( + Operation { + responses: Some(Responses { + default: None, + responses: { + let mut map = IndexMap::new(); + map.insert(StatusCode::Code(666), response("demo")); + map + }, + ..Default::default() + }), + ..Default::default() + }, + from_str("{ responses: { \"666\": { description: 'demo' } } }").unwrap(), + ); + + assert_eq!( + Operation { + responses: Some(Responses { + default: Some(response("def")), + responses: { + let mut map = IndexMap::new(); + map.insert(StatusCode::Code(666), response("demo")); + map.insert(StatusCode::Code(418), response("demo")); + map + }, + ..Default::default() + }), + ..Default::default() + }, + from_str("{ responses: { default: { description: 'def' }, \"666\": { description: 'demo' }, 418: { description: 'demo' } } }").unwrap(), + ); + } +} diff --git a/src/v3_1/parameter.rs b/src/v3_1/parameter.rs new file mode 100644 index 0000000..59adb11 --- /dev/null +++ b/src/v3_1/parameter.rs @@ -0,0 +1,231 @@ +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Describes a single operation parameter. +/// +/// A unique parameter is defined by a combination of a name and location. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ParameterData { + /// REQUIRED. The name of the parameter. Parameter names are case sensitive. + /// If in is "path", the name field MUST correspond to the associated path + /// segment from the path field in the Paths Object. See Path Templating for + /// further information. + /// + /// If in is "header" and the name field is "Accept", "Content-Type" or + /// "Authorization", the parameter definition SHALL be ignored. + /// + /// For all other cases, the name corresponds to the parameter name + /// used by the in property. + pub name: String, + /// A brief description of the parameter. This could + /// contain examples of use. CommonMark syntax MAY be + /// used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Determines whether this parameter is mandatory. + /// If the parameter location is "path", this property + /// is REQUIRED and its value MUST be true. Otherwise, + /// the property MAY be included and its default value + /// is false. + #[serde(default, skip_serializing_if = "is_false")] + pub required: bool, + /// Specifies that a parameter is deprecated and SHOULD + /// be transitioned out of usage. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + #[serde(flatten)] + pub format: ParameterSchemaOrContent, + #[serde(skip_serializing_if = "Option::is_none")] + pub example: Option, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub examples: IndexMap>, + #[serde(skip_serializing_if = "Option::is_none")] + pub explode: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ParameterSchemaOrContent { + /// The schema defining the type used for the parameter. + Schema(Schema), + /// A map containing the representations for the parameter. The key is the + /// media type and the value describes it. The map MUST only contain one + /// entry. + Content(Content), +} + +pub type Content = IndexMap; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "in", rename_all = "camelCase")] +pub enum Parameter { + Query { + #[serde(flatten)] + parameter_data: ParameterData, + /// Determines whether the parameter value SHOULD allow reserved + /// characters, as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included + /// without percent-encoding. This property only applies to parameters + /// with an in value of query. The default value is false. + #[serde(default, skip_serializing_if = "is_false")] + allow_reserved: bool, + /// Describes how the parameter value will be serialized depending on + /// the type of the parameter value. Default values (based on value of + /// in): for query - form; for path - simple; for header - simple; for + /// cookie - form. + #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] + style: QueryStyle, + /// Sets the ability to pass empty-valued parameters. This is + /// valid only for query parameters and allows sending a parameter + /// with an empty value. Default value is false. If style is used, + /// and if behavior is n/a (cannot be serialized), the value of + /// allowEmptyValue SHALL be ignored. + #[serde(skip_serializing_if = "Option::is_none")] + allow_empty_value: Option, + }, + Header { + #[serde(flatten)] + parameter_data: ParameterData, + /// Describes how the parameter value will be serialized depending on + /// the type of the parameter value. Default values (based on value of + /// in): for query - form; for path - simple; for header - simple; for + /// cookie - form. + #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] + style: HeaderStyle, + }, + Path { + #[serde(flatten)] + parameter_data: ParameterData, + /// Describes how the parameter value will be serialized depending on + /// the type of the parameter value. Default values (based on value of + /// in): for query - form; for path - simple; for header - simple; for + /// cookie - form. + #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] + style: PathStyle, + }, + Cookie { + #[serde(flatten)] + parameter_data: ParameterData, + /// Describes how the parameter value will be serialized depending on + /// the type of the parameter value. Default values (based on value of + /// in): for query - form; for path - simple; for header - simple; for + /// cookie - form. + #[serde(default, skip_serializing_if = "SkipSerializeIfDefault::skip")] + style: CookieStyle, + }, +} + +impl Parameter { + /// Returns the `parameter_data` field of this [ParameterData]. + pub fn parameter_data(self) -> ParameterData { + match self { + Parameter::Query { + parameter_data, + allow_reserved: _, + style: _, + allow_empty_value: _, + } => parameter_data, + Parameter::Header { + parameter_data, + style: _, + } => parameter_data, + Parameter::Path { + parameter_data, + style: _, + } => parameter_data, + Parameter::Cookie { + parameter_data, + style: _, + } => parameter_data, + } + } + + /// Returns the `parameter_data` field of this [ParameterData] by reference. + pub fn parameter_data_ref(&self) -> &ParameterData { + match self { + Parameter::Query { + parameter_data, + allow_reserved: _, + style: _, + allow_empty_value: _, + } => parameter_data, + Parameter::Header { + parameter_data, + style: _, + } => parameter_data, + Parameter::Path { + parameter_data, + style: _, + } => parameter_data, + Parameter::Cookie { + parameter_data, + style: _, + } => parameter_data, + } + } +} + +struct SkipSerializeIfDefault; +impl SkipSerializeIfDefault { + #[cfg(feature = "skip_serializing_defaults")] + fn skip(value: &D) -> bool { + value == &Default::default() + } + #[cfg(not(feature = "skip_serializing_defaults"))] + fn skip(_value: &D) -> bool { + false + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum PathStyle { + Matrix, + Label, + Simple, +} + +impl Default for PathStyle { + fn default() -> Self { + PathStyle::Simple + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum QueryStyle { + Form, + SpaceDelimited, + PipeDelimited, + DeepObject, +} + +impl Default for QueryStyle { + fn default() -> Self { + QueryStyle::Form + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum CookieStyle { + Form, +} + +impl Default for CookieStyle { + fn default() -> CookieStyle { + CookieStyle::Form + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum HeaderStyle { + Simple, +} + +impl Default for HeaderStyle { + fn default() -> HeaderStyle { + HeaderStyle::Simple + } +} diff --git a/src/v3_1/paths.rs b/src/v3_1/paths.rs new file mode 100644 index 0000000..f02bef0 --- /dev/null +++ b/src/v3_1/paths.rs @@ -0,0 +1,177 @@ +use std::marker::PhantomData; + +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize}; + +/// Describes the operations available on a single path. +/// A Path Item MAY be empty, due to ACL constraints. +/// The path itself is still exposed to the documentation +/// viewer but they will not know which operations and +/// parameters are available. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct PathItem { + /// Allows for a referenced definition of this path item. The referenced + /// structure MUST be in the form of a Path Item Object. In case a Path + /// Item Object field appears both in the defined object and the referenced + /// object, the behavior is undefined. See the rules for resolving Relative + /// References. + #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] + reference: Option, + /// An optional, string summary, intended to apply to all operations in + /// this path. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// An optional, string description, intended to apply to all operations in + /// this path. CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub get: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub put: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub post: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delete: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub head: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub patch: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trace: Option, + /// An alternative server array to service all operations in this path. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub servers: Vec, + /// A list of parameters that are applicable for all the + /// operations described under this path. These parameters + /// can be overridden at the operation level, but cannot be + /// removed there. The list MUST NOT include duplicated parameters. + /// A unique parameter is defined by a combination of a name and location. + /// The list can use the Reference Object to link to parameters that + /// are defined at the OpenAPI Object's components/parameters. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub parameters: Vec>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +impl PathItem { + /// Returns an iterator of references to the [Operation]s in the [PathItem]. + pub fn iter(&self) -> impl Iterator { + vec![ + ("get", &self.get), + ("put", &self.put), + ("post", &self.post), + ("delete", &self.delete), + ("options", &self.options), + ("head", &self.head), + ("patch", &self.patch), + ("trace", &self.trace), + ] + .into_iter() + .filter_map(|(method, maybe_op)| maybe_op.as_ref().map(|op| (method, op))) + } +} + +impl IntoIterator for PathItem { + type Item = (&'static str, Operation); + + type IntoIter = std::vec::IntoIter; + + /// Returns an iterator of the [Operation]s in the [PathItem]. + fn into_iter(self) -> Self::IntoIter { + vec![ + ("get", self.get), + ("put", self.put), + ("post", self.post), + ("delete", self.delete), + ("options", self.options), + ("head", self.head), + ("patch", self.patch), + ("trace", self.trace), + ] + .into_iter() + .filter_map(|(method, maybe_op)| maybe_op.map(|op| (method, op))) + .collect::>() + .into_iter() + } +} + +/// Holds the relative paths to the individual endpoints and +/// their operations. The path is appended to the URL from the +/// Server Object in order to construct the full URL. The Paths +/// MAY be empty, due to Access Control List (ACL) constraints. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Paths { + /// A map of PathItems or references to them. + #[serde(flatten, deserialize_with = "deserialize_paths")] + pub paths: IndexMap>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +impl Paths { + /// Iterate over path items. + pub fn iter(&self) -> indexmap::map::Iter> { + self.paths.iter() + } +} + +impl IntoIterator for Paths { + type Item = (String, ReferenceOr); + + type IntoIter = indexmap::map::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.paths.into_iter() + } +} + +fn deserialize_paths<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_map(PredicateVisitor( + |key: &String| key.starts_with('/'), + PhantomData, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_item_iterators() { + let operation = Operation::default(); + + let path_item = PathItem { + get: Some(operation.clone()), + post: Some(operation.clone()), + delete: Some(operation.clone()), + ..Default::default() + }; + + let expected = vec![ + ("get", &operation), + ("post", &operation), + ("delete", &operation), + ]; + assert_eq!(path_item.iter().collect::>(), expected); + + let expected = vec![ + ("get", operation.clone()), + ("post", operation.clone()), + ("delete", operation.clone()), + ]; + assert_eq!(path_item.into_iter().collect::>(), expected); + } +} diff --git a/src/v3_1/reference.rs b/src/v3_1/reference.rs new file mode 100644 index 0000000..ba1946f --- /dev/null +++ b/src/v3_1/reference.rs @@ -0,0 +1,103 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +pub enum ReferenceOr { + Reference { + /// REQUIRED. The reference identifier. This MUST be in the form of a + /// URI. + #[serde(rename = "$ref")] + reference: String, + /// A short summary which by default SHOULD override that of the + /// referenced component. If the referenced object-type does not allow a + /// `summary` field, then this field has no effect. + #[serde(skip_serializing_if = "Option::is_none")] + summary: Option, + /// A description which by default SHOULD override that of the + /// referenced component. CommonMark syntax MAY be used for rich text + /// representation. If the referenced object-type does not allow a + /// `description` field, then this field has no effect. + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + Item(T), +} + +impl ReferenceOr { + pub fn ref_(r: &str) -> Self { + ReferenceOr::Reference { + reference: r.to_owned(), + summary: None, + description: None, + } + } + pub fn boxed_item(item: T) -> ReferenceOr> { + ReferenceOr::Item(Box::new(item)) + } + + /// Converts this [ReferenceOr] to the item inside, if it exists. + /// + /// The return value will be [Option::Some] if this was a + /// [ReferenceOr::Item] or [Option::None] if this was a + /// [ReferenceOr::Reference]. + /// + /// # Examples + /// + /// ``` + /// # use openapiv3::v3_1::ReferenceOr; + /// + /// let i = ReferenceOr::Item(1); + /// assert_eq!(i.into_item(), Some(1)); + /// + /// let j: ReferenceOr = ReferenceOr::Reference { reference: String::new(), summary: None, description: None }; + /// assert_eq!(j.into_item(), None); + /// ``` + pub fn into_item(self) -> Option { + match self { + ReferenceOr::Reference { .. } => None, + ReferenceOr::Item(i) => Some(i), + } + } + + /// Returns a reference to to the item inside this [ReferenceOr], if it + /// exists. + /// + /// The return value will be [Option::Some] if this was a + /// [ReferenceOr::Item] or [Option::None] if this was a + /// [ReferenceOr::Reference]. + /// + /// # Examples + /// + /// ``` + /// # use openapiv3::v3_1::ReferenceOr; + /// + /// let i = ReferenceOr::Item(1); + /// assert_eq!(i.as_item(), Some(&1)); + /// + /// let j: ReferenceOr = ReferenceOr::Reference { reference: String::new(), summary: None, description: None }; + /// assert_eq!(j.as_item(), None); + /// ``` + pub fn as_item(&self) -> Option<&T> { + match self { + ReferenceOr::Reference { .. } => None, + ReferenceOr::Item(i) => Some(i), + } + } +} + +impl ReferenceOr> { + pub fn unbox(self) -> ReferenceOr { + match self { + ReferenceOr::Reference { + reference, + summary, + description, + } => ReferenceOr::Reference { + reference, + summary, + description, + }, + ReferenceOr::Item(boxed) => ReferenceOr::Item(*boxed), + } + } +} diff --git a/src/v3_1/request_body.rs b/src/v3_1/request_body.rs new file mode 100644 index 0000000..22c38d4 --- /dev/null +++ b/src/v3_1/request_body.rs @@ -0,0 +1,26 @@ +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct RequestBody { + /// A brief description of the request body. + /// This could contain examples of use. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// REQUIRED. The content of the request body. + /// The key is a media type or media type range and + /// the value describes it. For requests that match + /// multiple keys, only the most specific key is applicable. + /// e.g. text/plain overrides text/* + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub content: IndexMap, + /// Determines if the request body is required in the + /// request. Defaults to false. + #[serde(default, skip_serializing_if = "is_false")] + pub required: bool, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/responses.rs b/src/v3_1/responses.rs new file mode 100644 index 0000000..807178a --- /dev/null +++ b/src/v3_1/responses.rs @@ -0,0 +1,103 @@ +use std::marker::PhantomData; + +use crate::{util::*, v3_1::*}; +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Responses { + /// The documentation of responses other than the ones declared + /// for specific HTTP response codes. Use this field to cover + /// undeclared responses. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option>, + /// Any HTTP status code can be used as the property name, + /// but only one property per code, to describe the expected + /// response for that HTTP status code. This field MUST be enclosed in + /// quotation marks (for example, "200") for compatibility between + /// JSON and YAML. To define a range of response codes, this field + /// MAY contain the uppercase wildcard character X. For example, + /// 2XX represents all response codes between [200-299]. The following + /// range definitions are allowed: 1XX, 2XX, 3XX, 4XX, and 5XX. + /// If a response range is defined using an explicit code, the + /// explicit code definition takes precedence over the range + /// definition for that code. + #[serde(flatten, deserialize_with = "deserialize_responses")] + pub responses: IndexMap>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Response { + /// REQUIRED. A description of the response. + /// CommonMark syntax MAY be used for rich text representation. + pub description: String, + + /// Maps a header name to its definition. + /// RFC7230 states header names are case insensitive. + /// If a response header is defined with the name "Content-Type", + /// it SHALL be ignored. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub headers: IndexMap>, + + /// A map containing descriptions of potential response payloads. + /// The key is a media type or media type range and the value + /// describes it. For responses that match multiple keys, + /// only the most specific key is applicable. e.g. text/plain + /// overrides text/* + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub content: IndexMap, + + /// A map of operations links that can be followed from the response. + /// The key of the map is a short name for the link, following + /// the naming constraints of the names for Component Objects. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub links: IndexMap>, + + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} + +fn deserialize_responses<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + // We rely on the result of StatusCode::deserialize to act as our + // predicate; it will succeed only for status code. + deserializer.deserialize_map(PredicateVisitor(|_: &StatusCode| true, PhantomData)) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::v3_1::{ReferenceOr, Response, Responses, StatusCode}; + + #[test] + fn test_responses() { + let responses = serde_json::from_str::( + r#"{ + "404": { + "description": "xxx" + }, + "x-foo": "bar", + "ignored": "wat" + }"#, + ) + .unwrap(); + + assert_eq!( + responses.responses.get(&StatusCode::Code(404)), + Some(&ReferenceOr::Item(Response { + description: "xxx".to_string(), + ..Default::default() + })) + ); + assert_eq!(responses.extensions.get("x-foo"), Some(&json!("bar"))); + } +} diff --git a/src/v3_1/schema.rs b/src/v3_1/schema.rs new file mode 100644 index 0000000..b8a3312 --- /dev/null +++ b/src/v3_1/schema.rs @@ -0,0 +1,24 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Schema { + #[serde(flatten)] + pub json_schema: schemars::schema::Schema, + /// Additional external documentation for this schema. + #[serde(skip_serializing_if = "Option::is_none")] + pub external_docs: Option, + /// A free-form property to include an example of an instance for this + /// schema. To represent examples that cannot be naturally represented in + /// JSON or YAML, a string value can be used to contain the example with + /// escaping where necessary. **Deprecated:** The `example` property has + /// been deprecated in favor of the JSON Schema `examples` keyword. Use + /// of `example` is discouraged, and later versions of this + /// specification may remove it. + pub example: Option, + /// Inline extensions to this object. Additional properties MAY omit the + /// `x-` prefix within this object. + #[serde(flatten)] + pub extensions: IndexMap, +} diff --git a/src/v3_1/security_requirement.rs b/src/v3_1/security_requirement.rs new file mode 100644 index 0000000..8c29f85 --- /dev/null +++ b/src/v3_1/security_requirement.rs @@ -0,0 +1,16 @@ +use indexmap::IndexMap; + +/// Lists the required security schemes to execute this operation. +/// The name used for each property MUST correspond to a security +/// scheme declared in the Security Schemes under the Components Object. +/// +/// Security Requirement Objects that contain multiple schemes require +/// that all schemes MUST be satisfied for a request to be authorized. +/// This enables support for scenarios where multiple query parameters or +/// HTTP headers are required to convey security information. +/// +/// When a list of Security Requirement Objects is defined on the +/// Open API object or Operation Object, only one of +/// Security Requirement Objects in the list needs to be satisfied +/// to authorize the request. +pub type SecurityRequirement = IndexMap>; diff --git a/src/v3_1/security_scheme.rs b/src/v3_1/security_scheme.rs new file mode 100644 index 0000000..e2bf6ae --- /dev/null +++ b/src/v3_1/security_scheme.rs @@ -0,0 +1,96 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Defines a security scheme that can be used by the operations. +/// Supported schemes are HTTP authentication, an API key (either as a +/// header or as a query parameter), OAuth2's common flows (implicit, password, +/// application and access code) as defined in RFC6749, and OpenID Connect +/// Discovery. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum SecurityScheme { + #[serde(rename = "apiKey")] + APIKey { + #[serde(rename = "in")] + location: APIKeyLocation, + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + #[serde(rename = "http")] + HTTP { + scheme: String, + #[serde(rename = "bearerFormat")] + bearer_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + #[serde(rename = "oauth2")] + OAuth2 { + flows: OAuth2Flows, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + #[serde(rename = "openIdConnect")] + OpenIDConnect { + #[serde(rename = "openIdConnectUrl")] + open_id_connect_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum APIKeyLocation { + Query, + Header, + Cookie, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct OAuth2Flows { + #[serde(flatten)] + pub implicit: Option, + #[serde(flatten)] + pub password: Option, + #[serde(flatten)] + pub client_credentials: Option, + #[serde(flatten)] + pub authorization_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum OAuth2Flow { + #[serde(rename_all = "camelCase")] + Implicit { + authorization_url: String, + refresh_url: Option, + #[serde(default)] + scopes: IndexMap, + }, + #[serde(rename_all = "camelCase")] + Password { + refresh_url: Option, + token_url: String, + #[serde(default)] + scopes: IndexMap, + }, + #[serde(rename_all = "camelCase")] + ClientCredentials { + refresh_url: Option, + token_url: String, + #[serde(default)] + scopes: IndexMap, + }, + #[serde(rename_all = "camelCase")] + AuthorizationCode { + authorization_url: String, + token_url: String, + refresh_url: Option, + #[serde(default)] + scopes: IndexMap, + }, +} diff --git a/src/v3_1/server.rs b/src/v3_1/server.rs new file mode 100644 index 0000000..27221b4 --- /dev/null +++ b/src/v3_1/server.rs @@ -0,0 +1,27 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// An object representing a Server. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Server { + /// REQUIRED. A URL to the target host. + /// This URL supports Server Variables and MAY be relative, + /// to indicate that the host location is relative to the + /// location where the OpenAPI document is being served. + /// Variable substitutions will be made when a variable + /// is named in {brackets}. + pub url: String, + /// An optional string describing the host designated + /// by the URL. CommonMark syntax MAY be used for rich + /// text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A map between a variable name and its value. + /// The value is used for substitution in the server's URL template. + #[serde(skip_serializing_if = "Option::is_none")] + pub variables: Option>, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/server_variable.rs b/src/v3_1/server_variable.rs new file mode 100644 index 0000000..a7fde37 --- /dev/null +++ b/src/v3_1/server_variable.rs @@ -0,0 +1,26 @@ +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// An object representing a Server Variable +/// for server URL template substitution. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct ServerVariable { + /// An enumeration of string values to be + /// used if the substitution options are from a limited set. + #[serde(rename = "enum")] + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub enumeration: Vec, + /// REQUIRED. The default value to use for substitution, + /// and to send, if an alternate value is not supplied. + /// Unlike the Schema Object's default, this value MUST + /// be provided by the consumer. + pub default: String, + /// An optional description for the server + /// variable. CommonMark syntax MAY be used + /// for rich text representation. + pub description: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/status_code.rs b/src/v3_1/status_code.rs new file mode 100644 index 0000000..f134270 --- /dev/null +++ b/src/v3_1/status_code.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum StatusCode { + Code(u16), + Range(u16), +} + +impl fmt::Display for StatusCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StatusCode::Code(n) => write!(f, "{}", n), + StatusCode::Range(n) => write!(f, "{}XX", n), + } + } +} + +impl<'de> Deserialize<'de> for StatusCode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, Unexpected, Visitor}; + + struct StatusCodeVisitor; + + impl<'de> Visitor<'de> for StatusCodeVisitor { + type Value = StatusCode; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("number between 100 and 999 (as string or integer) or a string that matches `\\dXX`") + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + if value >= 100 && value < 1000 { + Ok(StatusCode::Code(value as u16)) + } else { + Err(E::invalid_value(Unexpected::Signed(value), &self)) + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + if value >= 100 && value < 1000 { + Ok(StatusCode::Code(value as u16)) + } else { + Err(E::invalid_value(Unexpected::Unsigned(value), &self)) + } + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + if value.len() != 3 { + return Err(E::invalid_value(Unexpected::Str(value), &"length 3")); + } + + if let Ok(number) = value.parse::() { + return self.visit_i64(number); + } + + if !value.is_ascii() { + return Err(E::invalid_value( + Unexpected::Str(value), + &"ascii, format `\\dXX`", + )); + } + + let v = value.as_bytes().to_ascii_uppercase(); + + match [v[0], v[1], v[2]] { + [n, b'X', b'X'] if n.is_ascii_digit() => { + Ok(StatusCode::Range(u16::from(n - b'0'))) + } + _ => Err(E::invalid_value(Unexpected::Str(value), &"format `\\dXX`")), + } + } + } + + deserializer.deserialize_any(StatusCodeVisitor) + } +} + +impl Serialize for StatusCode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::StatusCode; + use serde_yaml::from_str; + + #[test] + fn deserialize_strings_and_numbers() { + assert_eq!(StatusCode::Code(200), from_str("200").unwrap(),); + assert_eq!(StatusCode::Code(200), from_str("'200'").unwrap(),); + } + + #[test] + #[should_panic = "expected length 3"] + fn deserialize_invalid_code() { + let _: StatusCode = from_str("'6666'").unwrap(); + } + + #[test] + fn deserialize_ranges() { + assert_eq!(StatusCode::Range(2), from_str("2XX").unwrap(),); + assert_eq!(StatusCode::Range(4), from_str("'4xx'").unwrap(),); + } + + #[test] + #[should_panic = "invalid value"] + fn deserialize_invalid_range() { + let _: StatusCode = from_str("2XY").unwrap(); + } +} diff --git a/src/v3_1/tag.rs b/src/v3_1/tag.rs new file mode 100644 index 0000000..3aebbef --- /dev/null +++ b/src/v3_1/tag.rs @@ -0,0 +1,22 @@ +use crate::v3_1::*; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; + +/// Adds metadata to a single tag that is used by the +/// Operation Object. It is not mandatory to have a +/// Tag Object per tag defined in the Operation Object instances. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct Tag { + /// REQUIRED. The name of the tag. + pub name: String, + /// A description for the tag. + /// CommonMark syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional external documentation for this tag. + #[serde(rename = "externalDocs", skip_serializing_if = "Option::is_none")] + pub external_docs: Option, + /// Inline extensions to this object. + #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")] + pub extensions: IndexMap, +} diff --git a/src/v3_1/variant_or.rs b/src/v3_1/variant_or.rs new file mode 100644 index 0000000..9b2fdbc --- /dev/null +++ b/src/v3_1/variant_or.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum VariantOrUnknown { + Item(T), + Unknown(String), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum VariantOrUnknownOrEmpty { + Item(T), + Unknown(String), + Empty, +} + +impl VariantOrUnknownOrEmpty { + pub fn is_empty(&self) -> bool { + match self { + VariantOrUnknownOrEmpty::Empty => true, + _ => false, + } + } +} + +impl Default for VariantOrUnknownOrEmpty { + fn default() -> Self { + VariantOrUnknownOrEmpty::Empty + } +} diff --git a/tests/test.rs b/tests/test.rs index e99500d..5af8b0f 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,6 +1,6 @@ use indexmap::IndexMap; use newline_converter::dos2unix; -use openapiv3::*; +use openapiv3::v3_0::*; enum FileType { YAML, @@ -108,13 +108,23 @@ static TEST_CASES: &[(FileType, &str, &str)] = &[ "stripe.yaml", include_str!("../fixtures/stripe.yaml"), ), + ( + FileType::YAML, + "non-oauth-scopes.yaml", + include_str!("../fixtures/non-oauth-scopes.yaml"), + ), + ( + FileType::YAML, + "webhook-example.yaml", + include_str!("../fixtures/webhook-example.yaml"), + ), ]; #[test] fn run_tests() { for (file_type, name, contents) in TEST_CASES { println!("{}", name); - let openapi: OpenAPI = match file_type { + let openapi: openapiv3::OpenAPI = match file_type { FileType::YAML => serde_yaml::from_str(contents) .expect(&format!("Could not deserialize file {}", name)), FileType::JSON => serde_json::from_str(contents) @@ -138,8 +148,7 @@ macro_rules! map { #[test] fn petstore_discriminated() { - let api = OpenAPI { - openapi: "3.0.0".to_owned(), + let api = openapiv3::OpenAPI::Version30(OpenAPI { info: Info { title: "Swagger Petstore".to_owned(), license: Some(License { @@ -252,7 +261,7 @@ fn petstore_discriminated() { ..Default::default() }), ..Default::default() - }; + }); let yaml = include_str!("../fixtures/petstore-discriminated.yaml"); assert_eq!(serde_yaml::to_string(&api).unwrap(), dos2unix(yaml)); assert_eq!(api, serde_yaml::from_str(yaml).unwrap());