asyncapi_rust_models/lib.rs
1//! Runtime data structures for AsyncAPI 3.0 specifications
2//!
3//! This crate provides Rust types that represent [AsyncAPI 3.0](https://www.asyncapi.com/docs/reference/specification/v3.0.0)
4//! specification objects. These types are used by the proc macros to generate
5//! specifications at compile time and can also be constructed manually.
6//!
7//! ## Overview
8//!
9//! The main types mirror the AsyncAPI 3.0 specification structure:
10//!
11//! - [`AsyncApiSpec`] - Root specification object
12//! - [`Info`] - General API information
13//! - [`Server`] - Server connection details
14//! - [`Channel`] - Communication channels
15//! - [`Operation`] - Send/receive operations
16//! - [`Message`] - Message definitions
17//! - [`Schema`] - JSON Schema definitions
18//! - [`Components`] - Reusable components
19//!
20//! ## Serialization
21//!
22//! All types implement [`serde::Serialize`] and [`serde::Deserialize`] for JSON
23//! serialization, following the AsyncAPI 3.0 specification's JSON Schema.
24//!
25//! ## Example
26//!
27//! ```rust
28//! use asyncapi_rust_models::*;
29//! use indexmap::IndexMap;
30//!
31//! // Create a simple AsyncAPI specification
32//! let spec = AsyncApiSpec {
33//! asyncapi: "3.0.0".to_string(),
34//! info: Info {
35//! title: "My API".to_string(),
36//! version: "1.0.0".to_string(),
37//! description: Some("A simple API".to_string()),
38//! },
39//! servers: None,
40//! channels: None,
41//! operations: None,
42//! components: None,
43//! };
44//!
45//! // Serialize to JSON
46//! let json = serde_json::to_string_pretty(&spec).unwrap();
47//! ```
48
49#![deny(missing_docs)]
50#![warn(clippy::all)]
51
52use indexmap::IndexMap;
53use serde::{Deserialize, Serialize};
54
55/// AsyncAPI 3.0 Specification
56///
57/// Root document object representing a complete AsyncAPI specification.
58///
59/// This is the top-level object that contains all information about an API,
60/// including servers, channels, operations, and reusable components.
61///
62/// # Example
63///
64/// ```rust
65/// use asyncapi_rust_models::*;
66///
67/// let spec = AsyncApiSpec {
68/// asyncapi: "3.0.0".to_string(),
69/// info: Info {
70/// title: "My WebSocket API".to_string(),
71/// version: "1.0.0".to_string(),
72/// description: Some("Real-time messaging API".to_string()),
73/// },
74/// servers: None,
75/// channels: None,
76/// operations: None,
77/// components: None,
78/// };
79/// ```
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AsyncApiSpec {
82 /// AsyncAPI version (e.g., "3.0.0")
83 pub asyncapi: String,
84
85 /// General information about the API
86 pub info: Info,
87
88 /// Server connection details
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub servers: Option<IndexMap<String, Server>>,
91
92 /// Available channels (communication paths)
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub channels: Option<IndexMap<String, Channel>>,
95
96 /// Operations (send/receive)
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub operations: Option<IndexMap<String, Operation>>,
99
100 /// Reusable components (messages, schemas, etc.)
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub components: Option<Components>,
103}
104
105/// API information object
106///
107/// Contains general metadata about the API such as title, version, and description.
108/// This information is displayed in documentation tools and helps users understand
109/// the purpose and version of the API.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Info {
112 /// API title
113 ///
114 /// A human-readable name for the API (e.g., "Chat WebSocket API")
115 pub title: String,
116
117 /// API version
118 ///
119 /// The version of the API (e.g., "1.0.0"). Should follow semantic versioning.
120 pub version: String,
121
122 /// API description
123 ///
124 /// A longer description of the API's purpose and functionality (optional).
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub description: Option<String>,
127}
128
129/// Server connection information
130///
131/// Defines connection details for a server that hosts the API. Multiple servers
132/// can be defined to support different environments (production, staging, development).
133///
134/// # Example
135///
136/// ```rust
137/// use asyncapi_rust_models::{Server, ServerVariable};
138/// use indexmap::IndexMap;
139///
140/// let mut variables = IndexMap::new();
141/// variables.insert("userId".to_string(), ServerVariable {
142/// description: Some("User ID for connection".to_string()),
143/// default: None,
144/// enum_values: None,
145/// examples: Some(vec!["12".to_string(), "13".to_string()]),
146/// });
147///
148/// let server = Server {
149/// host: "chat.example.com:443".to_string(),
150/// protocol: "wss".to_string(),
151/// pathname: Some("/api/ws/{userId}".to_string()),
152/// description: Some("Production WebSocket server".to_string()),
153/// variables: Some(variables),
154/// };
155/// ```
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Server {
158 /// Server URL or host
159 ///
160 /// The hostname or URL where the server is hosted. May include port number.
161 /// Examples: "localhost:8080", "api.example.com", "ws.example.com:443"
162 pub host: String,
163
164 /// Protocol (e.g., "wss", "ws", "grpc")
165 ///
166 /// The protocol used to communicate with the server.
167 /// Common values: "ws" (WebSocket), "wss" (WebSocket Secure), "grpc", "mqtt"
168 pub protocol: String,
169
170 /// Optional pathname for the server URL
171 ///
172 /// The pathname to append to the host. Can contain variables in curly braces (e.g., "/api/ws/{userId}")
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub pathname: Option<String>,
175
176 /// Server description
177 ///
178 /// An optional human-readable description of the server's purpose or environment
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub description: Option<String>,
181
182 /// Server variables
183 ///
184 /// A map of variable name to ServerVariable definition for variables used in the pathname
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub variables: Option<IndexMap<String, ServerVariable>>,
187}
188
189/// Server variable definition
190///
191/// Defines a variable that can be used in the server pathname. Variables are
192/// substituted at runtime with actual values.
193///
194/// # Example
195///
196/// ```rust
197/// use asyncapi_rust_models::ServerVariable;
198///
199/// let user_id_var = ServerVariable {
200/// description: Some("Authenticated user ID".to_string()),
201/// default: None,
202/// enum_values: None,
203/// examples: Some(vec!["12".to_string(), "13".to_string()]),
204/// };
205/// ```
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ServerVariable {
208 /// Variable description
209 ///
210 /// Human-readable description of what this variable represents
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub description: Option<String>,
213
214 /// Default value
215 ///
216 /// The default value to use if no value is provided
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub default: Option<String>,
219
220 /// Enumeration of allowed values
221 ///
222 /// If specified, only these values are valid for this variable
223 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
224 pub enum_values: Option<Vec<String>>,
225
226 /// Example values
227 ///
228 /// A list of example values for documentation purposes
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub examples: Option<Vec<String>>,
231}
232
233/// Communication channel
234///
235/// Represents a communication path through which messages are exchanged.
236/// Channels define where messages are sent and received (e.g., WebSocket endpoints,
237/// message queue topics, gRPC methods).
238///
239/// # Example
240///
241/// ```rust
242/// use asyncapi_rust_models::{Channel, Parameter};
243/// use indexmap::IndexMap;
244///
245/// let mut parameters = IndexMap::new();
246/// parameters.insert("userId".to_string(), Parameter {
247/// description: Some("User ID for this WebSocket connection".to_string()),
248/// default: None,
249/// enum_values: None,
250/// examples: Some(vec!["42".to_string(), "100".to_string()]),
251/// location: None,
252/// });
253///
254/// let channel = Channel {
255/// address: Some("/ws/chat/{userId}".to_string()),
256/// messages: None,
257/// parameters: Some(parameters),
258/// };
259/// ```
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Channel {
262 /// Channel address/path
263 ///
264 /// The location where this channel is available. For WebSocket, this is typically
265 /// the WebSocket path (e.g., "/ws/chat"). For other protocols, this could be a
266 /// topic name, queue name, or method path.
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub address: Option<String>,
269
270 /// Messages available on this channel
271 ///
272 /// A map of message identifiers to message definitions or references.
273 /// Messages define the structure of data that flows through this channel.
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub messages: Option<IndexMap<String, MessageRef>>,
276
277 /// Channel parameters
278 ///
279 /// A map of parameter names to their schema definitions for variables used in the address
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub parameters: Option<IndexMap<String, Parameter>>,
282}
283
284/// Channel parameter definition
285///
286/// Defines a parameter that can be used in the channel address, following the
287/// [AsyncAPI 3.0 Parameter Object](https://www.asyncapi.com/docs/reference/specification/v3.0.0#parameterObject).
288///
289/// Note: AsyncAPI 3.0 removed the `schema` property from Parameter (present in 2.x).
290/// Parameters now use `description`, `default`, `enum`, `examples`, and `location`.
291///
292/// # Example
293///
294/// ```rust
295/// use asyncapi_rust_models::Parameter;
296///
297/// let user_id_param = Parameter {
298/// description: Some("User ID for this WebSocket connection".to_string()),
299/// default: Some("0".to_string()),
300/// enum_values: None,
301/// examples: Some(vec!["42".to_string(), "100".to_string()]),
302/// location: None,
303/// };
304/// ```
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct Parameter {
307 /// Human-readable description of what this parameter represents
308 #[serde(skip_serializing_if = "Option::is_none")]
309 pub description: Option<String>,
310
311 /// Default value for this parameter
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub default: Option<String>,
314
315 /// Enumeration of allowed values for this parameter
316 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
317 pub enum_values: Option<Vec<String>>,
318
319 /// Example values for this parameter
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub examples: Option<Vec<String>>,
322
323 /// Runtime expression specifying the location of the parameter value
324 ///
325 /// See <https://www.asyncapi.com/docs/reference/specification/v3.0.0#runtimeExpression>
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub location: Option<String>,
328}
329
330/// Reference to a message definition
331///
332/// Messages can be defined either inline or as references to reusable components.
333/// This enum supports both patterns, following the AsyncAPI 3.0 specification.
334///
335/// # Example
336///
337/// ```rust
338/// use asyncapi_rust_models::{MessageRef, Message};
339///
340/// // Reference to a component message
341/// let ref_msg = MessageRef::Reference {
342/// reference: "#/components/messages/ChatMessage".to_string(),
343/// };
344///
345/// // Inline message definition
346/// let inline_msg = MessageRef::Inline(Box::new(Message {
347/// name: Some("ChatMessage".to_string()),
348/// title: Some("Chat Message".to_string()),
349/// summary: Some("A chat message".to_string()),
350/// description: None,
351/// content_type: Some("application/json".to_string()),
352/// payload: None,
353/// }));
354/// ```
355#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(untagged)]
357pub enum MessageRef {
358 /// Reference to component message
359 ///
360 /// Points to a reusable message definition in the components section.
361 /// Format: "#/components/messages/{messageName}"
362 Reference {
363 /// $ref path
364 #[serde(rename = "$ref")]
365 reference: String,
366 },
367 /// Inline message definition
368 ///
369 /// Embeds the message definition directly rather than referencing a component
370 Inline(Box<Message>),
371}
372
373/// Message definition
374///
375/// Represents a message that can be sent or received through a channel.
376/// Messages describe the structure, content type, and documentation for data
377/// exchanged in asynchronous communication.
378///
379/// # Example
380///
381/// ```rust
382/// use asyncapi_rust_models::{Message, Schema, SchemaObject};
383/// use indexmap::IndexMap;
384///
385/// let message = Message {
386/// name: Some("ChatMessage".to_string()),
387/// title: Some("Chat Message".to_string()),
388/// summary: Some("A message in a chat room".to_string()),
389/// description: Some("Sent when a user posts a message".to_string()),
390/// content_type: Some("application/json".to_string()),
391/// payload: Some(Schema::Object(Box::new(SchemaObject {
392/// schema_type: Some(serde_json::json!("object")),
393/// properties: None,
394/// required: None,
395/// description: Some("Chat message payload".to_string()),
396/// title: None,
397/// enum_values: None,
398/// const_value: None,
399/// items: None,
400/// additional_properties: None,
401/// one_of: None,
402/// any_of: None,
403/// all_of: None,
404/// additional: IndexMap::new(),
405/// }))),
406/// };
407/// ```
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct Message {
410 /// Message name
411 ///
412 /// A machine-readable identifier for the message (e.g., "ChatMessage", "user.join")
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub name: Option<String>,
415
416 /// Message title
417 ///
418 /// A human-readable title for the message
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub title: Option<String>,
421
422 /// Message summary
423 ///
424 /// A short summary of what the message is for
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub summary: Option<String>,
427
428 /// Message description
429 ///
430 /// A detailed description of the message's purpose and usage
431 #[serde(skip_serializing_if = "Option::is_none")]
432 pub description: Option<String>,
433
434 /// Content type (e.g., "application/json")
435 ///
436 /// The MIME type of the message payload. Common values:
437 /// - "application/json" (default for text messages)
438 /// - "application/octet-stream" (binary data)
439 /// - "application/x-protobuf" (Protocol Buffers)
440 /// - "application/x-msgpack" (MessagePack)
441 #[serde(rename = "contentType", skip_serializing_if = "Option::is_none")]
442 pub content_type: Option<String>,
443
444 /// Message payload schema
445 ///
446 /// JSON Schema defining the structure of the message payload
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub payload: Option<Schema>,
449}
450
451/// Operation (send or receive)
452///
453/// Defines an action that can be performed on a channel. Operations describe
454/// whether an application sends or receives messages through a specific channel.
455///
456/// # Example
457///
458/// ```rust
459/// use asyncapi_rust_models::{Operation, OperationAction, ChannelRef};
460///
461/// let operation = Operation {
462/// action: OperationAction::Send,
463/// channel: ChannelRef {
464/// reference: "#/channels/chat".to_string(),
465/// },
466/// messages: None,
467/// };
468/// ```
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct Operation {
471 /// Operation action (send or receive)
472 ///
473 /// Specifies whether the application sends or receives messages
474 pub action: OperationAction,
475
476 /// Channel reference
477 ///
478 /// Points to the channel where this operation takes place
479 pub channel: ChannelRef,
480
481 /// Messages for this operation
482 ///
483 /// Optional list of messages that can be used with this operation
484 #[serde(skip_serializing_if = "Option::is_none")]
485 pub messages: Option<Vec<MessageRef>>,
486}
487
488/// Operation action type
489#[derive(Debug, Clone, Serialize, Deserialize)]
490#[serde(rename_all = "lowercase")]
491pub enum OperationAction {
492 /// Send message
493 Send,
494 /// Receive message
495 Receive,
496}
497
498/// Reference to a channel
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct ChannelRef {
501 /// $ref path
502 #[serde(rename = "$ref")]
503 pub reference: String,
504}
505
506/// Reusable components
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct Components {
509 /// Message definitions
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub messages: Option<IndexMap<String, Message>>,
512
513 /// Schema definitions
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub schemas: Option<IndexMap<String, Schema>>,
516}
517
518/// JSON Schema object
519///
520/// Flexible representation that can hold any valid JSON Schema. This type supports
521/// both schema references (using `$ref`) and complete inline schema definitions.
522///
523/// Schemas define the structure and validation rules for message payloads,
524/// following the JSON Schema specification.
525///
526/// # Example
527///
528/// ## Reference Schema
529///
530/// ```rust
531/// use asyncapi_rust_models::Schema;
532///
533/// let schema = Schema::Reference {
534/// reference: "#/components/schemas/ChatMessage".to_string(),
535/// };
536/// ```
537///
538/// ## Object Schema
539///
540/// ```rust
541/// use asyncapi_rust_models::{Schema, SchemaObject};
542/// use indexmap::IndexMap;
543///
544/// let schema = Schema::Object(Box::new(SchemaObject {
545/// schema_type: Some(serde_json::json!("object")),
546/// properties: None,
547/// required: Some(vec!["username".to_string(), "room".to_string()]),
548/// description: Some("A chat message".to_string()),
549/// title: Some("ChatMessage".to_string()),
550/// enum_values: None,
551/// const_value: None,
552/// items: None,
553/// additional_properties: None,
554/// one_of: None,
555/// any_of: None,
556/// all_of: None,
557/// additional: IndexMap::new(),
558/// }));
559/// ```
560#[derive(Debug, Clone, Serialize, Deserialize)]
561#[serde(untagged)]
562pub enum Schema {
563 /// Reference to another schema ($ref)
564 ///
565 /// Points to a reusable schema definition in the components section.
566 /// Format: "#/components/schemas/{schemaName}"
567 Reference {
568 /// $ref path
569 #[serde(rename = "$ref")]
570 reference: String,
571 },
572 /// Full schema object (boxed to reduce enum size)
573 ///
574 /// Contains a complete JSON Schema definition with all properties inline
575 Object(Box<SchemaObject>),
576 /// Catch-all for valid JSON Schemas that don't match the above variants
577 ///
578 /// Handles minimal schemas like `{}`, `{"title": "..."}`, or boolean schemas
579 /// (`true`/`false`) that are valid per the JSON Schema spec but carry no
580 /// structural information. `schemars` emits these for open-ended types such
581 /// as `serde_json::Value`.
582 Any(serde_json::Value),
583}
584
585/// Schema object with all JSON Schema properties
586///
587/// Complete representation of a JSON Schema with support for all standard properties.
588/// This struct provides fine-grained control over schema definitions for message payloads.
589///
590/// # Example
591///
592/// ```rust
593/// use asyncapi_rust_models::{Schema, SchemaObject};
594/// use indexmap::IndexMap;
595///
596/// // String property schema
597/// let username_schema = Schema::Object(Box::new(SchemaObject {
598/// schema_type: Some(serde_json::json!("string")),
599/// properties: None,
600/// required: None,
601/// description: Some("User's display name".to_string()),
602/// title: None,
603/// enum_values: None,
604/// const_value: None,
605/// items: None,
606/// additional_properties: None,
607/// one_of: None,
608/// any_of: None,
609/// all_of: None,
610/// additional: IndexMap::new(),
611/// }));
612///
613/// // Object schema with properties
614/// let mut properties = IndexMap::new();
615/// properties.insert("username".to_string(), Box::new(username_schema));
616///
617/// let message_schema = SchemaObject {
618/// schema_type: Some(serde_json::json!("object")),
619/// properties: Some(properties),
620/// required: Some(vec!["username".to_string()]),
621/// description: Some("A chat message".to_string()),
622/// title: Some("ChatMessage".to_string()),
623/// enum_values: None,
624/// const_value: None,
625/// items: None,
626/// additional_properties: None,
627/// one_of: None,
628/// any_of: None,
629/// all_of: None,
630/// additional: IndexMap::new(),
631/// };
632/// ```
633#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct SchemaObject {
635 /// Schema type
636 ///
637 /// The JSON Schema type: "object", "array", "string", "number", "integer", "boolean", "null"
638 /// Can also be an array of types for schemas that allow multiple types (e.g., ["string", "null"])
639 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
640 pub schema_type: Option<serde_json::Value>,
641
642 /// Properties (for object type)
643 ///
644 /// Map of property names to their schemas when schema_type is "object"
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub properties: Option<IndexMap<String, Box<Schema>>>,
647
648 /// Required properties
649 ///
650 /// List of property names that must be present (for object types)
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub required: Option<Vec<String>>,
653
654 /// Description
655 ///
656 /// Human-readable description of what this schema represents
657 #[serde(skip_serializing_if = "Option::is_none")]
658 pub description: Option<String>,
659
660 /// Title
661 ///
662 /// A short title for the schema
663 #[serde(skip_serializing_if = "Option::is_none")]
664 pub title: Option<String>,
665
666 /// Enum values
667 ///
668 /// List of allowed values (for enum types)
669 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
670 pub enum_values: Option<Vec<serde_json::Value>>,
671
672 /// Const value
673 ///
674 /// A single constant value that this schema must match
675 #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
676 pub const_value: Option<serde_json::Value>,
677
678 /// Items schema (for array type)
679 ///
680 /// Schema for array elements when schema_type is "array"
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub items: Option<Box<Schema>>,
683
684 /// Additional properties
685 ///
686 /// Schema for additional properties not explicitly defined (for object types)
687 #[serde(
688 rename = "additionalProperties",
689 skip_serializing_if = "Option::is_none"
690 )]
691 pub additional_properties: Option<Box<Schema>>,
692
693 /// OneOf schemas
694 ///
695 /// Value must match exactly one of these schemas (XOR logic)
696 #[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
697 pub one_of: Option<Vec<Schema>>,
698
699 /// AnyOf schemas
700 ///
701 /// Value must match at least one of these schemas (OR logic)
702 #[serde(rename = "anyOf", skip_serializing_if = "Option::is_none")]
703 pub any_of: Option<Vec<Schema>>,
704
705 /// AllOf schemas
706 ///
707 /// Value must match all of these schemas (AND logic)
708 #[serde(rename = "allOf", skip_serializing_if = "Option::is_none")]
709 pub all_of: Option<Vec<Schema>>,
710
711 /// Additional fields that may be present in the schema
712 ///
713 /// Captures any additional JSON Schema properties not explicitly defined above
714 #[serde(flatten)]
715 pub additional: IndexMap<String, serde_json::Value>,
716}
717
718impl Default for AsyncApiSpec {
719 fn default() -> Self {
720 Self {
721 asyncapi: "3.0.0".to_string(),
722 info: Info {
723 title: "API".to_string(),
724 version: "1.0.0".to_string(),
725 description: None,
726 },
727 servers: None,
728 channels: None,
729 operations: None,
730 components: None,
731 }
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 #[test]
740 fn test_spec_serialization() {
741 let spec = AsyncApiSpec::default();
742 let json = serde_json::to_string(&spec).unwrap();
743 assert!(json.contains("asyncapi"));
744 assert!(json.contains("3.0.0"));
745 }
746
747 #[test]
748 fn test_spec_deserialization() {
749 let json = r#"{
750 "asyncapi": "3.0.0",
751 "info": {
752 "title": "Test API",
753 "version": "1.0.0"
754 }
755 }"#;
756 let spec: AsyncApiSpec = serde_json::from_str(json).unwrap();
757 assert_eq!(spec.asyncapi, "3.0.0");
758 assert_eq!(spec.info.title, "Test API");
759 }
760}