diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5931308..2cb350b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,4 +17,6 @@ jobs: - name: Create tests run: cargo run -p generate-tests --verbose - name: Run tests - run: cargo test --verbose \ No newline at end of file + run: cargo test --verbose + - name: Run tests with feature utoipa + run: cargo test --features utoipa --doc --verbose \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 06f3041..4b45f6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,10 @@ readme = "README.md" serde = {version = "1.0.130", features = ["derive"]} serde_json = "1.0.67" serde_yaml = "0.9.21" -indexmap = { version = "1.8.1", features = ["serde-1"] } +utoipa = { version = "3.3.0", optional = true, features=["debug"] } + +[features] +utoipa = ["dep:utoipa"] [workspace] members = [ diff --git a/generate-tests/Cargo.toml b/generate-tests/Cargo.toml index f695419..7930cce 100644 --- a/generate-tests/Cargo.toml +++ b/generate-tests/Cargo.toml @@ -6,9 +6,8 @@ edition = "2018" [dependencies] serde = {version = "1.0.130", features = ["derive"]} serde_json = "1.0.67" -serde_yaml = "0.8.20" -indexmap = { version = "1.8.1", features = ["serde-1"] } +serde_yaml = "0.9.21" quote = "1.0.10" -convert_case = "0.4.0" +convert_case = "0.6.0" regex = "1.5.4" paste = "1.0.5" diff --git a/src/api.rs b/src/api.rs index 45e6878..3ed5382 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,7 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -use crate::{Channel, Components, ExternalDocumentation, Info, ReferenceOr, Server, Tag}; +use crate::{Channel, Components, ExternalDocumentation, Info, RefOr, Server, Tag}; /// This is the root document object for the API specification. /// It combines resource listing and API declaration together into one document. @@ -97,8 +97,8 @@ pub struct AsyncAPI { /// protocol: kafka /// protocolVersion: '1.0.0' /// ``` - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub servers: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub servers: BTreeMap>, /// Default content type to use when encoding/decoding a message's payload. /// A string representing the default content type to use when encoding/decoding a /// message's payload. The value MUST be a specific media type (e.g. `application/json`). @@ -150,7 +150,7 @@ pub struct AsyncAPI { /// subscribe: /// $ref: "#/components/messages/userSignedUp" /// ``` - pub channels: IndexMap, + pub channels: BTreeMap, /// An element to hold various schemas for the specification. #[serde(skip_serializing_if = "Option::is_none")] pub components: Option, @@ -164,5 +164,5 @@ pub struct AsyncAPI { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/channel.rs b/src/channel.rs index d00c878..9e65cf5 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,9 +1,9 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use crate::{ ChannelBinding, ExternalDocumentation, Message, OperationBinding, OperationTrait, Parameter, - ReferenceOr, Tag, + RefOr, Tag, }; /// Describes the operations available on a single channel. @@ -153,16 +153,16 @@ pub struct Channel { /// subscribe: /// $ref: "#/components/messages/userSignedUp" /// ``` - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub parameters: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub parameters: BTreeMap>, /// A map where the keys describe the name of the protocol and the values /// describe protocol-specific definitions for the channel. #[serde(skip_serializing_if = "Option::is_none")] - pub bindings: Option>, + pub bindings: Option>, /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Describes a publish or a subscribe operation. This provides a place to document how @@ -274,13 +274,13 @@ pub struct Operation { /// A map where the keys describe the name of the protocol and the /// values describe protocol-specific definitions for the operation. #[serde(skip_serializing_if = "Option::is_none")] - pub bindings: Option>, + pub bindings: Option>, /// A list of traits to apply to the operation object. /// Traits MUST be merged into the operation object using the /// [JSON Merge Patch](https://tools.ietf.org/html/rfc7386) /// algorithm in the same order they are defined here. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub traits: Vec>, + pub traits: Vec>, /// A definition of the message that will be published or received on /// this channel. `oneOf` is allowed here to specify multiple messages, however, /// **a message MUST be valid only against one of the referenced message objects.** @@ -289,12 +289,12 @@ pub struct Operation { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum OperationMessageType { - Map(IndexMap>), - Single(ReferenceOr), + Map(BTreeMap>), + Single(RefOr), } diff --git a/src/channel_binding.rs b/src/channel_binding.rs index 8dfd83a..4352dc8 100644 --- a/src/channel_binding.rs +++ b/src/channel_binding.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::Schema; @@ -60,7 +60,7 @@ pub struct ChannelBinding { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// This object MUST NOT contain any properties. Its name is reserved for future use. diff --git a/src/components.rs b/src/components.rs index c66f86e..1d47db1 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,9 +1,9 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use crate::{ message_binding::MessageBinding, Channel, ChannelBinding, CorrelationId, Message, MessageTrait, - OperationBinding, OperationTrait, Parameter, ReferenceOr, Schema, SecurityScheme, Server, + OperationBinding, OperationTrait, Parameter, RefOr, Schema, SecurityScheme, Server, ServerBinding, }; @@ -186,56 +186,56 @@ use crate::{ pub struct Components { /// An object to hold reusable /// [Schema Objects][crate::Schema]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub schemas: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub schemas: BTreeMap>, /// An object to hold reusable /// [Message Objects][crate::Message]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub messages: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub messages: BTreeMap>, /// An object to hold reusable /// [Security Scheme Objects][crate::SecurityScheme]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub security_schemes: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub security_schemes: BTreeMap>, /// An object to hold reusable /// [Parameter Objects][crate::Parameter]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub parameters: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub parameters: BTreeMap>, /// An object to hold reusable /// [Correlation ID Objects][crate::CorrelationId]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub correlation_ids: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub correlation_ids: BTreeMap>, /// An object to hold reusable /// [Operation Trait Objects][crate::OperationTrait]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub operation_traits: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub operation_traits: BTreeMap>, /// An object to hold reusable /// [Message Trait Objects][crate::MessageTrait]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub message_traits: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub message_traits: BTreeMap>, /// An object to hold reusable [Server Objects][crate::Server]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub servers: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub servers: BTreeMap>, /// An object to hold reusable /// [Server Bindings Objects][crate::ServerBinding]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub server_bindings: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub server_bindings: BTreeMap>, /// An object to hold reusable [Channel Item Objects][crate::Channel]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub channels: IndexMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub channels: BTreeMap, /// An object to hold reusable /// [Channel Bindings Objects][crate::ChannelBinding]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub channel_bindings: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub channel_bindings: BTreeMap>, /// An object to hold reusable /// [Operation Bindings Objects][crate::OperationBinding]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub operation_bindings: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub operation_bindings: BTreeMap>, /// An object to hold reusable /// [Message Bindings Objects][crate::MessageBinding]. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub message_bindings: IndexMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub message_bindings: BTreeMap>, /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/correlation_id.rs b/src/correlation_id.rs index b3c635d..85b3714 100644 --- a/src/correlation_id.rs +++ b/src/correlation_id.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// An object that specifies an identifier at design time that can used for @@ -21,5 +21,5 @@ pub struct CorrelationId { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/discriminator.rs b/src/discriminator.rs index ebe3f9a..3910f8f 100644 --- a/src/discriminator.rs +++ b/src/discriminator.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// When request bodies or response payloads may be one of a number of different schemas, @@ -15,9 +15,9 @@ pub struct Discriminator { /// 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, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub mapping: BTreeMap, /// Inline extensions to this object. #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/example.rs b/src/example.rs index 224f754..eec0f26 100644 --- a/src/example.rs +++ b/src/example.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// The asyncapi spec doesn't describe a structured example object. @@ -27,5 +27,5 @@ pub struct Example { pub headers: Option, /// Inline extensions to this object. #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/external_documentation.rs b/src/external_documentation.rs index 0ccd201..5f1b865 100644 --- a/src/external_documentation.rs +++ b/src/external_documentation.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// Allows referencing an external resource for extended documentation. @@ -14,5 +14,5 @@ pub struct ExternalDocumentation { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/info.rs b/src/info.rs index 7b9e487..4e0c3db 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// The object provides metadata about the API. The metadata can be used by the clients if needed. @@ -60,7 +60,7 @@ pub struct Info { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Contact information for the exposed API. @@ -96,7 +96,7 @@ pub struct Contact { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// License information for the exposed API. @@ -125,5 +125,5 @@ pub struct License { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/lib.rs b/src/lib.rs index 401e122..a5013d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,77 @@ +//! This crate aims to provide data structures that represent the [AsyncAPI v 2.3.0 specification](https://www.asyncapi.com/docs/specifications/v2.3.0). +//! +//! This crate is still in an early development stage. The official valid test files are all parsed without errors. +//! +//! This crate builds upon the work for the [openapiv3 crate](https://crates.io/crates/openapiv3) and adapts it for the AsyncAPI specification. +//! ## Example +//! ```rust +//! use asyncapi::AsyncAPI; +//! +//! # #[cfg(not(feature = "utoipa"))] +//! fn main() { +//! let data = include_str!("../tests/asyncapi.yml"); +//! +//! // Deserialize to AsyncAPI struct +//! let asyncapi: AsyncAPI = serde_yaml::from_str(data).expect("Could not deserialize input"); +//! +//! // Now serialize the struct to verify, that it remained the same +//! let serialized = serde_yaml::to_string(&asyncapi).expect("Could not serialize"); +//! println!("{}", serialized); +//! # assert_eq!(data, serialized); +//! } +//! # #[cfg(feature = "utoipa")] +//! # fn main() {} +//! ``` +//! +//! ## Features +//! +//! ### Utoipa +//! This uses Utoipa as internal structs. +//! Especially useful to generate the Schema from Rust structs and then convert it to AsyncAPI. +//! +//! OpenAPI Schemas have a subset of the Functions from AsyncAPI. +//! Because of this the conversion from OpenAPI -> AsyncAPI works, but AsyncAPI -> OpenAPI may fail. +//! ```rust +//! # #[cfg(feature = "utoipa")] +//! use utoipa::{openapi, OpenApi, ToSchema}; +//! use asyncapi::Components; +//! +//! # #[cfg(feature = "utoipa")] +//! #[derive(ToSchema)] +//! struct UserSignedUp { +//! email: String, +//! first_name: String, +//! last_name: String +//! } +//! +//! # #[cfg(feature = "utoipa")] +//! #[derive(OpenApi)] +//! #[openapi(components(schemas(UserSignedUp)))] +//! pub struct ApiDoc; +//! +//! # #[cfg(feature = "utoipa")] +//! fn main() { +//! // OpenAPI Spec generated from utoipa +//! let openapi: openapi::OpenApi = ApiDoc::openapi(); +//! // We only care about the schemas +//! let schemas = openapi.components.expect("Could not generate Schemas").schemas; +//! +//! // Use the schemas in a empty AsyncAPI definition +//! let asyncapi = asyncapi::AsyncAPI { +//! components: Some(Components { +//! schemas, +//! ..Default::default() +//! }), +//! ..Default::default() +//! }; +//! +//! let serialized = serde_yaml::to_string(&asyncapi).expect("Could not serialize"); +//! println!("{}", serialized); +//! # assert_eq!(include_str!("../tests/utoipa-asyncapi.yml"), serialized); +//! } +//! # #[cfg(not(feature = "utoipa"))] +//! # fn main() {} +//! ``` mod api; mod channel; pub mod channel_binding; @@ -5,6 +79,7 @@ mod components; mod correlation_id; mod example; mod external_documentation; +#[cfg(not(feature = "utoipa"))] mod info; mod message; pub mod message_binding; @@ -12,8 +87,15 @@ mod message_trait; pub mod operation_binding; mod operation_trait; mod parameter; +#[cfg(not(feature = "utoipa"))] mod reference; +#[cfg(not(feature = "utoipa"))] pub mod schema; +#[cfg(feature = "utoipa")] +pub mod schema { + pub use utoipa::openapi::Schema; +} +mod discriminator; mod security_scheme; mod server; pub mod server_binding; @@ -21,23 +103,34 @@ mod tag; mod variant_or; pub use api::AsyncAPI; -pub use channel::{Channel, Operation}; +pub use channel::{Channel, Operation, OperationMessageType}; pub use channel_binding::ChannelBinding; pub use components::Components; pub use correlation_id::CorrelationId; pub use example::Example; pub use external_documentation::ExternalDocumentation; +#[cfg(not(feature = "utoipa"))] pub use info::{Contact, Info, License}; pub use message::Message; +pub use message::Payload; pub use message_binding::MessageBinding; pub use message_trait::MessageTrait; pub use operation_binding::OperationBinding; pub use operation_trait::OperationTrait; pub use parameter::Parameter; -pub use reference::ReferenceOr; +#[cfg(not(feature = "utoipa"))] +pub use reference::Ref; +#[cfg(not(feature = "utoipa"))] +pub use reference::RefOr; pub use schema::Schema; pub use security_scheme::SecurityScheme; pub use server::{SecurityRequirement, Server, ServerVariable}; pub use server_binding::ServerBinding; pub use tag::Tag; +#[cfg(feature = "utoipa")] +pub use utoipa::openapi::Ref; +#[cfg(feature = "utoipa")] +pub use utoipa::openapi::RefOr; +#[cfg(feature = "utoipa")] +pub use utoipa::openapi::{Contact, Info, License}; pub use variant_or::{VariantOrUnknown, VariantOrUnknownOrEmpty}; diff --git a/src/message.rs b/src/message.rs index 7c2236c..9ab745b 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,9 +1,7 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -use crate::{ - CorrelationId, Example, ExternalDocumentation, MessageBinding, ReferenceOr, Schema, Tag, -}; +use crate::{CorrelationId, Example, ExternalDocumentation, MessageBinding, RefOr, Schema, Tag}; /// Describes a message received on a given channel and operation. /// @@ -172,7 +170,7 @@ pub struct Message { /// Schema definition of the application headers. /// Schema MUST be of type "object". It **MUST NOT** define the protocol headers. #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, + pub headers: Option>, /// Definition of the message payload. It can be of any type /// but defaults to [Schema object][crate::Schema]. It must match the schema format, /// including encoding type - e.g Avro should be inlined as either @@ -181,7 +179,7 @@ pub struct Message { pub payload: Option, /// Definition of the correlation ID used for message tracing or matching. #[serde(skip_serializing_if = "Option::is_none")] - pub correlation_id: Option>, + pub correlation_id: Option>, /// A string containing the name of the schema /// format/language used to define the message payload. /// If omitted, implementations should parse the payload as a @@ -216,19 +214,20 @@ pub struct Message { pub external_docs: Option, /// A map where the keys describe the name of /// the protocol and the values describe protocol-specific definitions for the message. - pub bindings: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub bindings: Option>, /// An array with examples of valid message objects. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub examples: Vec, // TODO try to parse better /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum Payload { - Schema(Schema), + RefOr(RefOr), Any(serde_json::Value), } diff --git a/src/message_binding.rs b/src/message_binding.rs index 1c778a6..17ff331 100644 --- a/src/message_binding.rs +++ b/src/message_binding.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::Schema; @@ -60,7 +60,7 @@ pub struct MessageBinding { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// This object contains information about the message representation in HTTP. diff --git a/src/message_trait.rs b/src/message_trait.rs index af8da1f..063a333 100644 --- a/src/message_trait.rs +++ b/src/message_trait.rs @@ -1,8 +1,8 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use crate::{ - message_binding::MessageBinding, CorrelationId, ExternalDocumentation, ReferenceOr, Schema, Tag, + message_binding::MessageBinding, CorrelationId, ExternalDocumentation, RefOr, Schema, Tag, }; /// Describes a trait that MAY be applied to a @@ -34,10 +34,10 @@ pub struct MessageTrait { /// Schema MUST be of type "object". /// It **MUST NOT** define the protocol headers. #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, + pub headers: Option>, /// Definition of the correlation ID used for message tracing or matching. #[serde(skip_serializing_if = "Option::is_none")] - pub correlation_id: Option>, + pub correlation_id: Option>, /// A string containing the name of the schema format/language used to define /// the message payload. If omitted, implementations should parse the payload as a /// [Schema object][crate::Schema]. @@ -74,14 +74,14 @@ pub struct MessageTrait { /// A map where the keys describe the name of the protocol /// and the values describe protocol-specific definitions for the message. #[serde(skip_serializing_if = "Option::is_none")] - pub bindings: Option>, + pub bindings: Option>, /// List of examples. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub examples: Vec, /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Message Example Object represents an example of a @@ -126,8 +126,8 @@ pub struct MessageTrait { pub struct MessageExample { /// The value of this field MUST validate against the /// [Message Object's][crate::Message] headers field. - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub headers: IndexMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub headers: BTreeMap, /// The value of this field MUST validate against the /// [Message Object's][crate::Message] payload field. #[serde(skip_serializing_if = "Option::is_none")] @@ -141,5 +141,5 @@ pub struct MessageExample { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/operation_binding.rs b/src/operation_binding.rs index fd39de6..50db51b 100644 --- a/src/operation_binding.rs +++ b/src/operation_binding.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::Schema; @@ -57,7 +57,7 @@ pub struct OperationBinding { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// diff --git a/src/operation_trait.rs b/src/operation_trait.rs index a76da0e..e426ccb 100644 --- a/src/operation_trait.rs +++ b/src/operation_trait.rs @@ -1,7 +1,7 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -use crate::{ExternalDocumentation, OperationBinding, ReferenceOr, Tag}; +use crate::{ExternalDocumentation, OperationBinding, RefOr, Tag}; /// Describes a trait that MAY be applied to an /// [Operation Object][crate::Operation]. @@ -56,9 +56,9 @@ pub struct OperationTrait { /// A map where the keys describe the name of the protocol and the values describe /// protocol-specific definitions for the operation. #[serde(skip_serializing_if = "Option::is_none")] - pub bindings: Option>, + pub bindings: Option>, /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/parameter.rs b/src/parameter.rs index 5d2b401..142a5bf 100644 --- a/src/parameter.rs +++ b/src/parameter.rs @@ -1,7 +1,7 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -use crate::{ReferenceOr, Schema}; +use crate::{RefOr, Schema}; /// Describes a parameter included in a channel name. /// @@ -47,7 +47,7 @@ pub struct Parameter { pub description: Option, /// Definition of the parameter. #[serde(skip_serializing_if = "Option::is_none")] - pub schema: Option>, + pub schema: Option>, /// A [runtime expression](https://www.asyncapi.com/docs/specifications/v2.3.0#runtimeExpression) /// that specifies the location of the parameter value. /// Even when a definition for the target field exists, @@ -58,5 +58,5 @@ pub struct Parameter { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/src/reference.rs b/src/reference.rs index f1d9fad..f031ace 100644 --- a/src/reference.rs +++ b/src/reference.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(untagged)] -pub enum ReferenceOr { +pub enum RefOr { /// A simple object to allow referencing other components in the specification, /// internally and externally. /// @@ -17,29 +17,33 @@ pub enum ReferenceOr { /// /// For this specification, reference resolution is done as defined by the /// JSON Reference specification and not by the JSON Schema specification. - Reference { - #[serde(rename = "$ref")] - reference: String, - }, - Item(T), + Ref(Ref), + T(T), } -impl ReferenceOr { +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Ref { + /// Reference location of the actual component. + #[serde(rename = "$ref")] + pub ref_location: String, +} + +impl RefOr { pub fn ref_(r: &str) -> Self { - ReferenceOr::Reference { - reference: r.to_owned(), - } + RefOr::Ref(Ref { + ref_location: r.to_owned(), + }) } - pub fn boxed_item(item: T) -> ReferenceOr> { - ReferenceOr::Item(Box::new(item)) + pub fn boxed_item(item: T) -> RefOr> { + RefOr::T(Box::new(item)) } } -impl ReferenceOr> { - pub fn unbox(self) -> ReferenceOr { +impl RefOr> { + pub fn unbox(self) -> RefOr { match self { - ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference }, - ReferenceOr::Item(boxed) => ReferenceOr::Item(*boxed), + RefOr::Ref(reference) => RefOr::Ref(reference), + RefOr::T(boxed) => RefOr::T(*boxed), } } } diff --git a/src/schema.rs b/src/schema.rs index 739f5c7..849cd1d 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,20 +1,20 @@ use crate::*; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use std::clone::Clone; +use std::collections::BTreeMap; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SchemaData { - #[serde(default, skip_serializing_if = "Clone::clone")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub nullable: bool, - #[serde(default, skip_serializing_if = "Clone::clone")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub read_only: bool, - #[serde(default, skip_serializing_if = "Clone::clone")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub write_only: bool, /// Specifies that a schema is deprecated and SHOULD be transitioned out /// of usage. Default value is `false`. - #[serde(default, skip_serializing_if = "Clone::clone")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub deprecated: bool, /// Additional external documentation for this schema. #[serde(skip_serializing_if = "Option::is_none")] @@ -52,15 +52,15 @@ pub enum SchemaKind { Type(Type), OneOf { #[serde(rename = "oneOf")] - one_of: Vec>, + one_of: Vec>, }, AllOf { #[serde(rename = "allOf")] - all_of: Vec>, + all_of: Vec>, }, AnyOf { #[serde(rename = "anyOf")] - any_of: Vec>, + any_of: Vec>, }, Any(AnySchema), } @@ -80,7 +80,7 @@ pub enum Type { #[serde(untagged)] pub enum AdditionalProperties { Any(bool), - Schema(Box>), + Schema(Box>), } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] @@ -98,8 +98,8 @@ pub struct AnySchema { pub minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub properties: IndexMap>>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub properties: BTreeMap>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub required: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -109,7 +109,7 @@ pub struct AnySchema { #[serde(skip_serializing_if = "Option::is_none")] pub max_properties: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>>, + pub items: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -176,8 +176,8 @@ pub struct IntegerType { #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ObjectType { - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub properties: IndexMap>>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub properties: BTreeMap>>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub required: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -192,7 +192,7 @@ pub struct ObjectType { #[serde(rename_all = "camelCase")] pub struct ArrayType { #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>>, + pub items: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/security_scheme.rs b/src/security_scheme.rs index b13bc26..e199283 100644 --- a/src/security_scheme.rs +++ b/src/security_scheme.rs @@ -1,5 +1,5 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; /// Defines a security scheme that can be used by the operations. Supported schemes are: /// @@ -153,7 +153,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "apiKey")] ApiKey { @@ -169,7 +169,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, X509 { /// A short description for security scheme. @@ -180,7 +180,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "symmetricEncryption")] SymmetricEncryption { @@ -192,7 +192,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "asymmetricEncryption")] AsymmetricEncryption { @@ -204,7 +204,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "httpApiKey")] HttpApiKey { @@ -223,7 +223,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "http", rename_all = "camelCase")] Http { @@ -244,7 +244,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, /// # Examples /// ```json @@ -298,7 +298,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "openIdConnect", rename_all = "camelCase")] OpenIdConnect { @@ -313,7 +313,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "plain")] Plain { @@ -325,7 +325,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "scramSha256")] ScramSha256 { @@ -337,7 +337,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "scramSha512")] ScramSha512 { @@ -349,7 +349,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, #[serde(rename = "gssapi")] Gssapi { @@ -361,7 +361,7 @@ pub enum SecurityScheme { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - extensions: IndexMap, + extensions: BTreeMap, }, } @@ -384,7 +384,7 @@ pub struct OAuthFlows { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Configuration details for a supported OAuth Flow @@ -400,11 +400,11 @@ pub struct OAuthFlowImplicit { pub refresh_url: Option, /// **REQUIRED**. The available scopes for the OAuth2 security scheme. /// A map between the scope name and a short description for it. - pub scopes: IndexMap, + pub scopes: BTreeMap, /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Configuration details for a supported OAuth Flow @@ -420,11 +420,11 @@ pub struct OAuthFlowPassword { pub refresh_url: Option, /// **REQUIRED**. The available scopes for the OAuth2 security scheme. /// A map between the scope name and a short description for it. - pub scopes: IndexMap, + pub scopes: BTreeMap, /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Configuration details for a supported OAuth Flow @@ -440,11 +440,11 @@ pub struct OAuthFlowClientCredentials { pub refresh_url: Option, /// **REQUIRED**. The available scopes for the OAuth2 security scheme. /// A map between the scope name and a short description for it. - pub scopes: IndexMap, + pub scopes: BTreeMap, /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Configuration details for a supported OAuth Flow @@ -463,26 +463,26 @@ pub struct OAuthFlowAuthorizationCode { pub refresh_url: Option, /// **REQUIRED**. The available scopes for the OAuth2 security scheme. /// A map between the scope name and a short description for it. - pub scopes: IndexMap, + pub scopes: BTreeMap, /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } #[test] fn test_deserialize_security_scheme() { - use crate::ReferenceOr; + use crate::RefOr; let example = r#" type: apiKey in: user description: Provide your API key as the user and leave the password empty. "#; - let asyncapi: ReferenceOr = serde_yaml::from_str(example) + let asyncapi: RefOr = serde_yaml::from_str(example) .expect(&format!("Could not deserialize api key security scheme")); assert_eq!( - ReferenceOr::Item(SecurityScheme::ApiKey { + RefOr::T(SecurityScheme::ApiKey { location: "user".to_string(), description: Some( "Provide your API key as the user and leave the password empty.".to_string(), diff --git a/src/server.rs b/src/server.rs index 20e28f0..28a0d6b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,7 @@ -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -use crate::{ReferenceOr, ServerBinding}; +use crate::{RefOr, ServerBinding}; /// An object representing a message broker, a server or any other kind of /// computer program capable of sending and/or receiving data. This object is @@ -151,8 +151,8 @@ pub struct Server { 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(default, skip_serializing_if = "IndexMap::is_empty")] - pub variables: IndexMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub variables: BTreeMap, /// A declaration of which security mechanisms can be used with this /// server. The list of values includes alternative security requirement /// objects that can be used. Only one of the security requirement objects @@ -162,11 +162,11 @@ pub struct Server { /// A map where the keys describe the name of the protocol and the values /// describe protocol-specific definitions for the server. #[serde(skip_serializing_if = "Option::is_none")] - pub bindings: Option>, + pub bindings: Option>, /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// An object representing a Server Variable for server URL @@ -193,7 +193,7 @@ pub struct ServerVariable { /// This object MAY be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// Lists the required security schemes to execute this operation. The name @@ -257,5 +257,5 @@ pub struct SecurityRequirement { /// needed, the list can be empty. For other security scheme types, the /// array MUST be empty. #[serde(flatten)] - pub values: IndexMap>, + pub values: BTreeMap>, } diff --git a/src/server_binding.rs b/src/server_binding.rs index f461941..bbed9e4 100644 --- a/src/server_binding.rs +++ b/src/server_binding.rs @@ -1,4 +1,4 @@ -use indexmap::IndexMap; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// Map describing protocol-specific definitions for a server. @@ -59,7 +59,7 @@ pub struct ServerBinding { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } /// This object MUST NOT contain any properties. Its name is reserved for future use. diff --git a/src/tag.rs b/src/tag.rs index 314a05b..676d979 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,5 +1,5 @@ +use std::collections::BTreeMap; use crate::*; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; /// Allows adding meta data to a single tag. @@ -30,5 +30,5 @@ pub struct Tag { /// This object can be extended with /// [Specification Extensions](https://www.asyncapi.com/docs/specifications/v2.3.0#specificationExtensions). #[serde(flatten)] - pub extensions: IndexMap, + pub extensions: BTreeMap, } diff --git a/tests/asyncapi.yml b/tests/asyncapi.yml new file mode 100644 index 0000000..769a6d6 --- /dev/null +++ b/tests/asyncapi.yml @@ -0,0 +1,47 @@ +asyncapi: 2.0.0 +info: + title: Email Service + version: 1.0.0 + description: | + Sends emails upon certain events + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: + production: + url: mqtt://test.mosquitto.org + protocol: mqtt + description: Test MQTT broker +channels: + user/signedup: + publish: + operationId: onUserSignUp + message: + $ref: '#/components/messages/UserSignedUp' +components: + schemas: + UserSignedUp: + type: object + properties: + email: + description: baz + type: string + format: email + firstName: + description: foo + type: string + lastName: + description: bar + type: string + required: + - email + - first_name + - last_name + messages: + UserSignedUp: + payload: + $ref: '#/components/schemas/UserSignedUp' + contentType: application/json + name: userSignedUp + title: User signed up event + summary: Inform about a new user registration in the system diff --git a/tests/utoipa-asyncapi.yml b/tests/utoipa-asyncapi.yml new file mode 100644 index 0000000..5ae6749 --- /dev/null +++ b/tests/utoipa-asyncapi.yml @@ -0,0 +1,20 @@ +asyncapi: '' +info: + title: '' + version: '' +channels: {} +components: + schemas: + UserSignedUp: + type: object + required: + - email + - first_name + - last_name + properties: + email: + type: string + first_name: + type: string + last_name: + type: string