From b886ad744ec613fd494d136b82dc99cb723852ad Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 15:57:47 +0300 Subject: [PATCH 01/15] Fix ToAsyncApiMessage: Extract individual variant schemas instead of using oneOf for all messages (#1) * Add test example to demonstrate oneOf bug in ToAsyncApiMessage macro * Fix oneOf bug: extract individual variant schemas for enum messages * Add comprehensive test to verify oneOf fix for enum messages --- asyncapi-rust-codegen/src/lib.rs | 102 ++++++++++++++++---- asyncapi-rust/examples/schema_inspection.rs | 41 ++++++++ asyncapi-rust/examples/test_oneof.rs | 25 +++++ asyncapi-rust/tests/integration_test.rs | 56 ++++++++++- 4 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 asyncapi-rust/examples/schema_inspection.rs create mode 100644 asyncapi-rust/examples/test_oneof.rs diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 0d587d5..b1a8e2a 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -219,7 +219,7 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { } // Parse enum variants or struct - let messages = match &input.data { + let (messages, _is_enum) = match &input.data { Data::Enum(data_enum) => { let mut message_metas = Vec::new(); @@ -243,20 +243,23 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { }); } - message_metas + (message_metas, true) } Data::Struct(_) => { // For structs, extract metadata from the struct itself let asyncapi_meta = extract_asyncapi_meta(&input.attrs); - vec![MessageMeta { - name: name.to_string(), - summary: asyncapi_meta.summary, - description: asyncapi_meta.description, - title: asyncapi_meta.title, - content_type: asyncapi_meta.content_type, - triggers_binary: asyncapi_meta.triggers_binary, - }] + ( + vec![MessageMeta { + name: name.to_string(), + summary: asyncapi_meta.summary, + description: asyncapi_meta.description, + title: asyncapi_meta.title, + content_type: asyncapi_meta.content_type, + triggers_binary: asyncapi_meta.triggers_binary, + }], + false, + ) } Data::Union(_) => { return syn::Error::new_spanned(name, "ToAsyncApiMessage cannot be derived for unions") @@ -338,22 +341,79 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { let schema = schema_for!(Self); - // Convert schemars RootSchema to our Schema type + // Convert schemars RootSchema to JSON let schema_json = serde_json::to_value(&schema) .expect("Failed to serialize schema"); - let payload_schema: asyncapi_rust::Schema = serde_json::from_value(schema_json) - .expect("Failed to deserialize schema"); + // For enums, extract individual variant schemas from oneOf + let variant_schemas = if let Some(one_of_array) = schema_json.get("oneOf") { + if let Some(variants) = one_of_array.as_array() { + // Create a map of variant name to its schema with capacity + let mut variant_map = std::collections::HashMap::with_capacity(variants.len()); + + for variant in variants { + // Extract the const value from the type field + if let Some(properties) = variant.get("properties") { + if let Some(type_prop) = properties.get("type") { + if let Some(const_val) = type_prop.get("const") { + if let Some(variant_name) = const_val.as_str() { + // Convert this variant to a Schema + // Note: clone is necessary here because we need ownership + // of the JSON value to deserialize it + let variant_schema: asyncapi_rust::Schema = + serde_json::from_value(variant.clone()) + .unwrap_or_else(|e| panic!( + "Failed to deserialize schema for variant '{}': {}", + variant_name, e + )); + variant_map.insert(variant_name.to_string(), variant_schema); + } + } + } + } + } + + Some(variant_map) + } else { + None + } + } else { + None + }; // Create messages with metadata - vec![#(asyncapi_rust::Message { - name: Some(#message_names_for_gen.to_string()), - title: #message_titles, - summary: #message_summaries, - description: #message_descriptions, - content_type: #message_content_types, - payload: Some(payload_schema.clone()), - }),*] + let message_names = vec![#(#message_names_for_gen),*]; + let message_titles = vec![#(#message_titles),*]; + let message_summaries = vec![#(#message_summaries),*]; + let message_descriptions = vec![#(#message_descriptions),*]; + let message_content_types = vec![#(#message_content_types),*]; + + let mut messages = Vec::new(); + for i in 0..message_names.len() { + let msg_name = message_names[i]; + + // For enums, try to find the specific variant schema + let msg_payload = if let Some(ref variant_schemas) = variant_schemas { + // Try to get the specific variant schema for this message + variant_schemas.get(msg_name).cloned() + } else { + // For structs, deserialize and use the full schema + let payload_schema: asyncapi_rust::Schema = serde_json::from_value(schema_json.clone()) + .expect("Failed to deserialize schema"); + Some(payload_schema) + }; + + messages.push(asyncapi_rust::Message { + name: Some(msg_name.to_string()), + title: message_titles[i].clone(), + summary: message_summaries[i].clone(), + description: message_descriptions[i].clone(), + content_type: message_content_types[i].clone(), + payload: msg_payload, + }); + } + + messages } } }; diff --git a/asyncapi-rust/examples/schema_inspection.rs b/asyncapi-rust/examples/schema_inspection.rs new file mode 100644 index 0000000..4f193f1 --- /dev/null +++ b/asyncapi-rust/examples/schema_inspection.rs @@ -0,0 +1,41 @@ +use schemars::{JsonSchema, schema_for}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type")] +pub enum TestMessage { + Echo { text: String }, + Broadcast { room: String, text: String }, +} + +fn main() { + // Get the schema for the full enum + let root_schema = schema_for!(TestMessage); + println!("=== Full Enum Schema ==="); + let schema_json = serde_json::to_string_pretty(&root_schema).unwrap(); + println!("{}", schema_json); + + // Parse back to see the structure + let schema_value: serde_json::Value = serde_json::from_str(&schema_json).unwrap(); + + if let Some(one_of_array) = schema_value.get("oneOf") { + println!("\n=== Found oneOf ==="); + if let Some(variants) = one_of_array.as_array() { + println!("Number of oneOf variants: {}", variants.len()); + for (idx, variant) in variants.iter().enumerate() { + println!("\n--- Variant {} ---", idx); + + // Check if this variant has a const field for the type + if let Some(props) = variant.get("properties") { + if let Some(type_prop) = props.get("type") { + if let Some(const_val) = type_prop.get("const") { + println!("Type const value: {}", const_val); + } + } + } + + println!("{}", serde_json::to_string_pretty(variant).unwrap()); + } + } + } +} diff --git a/asyncapi-rust/examples/test_oneof.rs b/asyncapi-rust/examples/test_oneof.rs new file mode 100644 index 0000000..eda7ecd --- /dev/null +++ b/asyncapi-rust/examples/test_oneof.rs @@ -0,0 +1,25 @@ +use asyncapi_rust::{ToAsyncApiMessage, schemars::JsonSchema}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)] +#[serde(tag = "type")] +pub enum TestMessage { + Echo { text: String }, + Broadcast { room: String, text: String }, +} + +fn main() { + let messages = TestMessage::asyncapi_messages(); + + println!("Number of messages: {}", messages.len()); + + for (idx, msg) in messages.iter().enumerate() { + println!("\n=== Message {} ===", idx); + println!("Name: {:?}", msg.name); + println!("Payload:"); + if let Some(ref payload) = msg.payload { + let json = serde_json::to_string_pretty(payload).unwrap(); + println!("{}", json); + } + } +} diff --git a/asyncapi-rust/tests/integration_test.rs b/asyncapi-rust/tests/integration_test.rs index dbe7bdd..d84bff4 100644 --- a/asyncapi-rust/tests/integration_test.rs +++ b/asyncapi-rust/tests/integration_test.rs @@ -118,13 +118,59 @@ fn test_enum_schema_generation() { let messages = TaggedMessage::asyncapi_messages(); assert_eq!(messages.len(), 2); - // Each variant should have its own message - assert_eq!(messages[0].name, Some("Echo".to_string())); - assert_eq!(messages[1].name, Some("Broadcast".to_string())); + // Find messages by name (more maintainable than hard-coded indices) + let echo_msg = messages + .iter() + .find(|m| m.name.as_ref().is_some_and(|n| n == "Echo")) + .expect("Echo message should exist"); + + let broadcast_msg = messages + .iter() + .find(|m| m.name.as_ref().is_some_and(|n| n == "Broadcast")) + .expect("Broadcast message should exist"); // Both should have schemas - assert!(messages[0].payload.is_some()); - assert!(messages[1].payload.is_some()); + assert!(echo_msg.payload.is_some()); + assert!(broadcast_msg.payload.is_some()); + + // Verify that each message has ONLY its specific variant schema (no oneOf) + // Convert to JSON to inspect the structure + let echo_payload_json = serde_json::to_value(&echo_msg.payload).unwrap(); + let broadcast_payload_json = serde_json::to_value(&broadcast_msg.payload).unwrap(); + + // Echo message should NOT have oneOf at the top level + assert!( + echo_payload_json.get("oneOf").is_none(), + "Echo message should not have oneOf in its schema" + ); + + // Broadcast message should NOT have oneOf at the top level + assert!( + broadcast_payload_json.get("oneOf").is_none(), + "Broadcast message should not have oneOf in its schema" + ); + + // Echo message should have the const value "Echo" in its type property + if let Some(properties) = echo_payload_json.get("properties") { + if let Some(type_prop) = properties.get("type") { + assert_eq!( + type_prop.get("const"), + Some(&serde_json::json!("Echo")), + "Echo message should have const 'Echo' in type property" + ); + } + } + + // Broadcast message should have the const value "Broadcast" in its type property + if let Some(properties) = broadcast_payload_json.get("properties") { + if let Some(type_prop) = properties.get("type") { + assert_eq!( + type_prop.get("const"), + Some(&serde_json::json!("Broadcast")), + "Broadcast message should have const 'Broadcast' in type property" + ); + } + } } #[test] From e6a692a507b00b7f16f60ed0ceb70be1a8444c85 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:29:59 +0300 Subject: [PATCH 02/15] feat: add messages parameter to asyncapi_operation attribute * Add messages parameter to asyncapi_operation attribute * Update documentation for messages parameter in asyncapi_operation * Merge operation messages to channel messages field * Fix operation message references to point to channel messages --- .../src/asyncapi_spec_attrs.rs | 63 ++++++++++++++ asyncapi-rust-codegen/src/lib.rs | 79 ++++++++++++++++- asyncapi-rust/examples/message_integration.rs | 26 +++++- asyncapi-rust/src/lib.rs | 13 ++- asyncapi-rust/tests/integration_test.rs | 85 +++++++++++++++++++ 5 files changed, 257 insertions(+), 9 deletions(-) diff --git a/asyncapi-rust-codegen/src/asyncapi_spec_attrs.rs b/asyncapi-rust-codegen/src/asyncapi_spec_attrs.rs index 6f520c6..9e80398 100644 --- a/asyncapi-rust-codegen/src/asyncapi_spec_attrs.rs +++ b/asyncapi-rust-codegen/src/asyncapi_spec_attrs.rs @@ -62,6 +62,7 @@ pub struct OperationMeta { pub channel: String, #[allow(dead_code)] // Reserved for future use pub description: Option, + pub messages: Vec, } /// Extract asyncapi spec metadata from `#[asyncapi(...)]` attributes @@ -299,10 +300,14 @@ fn extract_channel_parameter(nested: &syn::meta::ParseNestedMeta) -> Option Option { + use syn::Token; + use syn::punctuated::Punctuated; + let mut name = None; let mut action = None; let mut channel = None; let mut description = None; + let mut messages = Vec::new(); let _ = attr.parse_nested_meta(|nested| { if nested.path.is_ident("name") { @@ -321,6 +326,14 @@ fn extract_operation(attr: &Attribute) -> Option { let value = nested.value()?; let s: syn::LitStr = value.parse()?; description = Some(s.value()); + } else if nested.path.is_ident("messages") { + // Parse array of type paths: messages = [Type1, Type2, ...] + let _ = nested.value()?; // Parse the equals sign and prepare for value parsing + let content; + syn::bracketed!(content in nested.input); + let types: Punctuated = + content.parse_terminated(|stream| stream.parse(), Token![,])?; + messages = types.into_iter().collect(); } Ok(()) }); @@ -331,6 +344,7 @@ fn extract_operation(attr: &Attribute) -> Option { action: action?, channel: channel?, description, + messages, }) } @@ -606,4 +620,53 @@ mod tests { assert_eq!(param1.schema_type, Some("integer".to_string())); assert_eq!(param1.format, Some("int64".to_string())); } + + #[test] + fn test_extract_operation_with_messages() { + let attrs: Vec = vec![parse_quote! { + #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [ChatMessage])] + }]; + + let meta = extract_asyncapi_spec_meta(&attrs); + assert_eq!(meta.operations.len(), 1); + assert_eq!(meta.operations[0].name, "sendMessage"); + assert_eq!(meta.operations[0].action, "send"); + assert_eq!(meta.operations[0].channel, "chat"); + assert_eq!(meta.operations[0].messages.len(), 1); + let path0 = &meta.operations[0].messages[0]; + assert_eq!(quote!(#path0).to_string(), "ChatMessage"); + } + + #[test] + fn test_extract_operation_with_multiple_messages() { + let attrs: Vec = vec![parse_quote! { + #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [ChatMessage, SystemMessage])] + }]; + + let meta = extract_asyncapi_spec_meta(&attrs); + assert_eq!(meta.operations.len(), 1); + assert_eq!(meta.operations[0].messages.len(), 2); + let path0 = &meta.operations[0].messages[0]; + let path1 = &meta.operations[0].messages[1]; + assert_eq!(quote!(#path0).to_string(), "ChatMessage"); + assert_eq!(quote!(#path1).to_string(), "SystemMessage"); + } + + #[test] + fn test_extract_operation_with_module_path_messages() { + let attrs: Vec = vec![parse_quote! { + #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [super::messages::ChatMessage, crate::SystemMessage])] + }]; + + let meta = extract_asyncapi_spec_meta(&attrs); + assert_eq!(meta.operations.len(), 1); + assert_eq!(meta.operations[0].messages.len(), 2); + let path0 = &meta.operations[0].messages[0]; + let path1 = &meta.operations[0].messages[1]; + assert_eq!( + quote!(#path0).to_string(), + "super :: messages :: ChatMessage" + ); + assert_eq!(quote!(#path1).to_string(), "crate :: SystemMessage"); + } } diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index b1a8e2a..15bb323 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -75,13 +75,16 @@ //! #[asyncapi_operation( //! name = "sendMessage", //! action = "send", -//! channel = "chat" +//! channel = "chat", +//! messages = [ChatMessage] //! )] //! #[asyncapi_operation( //! name = "receiveMessage", //! action = "receive", -//! channel = "chat" +//! channel = "chat", +//! messages = [ChatMessage, SystemMessage] //! )] +//! #[asyncapi_messages(ChatMessage, SystemMessage)] //! struct ChatApi; //! //! // Generated method: @@ -131,6 +134,12 @@ //! - `name = "..."` - Operation identifier (required) //! - `action = "send"|"receive"` - Operation type (required) //! - `channel = "..."` - Channel reference (required) +//! - `messages = [Type1, Type2, ...]` - Message types available for this operation (optional) +//! +//! When the `messages` parameter is specified on operations, those messages are automatically +//! added to the channel that the operation references. Operation messages reference the channel's +//! messages (e.g., `#/channels/{channel}/messages/{message}`), while channel messages reference +//! the components section (e.g., `#/components/messages/{message}`), following AsyncAPI 3.0 spec. //! //! ## Integration with serde //! @@ -656,12 +665,50 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { } }; + // Collect messages from all operations that reference this channel + let channel_name_str = name.as_str(); + let operations_for_channel: Vec<_> = spec_meta.operations.iter() + .filter(|op| op.channel == channel_name_str) + .collect(); + + let messages_field = if operations_for_channel.is_empty() || + operations_for_channel.iter().all(|op| op.messages.is_empty()) { + quote! { None } + } else { + let message_calls: Vec<_> = operations_for_channel.iter() + .flat_map(|op| &op.messages) + .collect::>() // Deduplicate + .into_iter() + .map(|type_name| { + quote! { + // Call asyncapi_message_names() for this type and add references + for msg_name in #type_name::asyncapi_message_names() { + channel_messages.insert( + msg_name.to_string(), + asyncapi_rust::MessageRef::Reference { + reference: format!("#/components/messages/{}", msg_name), + } + ); + } + } + }) + .collect(); + + quote! { + { + let mut channel_messages = std::collections::HashMap::new(); + #(#message_calls)* + Some(channel_messages) + } + } + }; + quote! { channels.insert( #name.to_string(), asyncapi_rust::Channel { address: #address, - messages: None, + messages: #messages_field, parameters: #parameters, } ); @@ -699,6 +746,30 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { .to_compile_error(); }; + // Generate messages references if any messages are specified + let messages_field = if operation.messages.is_empty() { + quote! { None } + } else { + let message_calls = operation.messages.iter().map(|type_name| { + quote! { + // Call asyncapi_message_names() for this type and add references to channel messages + for msg_name in #type_name::asyncapi_message_names() { + message_refs.push(asyncapi_rust::MessageRef::Reference { + reference: format!("#/channels/{}/messages/{}", #channel_ref, msg_name), + }); + } + } + }); + + quote! { + { + let mut message_refs = Vec::new(); + #(#message_calls)* + Some(message_refs) + } + } + }; + quote! { operations.insert( #name.to_string(), @@ -707,7 +778,7 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { channel: asyncapi_rust::ChannelRef { reference: format!("#/channels/{}", #channel_ref), }, - messages: None, + messages: #messages_field, } ); } diff --git a/asyncapi-rust/examples/message_integration.rs b/asyncapi-rust/examples/message_integration.rs index 5f53e6b..45d3ce3 100644 --- a/asyncapi-rust/examples/message_integration.rs +++ b/asyncapi-rust/examples/message_integration.rs @@ -59,6 +59,12 @@ pub enum SystemMessage { /// /// The #[asyncapi_messages(...)] attribute automatically includes /// message definitions from the specified types in the components section. +/// The messages parameter in operations specifies which messages can be used, +/// and these messages are automatically added to the channel that the operation references. +/// +/// Per AsyncAPI 3.0 spec: +/// - Operations reference channel messages: #/channels/{channel}/messages/{message} +/// - Channels reference component messages: #/components/messages/{message} #[allow(clippy::duplicated_attributes)] #[derive(AsyncApi)] #[asyncapi( @@ -73,8 +79,8 @@ pub enum SystemMessage { description = "Production WebSocket server" )] #[asyncapi_channel(name = "chat", address = "/ws/chat")] -#[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat")] -#[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat")] +#[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [ChatMessage])] +#[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat", messages = [ChatMessage, SystemMessage])] #[asyncapi_messages(ChatMessage, SystemMessage)] struct ChatApi; @@ -107,6 +113,14 @@ fn main() { if let Some(address) = &channel.address { println!(" - {}: {}", name, address); } + if let Some(messages) = &channel.messages { + println!(" Channel Messages:"); + for (msg_name, msg_ref) in messages { + if let asyncapi_rust::MessageRef::Reference { reference } = msg_ref { + println!(" - {}: {}", msg_name, reference); + } + } + } } println!(); } @@ -123,6 +137,14 @@ fn main() { " - {}: {} to {}", name, action, operation.channel.reference ); + if let Some(messages) = &operation.messages { + println!(" Messages:"); + for msg in messages { + if let asyncapi_rust::MessageRef::Reference { reference } = msg { + println!(" - {}", reference); + } + } + } } println!(); } diff --git a/asyncapi-rust/src/lib.rs b/asyncapi-rust/src/lib.rs index 7b6c835..e6cfe21 100644 --- a/asyncapi-rust/src/lib.rs +++ b/asyncapi-rust/src/lib.rs @@ -42,8 +42,9 @@ //! #[asyncapi(title = "Chat API", version = "1.0.0")] //! #[asyncapi_server(name = "production", host = "api.example.com", protocol = "wss")] //! #[asyncapi_channel(name = "chat", address = "/ws/chat")] -//! #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat")] -//! #[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat")] +//! #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [ChatMessage])] +//! #[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat", messages = [ChatMessage])] +//! #[asyncapi_messages(ChatMessage)] //! struct ChatApi; //! //! fn main() { @@ -76,7 +77,13 @@ //! - `#[asyncapi(...)]` - Basic info (title, version, description) //! - `#[asyncapi_server(...)]` - Server definitions //! - `#[asyncapi_channel(...)]` - Channel definitions -//! - `#[asyncapi_operation(...)]` - Operation definitions +//! - `#[asyncapi_operation(...)]` - Operation definitions (with optional `messages` parameter) +//! - `#[asyncapi_messages(...)]` - Include message types in components +//! +//! When you specify messages in operations, they are automatically added to the channel +//! that the operation references. Operations reference channel messages +//! (`#/channels/{channel}/messages/{message}`), while channels reference components +//! (`#/components/messages/{message}`), following AsyncAPI 3.0 specification. //! //! ## Framework Integration //! diff --git a/asyncapi-rust/tests/integration_test.rs b/asyncapi-rust/tests/integration_test.rs index d84bff4..94f41d0 100644 --- a/asyncapi-rust/tests/integration_test.rs +++ b/asyncapi-rust/tests/integration_test.rs @@ -408,3 +408,88 @@ fn test_asyncapi_with_messages() { assert_eq!(system_status.name, Some("system.status".to_string())); assert_eq!(system_status.summary, Some("System status".to_string())); } + +#[test] +fn test_asyncapi_operation_with_messages() { + // Define message types for operations + #[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)] + #[serde(tag = "type")] + pub enum ChatMessage { + #[serde(rename = "user.join")] + UserJoin { username: String, room: String }, + #[serde(rename = "chat.message")] + ChatMessage { username: String, text: String }, + } + + #[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)] + #[serde(tag = "type")] + pub enum SystemMessage { + #[serde(rename = "system.status")] + Status { status: String }, + } + + // Define API with operations that specify messages + #[allow(clippy::duplicated_attributes)] + #[derive(AsyncApi)] + #[asyncapi(title = "Chat API", version = "1.0.0")] + #[asyncapi_channel(name = "chat", address = "/ws/chat")] + #[asyncapi_operation(name = "sendMessage", action = "send", channel = "chat", messages = [ChatMessage])] + #[asyncapi_operation(name = "receiveMessage", action = "receive", channel = "chat", messages = [ChatMessage, SystemMessage])] + #[asyncapi_messages(ChatMessage, SystemMessage)] + struct ChatApi; + + let spec = ChatApi::asyncapi_spec(); + + // Verify operations exist + let operations = spec.operations.expect("Should have operations"); + assert_eq!(operations.len(), 2); + + // Verify sendMessage operation has messages + let send_op = operations + .get("sendMessage") + .expect("Should have sendMessage operation"); + assert!(send_op.messages.is_some()); + let send_messages = send_op.messages.as_ref().unwrap(); + assert_eq!(send_messages.len(), 2); // ChatMessage has 2 variants + + // Verify receiveMessage operation has messages + let receive_op = operations + .get("receiveMessage") + .expect("Should have receiveMessage operation"); + assert!(receive_op.messages.is_some()); + let receive_messages = receive_op.messages.as_ref().unwrap(); + assert_eq!(receive_messages.len(), 3); // ChatMessage (2 variants) + SystemMessage (1 variant) + + // Verify operation message references point to channel messages (not components directly) + match &send_messages[0] { + asyncapi_rust::MessageRef::Reference { reference } => { + assert!( + reference == "#/channels/chat/messages/user.join" + || reference == "#/channels/chat/messages/chat.message" + ); + } + _ => panic!("Expected message reference"), + } + + // Verify channels exist and have messages + let channels = spec.channels.expect("Should have channels"); + assert_eq!(channels.len(), 1); + + let chat_channel = channels.get("chat").expect("Should have chat channel"); + assert!(chat_channel.messages.is_some()); + let channel_messages = chat_channel.messages.as_ref().unwrap(); + assert_eq!(channel_messages.len(), 3); // All unique messages from both operations + + // Verify channel messages reference components directly + assert!(channel_messages.contains_key("user.join")); + assert!(channel_messages.contains_key("chat.message")); + assert!(channel_messages.contains_key("system.status")); + + // Verify channel messages reference components (not other channels) + match channel_messages.get("user.join").unwrap() { + asyncapi_rust::MessageRef::Reference { reference } => { + assert_eq!(reference, "#/components/messages/user.join"); + } + _ => panic!("Expected message reference"), + } +} From 6b3b04bfc762043a5c3d8c1ca78b79631570d4d4 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Tue, 23 Jun 2026 08:46:26 +0200 Subject: [PATCH 03/15] fix: Enum derive --- asyncapi-rust-codegen/src/lib.rs | 37 +------------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 4bd97b2..fe85465 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -226,42 +226,7 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { // Parse enum variants or struct let messages = match &input.data { - Data::Enum(data_enum) => { - let mut message_metas = Vec::new(); - - for variant in &data_enum.variants { - let variant_name = &variant.ident; - let variant_ident_str = variant_name.to_string(); - - // Wire discriminant: serde rename if present (even if empty), else variant ident. - let discriminant = extract_serde_rename(&variant.attrs) - .unwrap_or_else(|| variant_ident_str.clone()); - - // Extract asyncapi metadata - let asyncapi_meta = extract_asyncapi_meta(&variant.attrs); - - // Message identity: explicit message_name override, else variant ident. - // We deliberately do NOT use the serde rename here — it may be empty, - // non-unique across enums, or unsuitable as a code identifier. - let message_name = asyncapi_meta - .message_name - .clone() - .unwrap_or_else(|| variant_ident_str.clone()); - - message_metas.push(MessageMeta { - name: message_name, - discriminant, - summary: asyncapi_meta.summary, - description: asyncapi_meta.description, - title: asyncapi_meta.title, - content_type: asyncapi_meta.content_type, - triggers_binary: asyncapi_meta.triggers_binary, - }); - } - - message_metas - } - Data::Struct(_) => { + Data::Struct(_) | Data::Enum(_) => { // For structs, extract metadata from the struct itself let asyncapi_meta = extract_asyncapi_meta(&input.attrs); let struct_name = name.to_string(); From 33cd79f23504270687c44dcf1c66eae07a1c0a3f Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Thu, 9 Jul 2026 12:15:53 +0200 Subject: [PATCH 04/15] Revert "fix: Enum derive" This reverts commit 6b3b04bfc762043a5c3d8c1ca78b79631570d4d4. --- asyncapi-rust-codegen/src/lib.rs | 60 ++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 441d652..e2716ed 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -234,8 +234,43 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { } // Parse enum variants or struct - let (messages, _is_enum) = match &input.data { - Data::Struct(_) | Data::Enum(_) => { + let messages = match &input.data { + Data::Enum(data_enum) => { + let mut message_metas = Vec::new(); + + for variant in &data_enum.variants { + let variant_name = &variant.ident; + let variant_ident_str = variant_name.to_string(); + + // Wire discriminant: serde rename if present (even if empty), else variant ident. + let discriminant = extract_serde_rename(&variant.attrs) + .unwrap_or_else(|| variant_ident_str.clone()); + + // Extract asyncapi metadata + let asyncapi_meta = extract_asyncapi_meta(&variant.attrs); + + // Message identity: explicit message_name override, else variant ident. + // We deliberately do NOT use the serde rename here — it may be empty, + // non-unique across enums, or unsuitable as a code identifier. + let message_name = asyncapi_meta + .message_name + .clone() + .unwrap_or_else(|| variant_ident_str.clone()); + + message_metas.push(MessageMeta { + name: message_name, + discriminant, + summary: asyncapi_meta.summary, + description: asyncapi_meta.description, + title: asyncapi_meta.title, + content_type: asyncapi_meta.content_type, + triggers_binary: asyncapi_meta.triggers_binary, + }); + } + + message_metas + } + Data::Struct(_) => { // For structs, extract metadata from the struct itself let asyncapi_meta = extract_asyncapi_meta(&input.attrs); let struct_name = name.to_string(); @@ -244,18 +279,15 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { .clone() .unwrap_or_else(|| struct_name.clone()); - ( - vec![MessageMeta { - name: message_name, - discriminant: struct_name, - summary: asyncapi_meta.summary, - description: asyncapi_meta.description, - title: asyncapi_meta.title, - content_type: asyncapi_meta.content_type, - triggers_binary: asyncapi_meta.triggers_binary, - }], - false, - ) + vec![MessageMeta { + name: message_name, + discriminant: struct_name, + summary: asyncapi_meta.summary, + description: asyncapi_meta.description, + title: asyncapi_meta.title, + content_type: asyncapi_meta.content_type, + triggers_binary: asyncapi_meta.triggers_binary, + }] } Data::Union(_) => { return syn::Error::new_spanned(name, "ToAsyncApiMessage cannot be derived for unions") From 7155f41b6f358a5b9f80b49271ddbef276fd4be3 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Thu, 3 Sep 2026 18:14:22 +0200 Subject: [PATCH 05/15] Fixed wrong references in integration test asserts --- asyncapi-rust/examples/message_integration.rs | 14 ++++++++++++-- asyncapi-rust/tests/integration_test.rs | 17 ++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/asyncapi-rust/examples/message_integration.rs b/asyncapi-rust/examples/message_integration.rs index 45d3ce3..08b129c 100644 --- a/asyncapi-rust/examples/message_integration.rs +++ b/asyncapi-rust/examples/message_integration.rs @@ -10,6 +10,12 @@ use asyncapi_rust::{AsyncApi, ToAsyncApiMessage, schemars::JsonSchema}; use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct UserJoin { + pub username: String, + pub room: String, +} + /// Chat messages for a chat application #[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)] #[serde(tag = "type")] @@ -20,7 +26,8 @@ pub enum ChatMessage { summary = "User joins", description = "Sent when a user enters a chat room" )] - UserJoin { username: String, room: String }, + UserJoin(UserJoin), + UserJoin2(UserJoin), /// User sends a chat message #[serde(rename = "chat.message")] @@ -37,7 +44,10 @@ pub enum ChatMessage { /// User leaves a chat room #[serde(rename = "user.leave")] #[asyncapi(summary = "User leaves", description = "Sent when a user exits a room")] - UserLeave { username: String, room: String }, + UserLeave { + username: String, + room: String, + }, } /// System messages for status and errors diff --git a/asyncapi-rust/tests/integration_test.rs b/asyncapi-rust/tests/integration_test.rs index e7db810..c5b541c 100644 --- a/asyncapi-rust/tests/integration_test.rs +++ b/asyncapi-rust/tests/integration_test.rs @@ -472,6 +472,8 @@ fn test_asyncapi_operation_with_messages() { let spec = ChatApi::asyncapi_spec(); + println!("{}", serde_json::to_string_pretty(&spec).unwrap()); + // Verify operations exist let operations = spec.operations.expect("Should have operations"); assert_eq!(operations.len(), 2); @@ -495,9 +497,10 @@ fn test_asyncapi_operation_with_messages() { // Verify operation message references point to channel messages (not components directly) match &send_messages[0] { asyncapi_rust::MessageRef::Reference { reference } => { + println!("{}", reference); assert!( - reference == "#/channels/chat/messages/user.join" - || reference == "#/channels/chat/messages/chat.message" + reference == "#/channels/chat/messages/UserJoin" + || reference == "#/channels/chat/messages/ChatMessage" ); } _ => panic!("Expected message reference"), @@ -513,14 +516,14 @@ fn test_asyncapi_operation_with_messages() { assert_eq!(channel_messages.len(), 3); // All unique messages from both operations // Verify channel messages reference components directly - assert!(channel_messages.contains_key("user.join")); - assert!(channel_messages.contains_key("chat.message")); - assert!(channel_messages.contains_key("system.status")); + assert!(channel_messages.contains_key("UserJoin")); + assert!(channel_messages.contains_key("ChatMessage")); + assert!(channel_messages.contains_key("Status")); // Verify channel messages reference components (not other channels) - match channel_messages.get("user.join").unwrap() { + match channel_messages.get("UserJoin").unwrap() { asyncapi_rust::MessageRef::Reference { reference } => { - assert_eq!(reference, "#/components/messages/user.join"); + assert_eq!(reference, "#/components/messages/UserJoin"); } _ => panic!("Expected message reference"), } From 6a427c343989226392b07ff61f5923f4a7c2587d Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Thu, 3 Sep 2026 18:24:30 +0200 Subject: [PATCH 06/15] Removed unused enum variant --- asyncapi-rust/examples/message_integration.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/asyncapi-rust/examples/message_integration.rs b/asyncapi-rust/examples/message_integration.rs index 08b129c..17dc2b5 100644 --- a/asyncapi-rust/examples/message_integration.rs +++ b/asyncapi-rust/examples/message_integration.rs @@ -27,7 +27,6 @@ pub enum ChatMessage { description = "Sent when a user enters a chat room" )] UserJoin(UserJoin), - UserJoin2(UserJoin), /// User sends a chat message #[serde(rename = "chat.message")] @@ -44,10 +43,7 @@ pub enum ChatMessage { /// User leaves a chat room #[serde(rename = "user.leave")] #[asyncapi(summary = "User leaves", description = "Sent when a user exits a room")] - UserLeave { - username: String, - room: String, - }, + UserLeave { username: String, room: String }, } /// System messages for status and errors From 5c95f22763201c490db9c2692b63e5570b8bc35c Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 4 Sep 2026 07:42:33 +0200 Subject: [PATCH 07/15] Fixed by preserving the message order. --- asyncapi-rust-codegen/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 70e55cd..15313fd 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -950,9 +950,7 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { } else { let message_calls: Vec<_> = operations_for_channel .iter() - .flat_map(|op| &op.messages) - .collect::>() // Deduplicate - .into_iter() + .flat_map(|op| &op.messages) // Deduplicate .map(|type_name| { quote! { // Call asyncapi_message_names() for this type and add references From 3196c7003c6a2b2f7105d52b52732393fd61de0b Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 4 Sep 2026 16:50:11 +0200 Subject: [PATCH 08/15] remove oneof example --- asyncapi-rust/examples/test_oneof.rs | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 asyncapi-rust/examples/test_oneof.rs diff --git a/asyncapi-rust/examples/test_oneof.rs b/asyncapi-rust/examples/test_oneof.rs deleted file mode 100644 index eda7ecd..0000000 --- a/asyncapi-rust/examples/test_oneof.rs +++ /dev/null @@ -1,25 +0,0 @@ -use asyncapi_rust::{ToAsyncApiMessage, schemars::JsonSchema}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)] -#[serde(tag = "type")] -pub enum TestMessage { - Echo { text: String }, - Broadcast { room: String, text: String }, -} - -fn main() { - let messages = TestMessage::asyncapi_messages(); - - println!("Number of messages: {}", messages.len()); - - for (idx, msg) in messages.iter().enumerate() { - println!("\n=== Message {} ===", idx); - println!("Name: {:?}", msg.name); - println!("Payload:"); - if let Some(ref payload) = msg.payload { - let json = serde_json::to_string_pretty(payload).unwrap(); - println!("{}", json); - } - } -} From 009c21680f7d51c9a5f20ebbed5ed91cdad255a0 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 4 Sep 2026 16:50:26 +0200 Subject: [PATCH 09/15] revert UserJoin variant --- asyncapi-rust/examples/message_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncapi-rust/examples/message_integration.rs b/asyncapi-rust/examples/message_integration.rs index 17dc2b5..bc4a82f 100644 --- a/asyncapi-rust/examples/message_integration.rs +++ b/asyncapi-rust/examples/message_integration.rs @@ -26,7 +26,7 @@ pub enum ChatMessage { summary = "User joins", description = "Sent when a user enters a chat room" )] - UserJoin(UserJoin), + UserJoin { username: String, room: String }, /// User sends a chat message #[serde(rename = "chat.message")] From 70a73e35743de7313bc3402fe6d01bb8e250d72d Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 4 Sep 2026 16:51:08 +0200 Subject: [PATCH 10/15] remove leftover example file --- asyncapi-rust/examples/schema_inspection.rs | 41 --------------------- 1 file changed, 41 deletions(-) delete mode 100644 asyncapi-rust/examples/schema_inspection.rs diff --git a/asyncapi-rust/examples/schema_inspection.rs b/asyncapi-rust/examples/schema_inspection.rs deleted file mode 100644 index 4f193f1..0000000 --- a/asyncapi-rust/examples/schema_inspection.rs +++ /dev/null @@ -1,41 +0,0 @@ -use schemars::{JsonSchema, schema_for}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type")] -pub enum TestMessage { - Echo { text: String }, - Broadcast { room: String, text: String }, -} - -fn main() { - // Get the schema for the full enum - let root_schema = schema_for!(TestMessage); - println!("=== Full Enum Schema ==="); - let schema_json = serde_json::to_string_pretty(&root_schema).unwrap(); - println!("{}", schema_json); - - // Parse back to see the structure - let schema_value: serde_json::Value = serde_json::from_str(&schema_json).unwrap(); - - if let Some(one_of_array) = schema_value.get("oneOf") { - println!("\n=== Found oneOf ==="); - if let Some(variants) = one_of_array.as_array() { - println!("Number of oneOf variants: {}", variants.len()); - for (idx, variant) in variants.iter().enumerate() { - println!("\n--- Variant {} ---", idx); - - // Check if this variant has a const field for the type - if let Some(props) = variant.get("properties") { - if let Some(type_prop) = props.get("type") { - if let Some(const_val) = type_prop.get("const") { - println!("Type const value: {}", const_val); - } - } - } - - println!("{}", serde_json::to_string_pretty(variant).unwrap()); - } - } - } -} From 483bddbb3793bd34da4cf45bc6b7fe6e118a32f4 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 4 Sep 2026 16:53:05 +0200 Subject: [PATCH 11/15] fixed idention in quote --- asyncapi-rust-codegen/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 15313fd..6f4311a 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -511,11 +511,11 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { { use schemars::schema_for; - let schema = schema_for!(Self); + let schema = schema_for!(Self); - // Convert schemars RootSchema to JSON - let schema_json = serde_json::to_value(&schema) - .expect("Failed to serialize schema"); + // Convert schemars RootSchema to JSON + let schema_json = serde_json::to_value(&schema) + .expect("Failed to serialize schema"); // Build a discriminant→schema map using the actual serde tag field name. let tag_field = Self::asyncapi_tag_field(); From a96552651c7f0986f160e876ad2d0be621f213bb Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Mon, 7 Sep 2026 07:22:29 +0200 Subject: [PATCH 12/15] Always include operation messages in the components. This makes it optionally to define the messages in asyncapi_messages(...) --- asyncapi-rust-codegen/src/lib.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 6f4311a..6e63ad2 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -1127,7 +1127,19 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { let components_code = if spec_meta.message_types.is_empty() { quote! { None } } else { - let type_calls = spec_meta.message_types.iter().map(|type_name| { + let mut all_message_types = spec_meta.message_types.clone(); + + for type_name in spec_meta + .operations + .iter() + .flat_map(|operation| operation.messages.iter()) + { + if !all_message_types.contains(type_name) { + all_message_types.push(type_name.clone()); + } + } + + let type_calls = all_message_types.iter().map(|type_name| { quote! { for msg in #type_name::asyncapi_messages() { if let Some(ref name) = msg.name { From 49d7ffec48a5c8347696b4a7169f70fc435f5153 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Mon, 7 Sep 2026 07:22:56 +0200 Subject: [PATCH 13/15] Dedup operation messages --- asyncapi-rust-codegen/src/lib.rs | 36 ++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 6e63ad2..99a751e 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -1022,22 +1022,36 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { let messages_field = if operation.messages.is_empty() { quote! { None } } else { - let message_calls = operation.messages.iter().map(|type_name| { - quote! { - // Call asyncapi_message_names() for this type and add references to channel messages - for msg_name in #type_name::asyncapi_message_names() { - message_refs.push(asyncapi_rust::MessageRef::Reference { - reference: format!("#/channels/{}/messages/{}", #channel_ref, msg_name), - }); + let message_calls: Vec<_> = operation + .messages + .iter() + .map(|type_name| { + quote! { + for msg_name in #type_name::asyncapi_message_names() { + message_refs + .entry(msg_name.to_string()) + .or_insert_with(|| { + asyncapi_rust::MessageRef::Reference { + reference: format!( + "#/channels/{}/messages/{}", + #channel_ref, + msg_name + ), + } + }); + } } - } - }); + }) + .collect(); quote! { { - let mut message_refs = Vec::new(); + let mut message_refs = + asyncapi_rust::indexmap::IndexMap::new(); + #(#message_calls)* - Some(message_refs) + + Some(message_refs.into_values().collect()) } } }; From 2ce3569b3a462da9f30740095e9795fe09eb7eb9 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Mon, 7 Sep 2026 07:23:06 +0200 Subject: [PATCH 14/15] Check if operation references an existing channel --- asyncapi-rust-codegen/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 99a751e..9b6dc88 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -1005,6 +1005,22 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { let channel_ref = &operation.channel; let action = &operation.action; + // Validate that the operation references a declared channel + if !spec_meta + .channels + .iter() + .any(|channel| channel.name == *channel_ref) + { + return syn::Error::new_spanned( + channel_ref, + format!( + "Operation '{}' references unknown channel '{}'", + name, channel_ref + ), + ) + .to_compile_error(); + } + // Convert action string to OperationAction enum let action_enum = if action == "send" { quote! { asyncapi_rust::OperationAction::Send } From 3a2a592fedddf0e274ddb0f9176966159c898ff7 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Wed, 9 Sep 2026 08:27:07 +0200 Subject: [PATCH 15/15] Hoist all messages list construction above the guard --- asyncapi-rust-codegen/src/lib.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/asyncapi-rust-codegen/src/lib.rs b/asyncapi-rust-codegen/src/lib.rs index 9b6dc88..791bf0f 100644 --- a/asyncapi-rust-codegen/src/lib.rs +++ b/asyncapi-rust-codegen/src/lib.rs @@ -1154,21 +1154,21 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { }; // Generate components with messages and hoisted shared schemas - let components_code = if spec_meta.message_types.is_empty() { - quote! { None } - } else { - let mut all_message_types = spec_meta.message_types.clone(); - - for type_name in spec_meta - .operations - .iter() - .flat_map(|operation| operation.messages.iter()) - { - if !all_message_types.contains(type_name) { - all_message_types.push(type_name.clone()); - } + let mut all_message_types = spec_meta.message_types.clone(); + + for type_name in spec_meta + .operations + .iter() + .flat_map(|operation| operation.messages.iter()) + { + if !all_message_types.contains(type_name) { + all_message_types.push(type_name.clone()); } + } + let components_code = if all_message_types.is_empty() { + quote! { None } + } else { let type_calls = all_message_types.iter().map(|type_name| { quote! { for msg in #type_name::asyncapi_messages() {