Repository navigation
Derive channel and operation messages #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b886ad7
e6a692a
6b3b04b
9b32695
33cd79f
965b962
7155f41
6a427c3
5c95f22
3196c70
009c216
70a73e3
483bddb
a965526
49d7ffe
2ce3569
3a2a592
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| //! | ||
|
|
@@ -503,6 +512,8 @@ pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream { | |
| use schemars::schema_for; | ||
|
|
||
| let schema = schema_for!(Self); | ||
|
|
||
| // Convert schemars RootSchema to JSON | ||
| let schema_json = serde_json::to_value(&schema) | ||
| .expect("Failed to serialize schema"); | ||
|
|
||
|
|
@@ -922,12 +933,54 @@ 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() | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No validation that Components messages are generated only from Verified: a struct with only
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I modified the components generation code and included all operation messages. Hence messages declared in operations always land in the components. This would make using |
||
| || 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) // Deduplicate | ||
| .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 = asyncapi_rust::indexmap::IndexMap::new(); | ||
| #(#message_calls)* | ||
| Some(channel_messages) | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| quote! { | ||
| channels.insert( | ||
| #name.to_string(), | ||
| asyncapi_rust::Channel { | ||
| address: #address, | ||
| messages: None, | ||
| messages: #messages_field, | ||
| parameters: #parameters, | ||
| } | ||
| ); | ||
|
|
@@ -952,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 } | ||
|
|
@@ -965,6 +1034,44 @@ 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() { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as on the channel side (see the comment on line 944): the types listed here are never checked against |
||
| quote! { None } | ||
| } else { | ||
| 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 = | ||
| asyncapi_rust::indexmap::IndexMap::new(); | ||
|
|
||
| #(#message_calls)* | ||
|
|
||
| Some(message_refs.into_values().collect()) | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| let bindings = if let Some(mqtt) = &operation.mqtt { | ||
| let qos = if let Some(s) = mqtt.qos { | ||
| quote! { Some(#s) } | ||
|
|
@@ -1030,7 +1137,7 @@ pub fn derive_asyncapi(input: TokenStream) -> TokenStream { | |
| channel: asyncapi_rust::ChannelRef { | ||
| reference: format!("#/channels/{}", #channel_ref), | ||
| }, | ||
| messages: None, | ||
| messages: #messages_field, | ||
| bindings: #bindings | ||
| } | ||
| ); | ||
|
|
@@ -1047,10 +1154,22 @@ 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() { | ||
| 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 = spec_meta.message_types.iter().map(|type_name| { | ||
| 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: generic message types produce a confusing error. Parsing as
PathacceptsFoo<Bar>only in expression-ambiguous form, and the emitted#type_name::asyncapi_message_names()would need a turbofish anyway.Verified that
messages = [Option<Msg>]fails with "comparison operators cannot be chained" plus "proc-macro derive produced unparsable tokens". Either reject non-generic-free paths with a clearsyn::Error, or emit<#type_name>::asyncapi_message_names().