Skip to main content

asyncapi_rust_codegen/
lib.rs

1//! Procedural macro implementation for asyncapi-rust
2//!
3//! This crate provides the procedural macros that power `asyncapi-rust`, enabling
4//! compile-time generation of AsyncAPI 3.0 specifications from Rust code.
5//!
6//! ## Overview
7//!
8//! Two derive macros are provided:
9//!
10//! ### `#[derive(ToAsyncApiMessage)]`
11//!
12//! Generates message metadata and JSON schemas from Rust types (structs or enums).
13//!
14//! - Works with [`serde`](https://serde.rs) for serialization patterns
15//! - Uses [`schemars`](https://docs.rs/schemars) for JSON Schema generation
16//! - Supports `#[asyncapi(...)]` helper attributes for documentation
17//! - Generates methods: `asyncapi_message_names()`, `asyncapi_messages()`, etc.
18//!
19//! **Example:**
20//! ```rust,ignore
21//! use asyncapi_rust::{ToAsyncApiMessage, schemars::JsonSchema};
22//! use serde::{Deserialize, Serialize};
23//!
24//! #[derive(Serialize, Deserialize, JsonSchema, ToAsyncApiMessage)]
25//! #[serde(tag = "type")]
26//! pub enum ChatMessage {
27//!     #[serde(rename = "user.join")]
28//!     #[asyncapi(
29//!         summary = "User joins",
30//!         description = "Sent when a user enters a room"
31//!     )]
32//!     UserJoin { username: String, room: String },
33//!
34//!     #[serde(rename = "chat.message")]
35//!     #[asyncapi(summary = "Chat message")]
36//!     Chat { username: String, room: String, text: String },
37//! }
38//!
39//! // Generated methods available:
40//! let names = ChatMessage::asyncapi_message_names();
41//! let messages = ChatMessage::asyncapi_messages(); // Requires JsonSchema
42//! ```
43//!
44//! ### `#[derive(AsyncApi)]`
45//!
46//! Generates complete AsyncAPI 3.0 specifications with servers, channels, and operations.
47//!
48//! - Requires `title` and `version` attributes
49//! - Supports optional `description` attribute
50//! - Use `#[asyncapi_server(...)]` to define servers
51//! - Use `#[asyncapi_channel(...)]` to define channels
52//! - Use `#[asyncapi_operation(...)]` to define operations
53//! - Can use multiple of each attribute type
54//!
55//! **Example:**
56//! ```rust,ignore
57//! use asyncapi_rust::AsyncApi;
58//!
59//! #[derive(AsyncApi)]
60//! #[asyncapi(
61//!     title = "Chat API",
62//!     version = "1.0.0",
63//!     description = "Real-time chat application"
64//! )]
65//! #[asyncapi_server(
66//!     name = "production",
67//!     host = "chat.example.com",
68//!     protocol = "wss",
69//!     description = "Production WebSocket server"
70//! )]
71//! #[asyncapi_channel(
72//!     name = "chat",
73//!     address = "/ws/chat"
74//! )]
75//! #[asyncapi_operation(
76//!     name = "sendMessage",
77//!     action = "send",
78//!     channel = "chat"
79//! )]
80//! #[asyncapi_operation(
81//!     name = "receiveMessage",
82//!     action = "receive",
83//!     channel = "chat"
84//! )]
85//! struct ChatApi;
86//!
87//! // Generated method:
88//! let spec = ChatApi::asyncapi_spec();
89//! ```
90//!
91//! ## Supported Attributes
92//!
93//! ### `#[asyncapi(...)]` on message types
94//!
95//! Helper attributes for documenting messages (used with `ToAsyncApiMessage`):
96//!
97//! - `summary = "..."` - Short summary of the message
98//! - `description = "..."` - Detailed description
99//! - `title = "..."` - Human-readable title (defaults to message name)
100//! - `content_type = "..."` - Content type (defaults to "application/json")
101//! - `triggers_binary` - Flag for binary messages (sets content_type to "application/octet-stream")
102//!
103//! ### `#[asyncapi(...)]` on API specs
104//!
105//! Required attributes for complete specifications (used with `AsyncApi`):
106//!
107//! - `title = "..."` - API title (required)
108//! - `version = "..."` - API version (required)
109//! - `description = "..."` - API description (optional)
110//!
111//! ### `#[asyncapi_server(...)]`
112//!
113//! Define server connection information:
114//!
115//! - `name = "..."` - Server identifier (required)
116//! - `host = "..."` - Server host/URL (required)
117//! - `protocol = "..."` - Protocol (e.g., "wss", "ws", "grpc") (required)
118//! - `description = "..."` - Server description (optional)
119//!
120//! ### `#[asyncapi_channel(...)]`
121//!
122//! Define communication channels:
123//!
124//! - `name = "..."` - Channel identifier (required)
125//! - `address = "..."` - Channel path/address (optional)
126//!
127//! ### `#[asyncapi_operation(...)]`
128//!
129//! Define send/receive operations:
130//!
131//! - `name = "..."` - Operation identifier (required)
132//! - `action = "send"|"receive"` - Operation type (required)
133//! - `channel = "..."` - Channel reference (required)
134//!
135//! ## Integration with serde
136//!
137//! The macros respect serde attributes for naming and structure:
138//!
139//! - `#[serde(rename = "...")]` - Use custom name in AsyncAPI spec
140//! - `#[serde(tag = "...")]` - Tagged enum with discriminator field
141//! - `#[serde(skip)]` - Exclude fields from schema
142//! - `#[serde(skip_serializing_if = "...")]` - Optional fields
143//!
144//! ## Integration with schemars
145//!
146//! JSON schemas are generated automatically using schemars:
147//!
148//! - Requires `JsonSchema` derive on message types
149//! - Generates complete JSON Schema from Rust type definitions
150//! - Supports nested types, generics, and references
151//! - Schemas include validation rules from type constraints
152//!
153//! ## Generated Code
154//!
155//! The macros generate implementations with these methods:
156//!
157//! **From `ToAsyncApiMessage`:**
158//! - `asyncapi_message_names() -> Vec<&'static str>` - Get all message names
159//! - `asyncapi_message_count() -> usize` - Number of messages
160//! - `asyncapi_tag_field() -> Option<&'static str>` - Serde tag field if present
161//! - `asyncapi_messages() -> Vec<Message>` - Generate messages with schemas
162//!
163//! **From `AsyncApi`:**
164//! - `asyncapi_spec() -> AsyncApiSpec` - Generate complete specification
165//!
166//! ## Implementation Notes
167//!
168//! - All code generation happens at compile time (proc macros)
169//! - Zero runtime cost - generates plain Rust code
170//! - Compile errors if documentation drifts from code
171//! - Type-safe - uses Rust's type system for validation
172
173#![warn(clippy::all)]
174
175use proc_macro::TokenStream;
176use quote::quote;
177use syn::{Data, DeriveInput, parse_macro_input};
178
179mod asyncapi_attrs;
180mod asyncapi_spec_attrs;
181mod serde_attrs;
182
183use asyncapi_attrs::extract_asyncapi_meta;
184use asyncapi_spec_attrs::extract_asyncapi_spec_meta;
185use serde_attrs::{extract_serde_rename, extract_serde_tag};
186
187/// Derive macro for generating AsyncAPI message metadata
188///
189/// # Example
190///
191/// ```rust,ignore
192/// use asyncapi_rust::ToAsyncApiMessage;
193/// use serde::{Deserialize, Serialize};
194///
195/// #[derive(Serialize, Deserialize, ToAsyncApiMessage)]
196/// #[serde(tag = "type")]
197/// pub enum Message {
198///     #[serde(rename = "chat")]
199///     Chat { room: String, text: String },
200///     Echo { id: i64, text: String },
201/// }
202/// ```
203#[proc_macro_derive(ToAsyncApiMessage, attributes(asyncapi))]
204pub fn derive_to_asyncapi_message(input: TokenStream) -> TokenStream {
205    let input = parse_macro_input!(input as DeriveInput);
206    let name = &input.ident;
207
208    // Extract serde tag attribute from enum
209    let tag_field = extract_serde_tag(&input.attrs);
210
211    // Struct to hold message metadata
212    struct MessageMeta {
213        /// Stable message identity used in components.messages and asyncapi_message_names().
214        /// Defaults to the Rust variant/type identifier; overridable via
215        /// `#[asyncapi(message_name = "...")]`.
216        name: String,
217        /// Wire discriminant value from serde rename (used for payload schema lookup).
218        /// May be an empty string when `#[serde(rename = "")]`; defaults to variant ident.
219        discriminant: String,
220        summary: Option<String>,
221        description: Option<String>,
222        title: Option<String>,
223        content_type: Option<String>,
224        triggers_binary: bool,
225    }
226
227    // Parse enum variants or struct
228    let messages = match &input.data {
229        Data::Enum(data_enum) => {
230            let mut message_metas = Vec::new();
231
232            for variant in &data_enum.variants {
233                let variant_name = &variant.ident;
234                let variant_ident_str = variant_name.to_string();
235
236                // Wire discriminant: serde rename if present (even if empty), else variant ident.
237                let discriminant = extract_serde_rename(&variant.attrs)
238                    .unwrap_or_else(|| variant_ident_str.clone());
239
240                // Extract asyncapi metadata
241                let asyncapi_meta = extract_asyncapi_meta(&variant.attrs);
242
243                // Message identity: explicit message_name override, else variant ident.
244                // We deliberately do NOT use the serde rename here — it may be empty,
245                // non-unique across enums, or unsuitable as a code identifier.
246                let message_name = asyncapi_meta
247                    .message_name
248                    .clone()
249                    .unwrap_or_else(|| variant_ident_str.clone());
250
251                message_metas.push(MessageMeta {
252                    name: message_name,
253                    discriminant,
254                    summary: asyncapi_meta.summary,
255                    description: asyncapi_meta.description,
256                    title: asyncapi_meta.title,
257                    content_type: asyncapi_meta.content_type,
258                    triggers_binary: asyncapi_meta.triggers_binary,
259                });
260            }
261
262            message_metas
263        }
264        Data::Struct(_) => {
265            // For structs, extract metadata from the struct itself
266            let asyncapi_meta = extract_asyncapi_meta(&input.attrs);
267            let struct_name = name.to_string();
268            let message_name = asyncapi_meta
269                .message_name
270                .clone()
271                .unwrap_or_else(|| struct_name.clone());
272
273            vec![MessageMeta {
274                name: message_name,
275                discriminant: struct_name,
276                summary: asyncapi_meta.summary,
277                description: asyncapi_meta.description,
278                title: asyncapi_meta.title,
279                content_type: asyncapi_meta.content_type,
280                triggers_binary: asyncapi_meta.triggers_binary,
281            }]
282        }
283        Data::Union(_) => {
284            return syn::Error::new_spanned(name, "ToAsyncApiMessage cannot be derived for unions")
285                .to_compile_error()
286                .into();
287        }
288    };
289
290    let message_count = messages.len();
291    let message_literals = messages.iter().map(|m| m.name.as_str());
292
293    // Prepare metadata for message generation
294    let message_names_for_gen = messages.iter().map(|m| m.name.as_str());
295    // Wire discriminant for each variant — used to look up per-variant schemas at runtime.
296    let message_discriminants = messages.iter().map(|m| m.discriminant.as_str());
297    let message_titles = messages.iter().map(|m| {
298        if let Some(ref title) = m.title {
299            quote! { Some(#title.to_string()) }
300        } else {
301            let name = &m.name;
302            quote! { Some(#name.to_string()) }
303        }
304    });
305    let message_summaries = messages.iter().map(|m| {
306        if let Some(ref summary) = m.summary {
307            quote! { Some(#summary.to_string()) }
308        } else {
309            quote! { None }
310        }
311    });
312    let message_descriptions = messages.iter().map(|m| {
313        if let Some(ref desc) = m.description {
314            quote! { Some(#desc.to_string()) }
315        } else {
316            quote! { None }
317        }
318    });
319    let message_content_types = messages.iter().map(|m| {
320        if let Some(ref ct) = m.content_type {
321            quote! { Some(#ct.to_string()) }
322        } else if m.triggers_binary {
323            quote! { Some("application/octet-stream".to_string()) }
324        } else {
325            quote! { Some("application/json".to_string()) }
326        }
327    });
328
329    let tag_info = if let Some(tag) = tag_field {
330        quote! {
331            Some(#tag)
332        }
333    } else {
334        quote! { None }
335    };
336
337    let expanded = quote! {
338        // const _: () scopes the helper so it doesn't leak into the user's namespace
339        const _: () = {
340            /// Rewrites schemars' `#/$defs/X` refs to `#/components/schemas/X` in-place.
341            fn rewrite_defs_refs(value: &mut serde_json::Value) {
342                match value {
343                    serde_json::Value::Object(map) => {
344                        if let Some(r) = map.get_mut("$ref") {
345                            if let Some(s) = r.as_str() {
346                                if let Some(name) = s.strip_prefix("#/$defs/") {
347                                    *r = serde_json::Value::String(
348                                        format!("#/components/schemas/{}", name)
349                                    );
350                                }
351                            }
352                        }
353                        for v in map.values_mut() {
354                            rewrite_defs_refs(v);
355                        }
356                    }
357                    serde_json::Value::Array(arr) => {
358                        for v in arr.iter_mut() {
359                            rewrite_defs_refs(v);
360                        }
361                    }
362                    _ => {}
363                }
364            }
365
366            impl #name {
367                /// Get AsyncAPI message names for this type
368                pub fn asyncapi_message_names() -> Vec<&'static str> {
369                    vec![#(#message_literals),*]
370                }
371
372                /// Get the number of messages in this type
373                pub fn asyncapi_message_count() -> usize {
374                    #message_count
375                }
376
377                /// Get the serde tag field name if this is a tagged enum
378                pub fn asyncapi_tag_field() -> Option<&'static str> {
379                    #tag_info
380                }
381
382                /// Return shared schema definitions for this type, keyed by name.
383                ///
384                /// These are the `$defs` that schemars generates for sub-types referenced
385                /// by this type's variants. The `AsyncApi` derive collects them into
386                /// `components.schemas` so message payloads can reference them via
387                /// `#/components/schemas/X` instead of embedding them inline.
388                pub fn asyncapi_schemas() -> asyncapi_rust::indexmap::IndexMap<String, asyncapi_rust::Schema>
389                where
390                    Self: schemars::JsonSchema,
391                {
392                    use schemars::schema_for;
393                    let schema = schema_for!(Self);
394                    let schema_json = serde_json::to_value(&schema)
395                        .expect("Failed to serialize schema");
396
397                    let mut result = asyncapi_rust::indexmap::IndexMap::new();
398                    if let Some(defs) = schema_json.get("$defs").and_then(|v| v.as_object()) {
399                        for (name, def_schema) in defs {
400                            let mut def = def_schema.clone();
401                            rewrite_defs_refs(&mut def);
402                            if let Ok(s) = serde_json::from_value::<asyncapi_rust::Schema>(def) {
403                                result.insert(name.clone(), s);
404                            }
405                        }
406                    }
407                    result
408                }
409
410                /// Generate AsyncAPI Message objects with JSON schemas.
411                ///
412                /// For internally-tagged enums each message carries only its own variant
413                /// schema. `$ref`s within payloads point to `#/components/schemas/X`;
414                /// the corresponding definitions are available via `asyncapi_schemas()`.
415                pub fn asyncapi_messages() -> Vec<asyncapi_rust::Message>
416                where
417                    Self: schemars::JsonSchema,
418                {
419                    use schemars::schema_for;
420
421                    let schema = schema_for!(Self);
422                    let schema_json = serde_json::to_value(&schema)
423                        .expect("Failed to serialize schema");
424
425                    // Build a discriminant→schema map using the actual serde tag field name.
426                    let tag_field = Self::asyncapi_tag_field();
427                    let mut variant_schemas: asyncapi_rust::indexmap::IndexMap<String, serde_json::Value> =
428                        asyncapi_rust::indexmap::IndexMap::new();
429                    if let Some(tag) = tag_field {
430                        if let Some(variants) = schema_json.get("oneOf").and_then(|v| v.as_array()) {
431                            for variant in variants {
432                                let discriminant = variant
433                                    .get("properties")
434                                    .and_then(|props| props.get(tag))
435                                    .and_then(|tag_prop| {
436                                        tag_prop.get("const").or_else(|| {
437                                            tag_prop
438                                                .get("enum")
439                                                .and_then(|e| e.as_array())
440                                                .and_then(|a| a.first())
441                                        })
442                                    })
443                                    .and_then(|v| v.as_str())
444                                    .map(|s| s.to_string());
445
446                                if let Some(name) = discriminant {
447                                    let mut variant_schema = variant.clone();
448                                    // Drop $defs — they live in components.schemas, not the payload.
449                                    if let Some(obj) = variant_schema.as_object_mut() {
450                                        obj.remove("$defs");
451                                    }
452                                    rewrite_defs_refs(&mut variant_schema);
453                                    variant_schemas.insert(name, variant_schema);
454                                }
455                            }
456                        }
457                    }
458
459                    // Metadata arrays are baked in at compile time; schemas resolved at runtime.
460                    let names: &[&str] = &[#(#message_names_for_gen),*];
461                    // Discriminants are the serde rename values — used to look up per-variant
462                    // schemas. Separate from names so empty renames and cross-enum collisions
463                    // don't affect message identity.
464                    let discriminants: &[&str] = &[#(#message_discriminants),*];
465                    let titles: &[Option<String>] = &[#(#message_titles),*];
466                    let summaries: &[Option<String>] = &[#(#message_summaries),*];
467                    let descriptions: &[Option<String>] = &[#(#message_descriptions),*];
468                    let content_types: &[Option<String>] = &[#(#message_content_types),*];
469
470                    let mut messages = Vec::with_capacity(names.len());
471                    for i in 0..names.len() {
472                        let msg_name = names[i];
473                        let discriminant = discriminants[i];
474                        let payload = if let Some(v) = variant_schemas.get(discriminant) {
475                            serde_json::from_value(v.clone()).ok()
476                        } else {
477                            // Structs, untagged enums, or variants not in the map:
478                            // remove $defs and rewrite refs in the full schema.
479                            let mut fallback = schema_json.clone();
480                            if let Some(obj) = fallback.as_object_mut() {
481                                obj.remove("$defs");
482                            }
483                            rewrite_defs_refs(&mut fallback);
484                            serde_json::from_value(fallback).ok()
485                        };
486                        messages.push(asyncapi_rust::Message {
487                            name: Some(msg_name.to_string()),
488                            title: titles[i].clone(),
489                            summary: summaries[i].clone(),
490                            description: descriptions[i].clone(),
491                            content_type: content_types[i].clone(),
492                            payload,
493                        });
494                    }
495                    messages
496                }
497            }
498        };
499    };
500
501    TokenStream::from(expanded)
502}
503
504/// Derive macro for generating complete AsyncAPI specification
505///
506/// # Example
507///
508/// ```rust,ignore
509/// use asyncapi_rust::AsyncApi;
510///
511/// #[derive(AsyncApi)]
512/// #[asyncapi(
513///     title = "Chat API",
514///     version = "1.0.0",
515///     description = "A real-time chat API"
516/// )]
517/// struct ChatApi;
518/// ```
519#[proc_macro_derive(
520    AsyncApi,
521    attributes(
522        asyncapi,
523        asyncapi_server,
524        asyncapi_channel,
525        asyncapi_operation,
526        asyncapi_messages
527    )
528)]
529pub fn derive_asyncapi(input: TokenStream) -> TokenStream {
530    let input = parse_macro_input!(input as DeriveInput);
531    let name = &input.ident;
532
533    // Extract asyncapi spec metadata
534    let spec_meta = extract_asyncapi_spec_meta(&input.attrs);
535
536    // Validate required fields
537    let title = match spec_meta.title {
538        Some(t) => t,
539        None => {
540            return syn::Error::new_spanned(
541                name,
542                "AsyncApi requires a title attribute: #[asyncapi(title = \"...\")]",
543            )
544            .to_compile_error()
545            .into();
546        }
547    };
548
549    let version = match spec_meta.version {
550        Some(v) => v,
551        None => {
552            return syn::Error::new_spanned(
553                name,
554                "AsyncApi requires a version attribute: #[asyncapi(version = \"...\")]",
555            )
556            .to_compile_error()
557            .into();
558        }
559    };
560
561    let description = if let Some(desc) = spec_meta.description {
562        quote! { Some(#desc.to_string()) }
563    } else {
564        quote! { None }
565    };
566
567    // Generate servers
568    let servers_code = if spec_meta.servers.is_empty() {
569        quote! { None }
570    } else {
571        let server_entries = spec_meta.servers.iter().map(|server| {
572            let name = &server.name;
573            let host = &server.host;
574            let protocol = &server.protocol;
575            let pathname = if let Some(p) = &server.pathname {
576                quote! { Some(#p.to_string()) }
577            } else {
578                quote! { None }
579            };
580            let desc = if let Some(d) = &server.description {
581                quote! { Some(#d.to_string()) }
582            } else {
583                quote! { None }
584            };
585
586            // Generate server variables
587            let variables = if server.variables.is_empty() {
588                quote! { None }
589            } else {
590                let var_entries = server.variables.iter().map(|var| {
591                    let var_name = &var.name;
592                    let var_desc = if let Some(d) = &var.description {
593                        quote! { Some(#d.to_string()) }
594                    } else {
595                        quote! { None }
596                    };
597                    let var_default = if let Some(d) = &var.default {
598                        quote! { Some(#d.to_string()) }
599                    } else {
600                        quote! { None }
601                    };
602                    let var_enum = if var.enum_values.is_empty() {
603                        quote! { None }
604                    } else {
605                        let enum_vals = &var.enum_values;
606                        quote! { Some(vec![#(#enum_vals.to_string()),*]) }
607                    };
608                    let var_examples = if var.examples.is_empty() {
609                        quote! { None }
610                    } else {
611                        let examples = &var.examples;
612                        quote! { Some(vec![#(#examples.to_string()),*]) }
613                    };
614
615                    quote! {
616                        server_variables.insert(
617                            #var_name.to_string(),
618                            asyncapi_rust::ServerVariable {
619                                description: #var_desc,
620                                default: #var_default,
621                                enum_values: #var_enum,
622                                examples: #var_examples,
623                            }
624                        );
625                    }
626                });
627
628                quote! {
629                    {
630                        let mut server_variables = asyncapi_rust::indexmap::IndexMap::new();
631                        #(#var_entries)*
632                        Some(server_variables)
633                    }
634                }
635            };
636
637            quote! {
638                servers.insert(
639                    #name.to_string(),
640                    asyncapi_rust::Server {
641                        host: #host.to_string(),
642                        protocol: #protocol.to_string(),
643                        pathname: #pathname,
644                        description: #desc,
645                        variables: #variables,
646                    }
647                );
648            }
649        });
650
651        quote! {
652            {
653                let mut servers = asyncapi_rust::indexmap::IndexMap::new();
654                #(#server_entries)*
655                Some(servers)
656            }
657        }
658    };
659
660    // Generate channels
661    let channels_code = if spec_meta.channels.is_empty() {
662        quote! { None }
663    } else {
664        let channel_entries = spec_meta.channels.iter().map(|channel| {
665            let name = &channel.name;
666            let address = if let Some(addr) = &channel.address {
667                quote! { Some(#addr.to_string()) }
668            } else {
669                quote! { None }
670            };
671
672            // Generate channel parameters
673            let parameters = if channel.parameters.is_empty() {
674                quote! { None }
675            } else {
676                let param_entries = channel.parameters.iter().map(|param| {
677                    let param_name = &param.name;
678                    let param_desc = if let Some(d) = &param.description {
679                        quote! { Some(#d.to_string()) }
680                    } else {
681                        quote! { None }
682                    };
683                    let param_default = if let Some(d) = &param.default {
684                        quote! { Some(#d.to_string()) }
685                    } else {
686                        quote! { None }
687                    };
688                    let param_enum = if param.enum_values.is_empty() {
689                        quote! { None }
690                    } else {
691                        let vals = &param.enum_values;
692                        quote! { Some(vec![#(#vals.to_string()),*]) }
693                    };
694                    let param_examples = if param.examples.is_empty() {
695                        quote! { None }
696                    } else {
697                        let vals = &param.examples;
698                        quote! { Some(vec![#(#vals.to_string()),*]) }
699                    };
700                    let param_location = if let Some(l) = &param.location {
701                        quote! { Some(#l.to_string()) }
702                    } else {
703                        quote! { None }
704                    };
705
706                    quote! {
707                        channel_parameters.insert(
708                            #param_name.to_string(),
709                            asyncapi_rust::Parameter {
710                                description: #param_desc,
711                                default: #param_default,
712                                enum_values: #param_enum,
713                                examples: #param_examples,
714                                location: #param_location,
715                            }
716                        );
717                    }
718                });
719
720                quote! {
721                    {
722                        let mut channel_parameters = asyncapi_rust::indexmap::IndexMap::new();
723                        #(#param_entries)*
724                        Some(channel_parameters)
725                    }
726                }
727            };
728
729            quote! {
730                channels.insert(
731                    #name.to_string(),
732                    asyncapi_rust::Channel {
733                        address: #address,
734                        messages: None,
735                        parameters: #parameters,
736                    }
737                );
738            }
739        });
740
741        quote! {
742            {
743                let mut channels = asyncapi_rust::indexmap::IndexMap::new();
744                #(#channel_entries)*
745                Some(channels)
746            }
747        }
748    };
749
750    // Generate operations
751    let operations_code = if spec_meta.operations.is_empty() {
752        quote! { None }
753    } else {
754        let operation_entries = spec_meta.operations.iter().map(|operation| {
755            let name = &operation.name;
756            let channel_ref = &operation.channel;
757            let action = &operation.action;
758
759            // Convert action string to OperationAction enum
760            let action_enum = if action == "send" {
761                quote! { asyncapi_rust::OperationAction::Send }
762            } else if action == "receive" {
763                quote! { asyncapi_rust::OperationAction::Receive }
764            } else {
765                return syn::Error::new_spanned(
766                    name,
767                    format!("Invalid action '{}', must be 'send' or 'receive'", action),
768                )
769                .to_compile_error();
770            };
771
772            quote! {
773                operations.insert(
774                    #name.to_string(),
775                    asyncapi_rust::Operation {
776                        action: #action_enum,
777                        channel: asyncapi_rust::ChannelRef {
778                            reference: format!("#/channels/{}", #channel_ref),
779                        },
780                        messages: None,
781                    }
782                );
783            }
784        });
785
786        quote! {
787            {
788                let mut operations = asyncapi_rust::indexmap::IndexMap::new();
789                #(#operation_entries)*
790                Some(operations)
791            }
792        }
793    };
794
795    // Generate components with messages and hoisted shared schemas
796    let components_code = if spec_meta.message_types.is_empty() {
797        quote! { None }
798    } else {
799        let type_calls = spec_meta.message_types.iter().map(|type_name| {
800            quote! {
801                for msg in #type_name::asyncapi_messages() {
802                    if let Some(ref name) = msg.name {
803                        if messages.contains_key(name.as_str()) {
804                            panic!(
805                                "asyncapi-rust: message name collision for '{}' from {}. \
806                                 Use #[asyncapi(message_name = \"...\")] on one variant to disambiguate.",
807                                name,
808                                stringify!(#type_name)
809                            );
810                        }
811                        messages.insert(name.clone(), msg.clone());
812                    }
813                }
814                // Hoist shared $defs into components.schemas (first writer wins on name collision)
815                for (name, schema) in #type_name::asyncapi_schemas() {
816                    schemas.entry(name).or_insert(schema);
817                }
818            }
819        });
820
821        quote! {
822            {
823                let mut messages = asyncapi_rust::indexmap::IndexMap::new();
824                let mut schemas = asyncapi_rust::indexmap::IndexMap::new();
825                #(#type_calls)*
826                Some(asyncapi_rust::Components {
827                    messages: if messages.is_empty() { None } else { Some(messages) },
828                    schemas: if schemas.is_empty() { None } else { Some(schemas) },
829                })
830            }
831        }
832    };
833
834    let expanded = quote! {
835        impl #name {
836            /// Generate the AsyncAPI specification
837            ///
838            /// Returns an AsyncApiSpec with Info, Servers, Channels, and Operations
839            /// sections populated from attributes.
840            pub fn asyncapi_spec() -> asyncapi_rust::AsyncApiSpec {
841                asyncapi_rust::AsyncApiSpec {
842                    asyncapi: "3.0.0".to_string(),
843                    info: asyncapi_rust::Info {
844                        title: #title.to_string(),
845                        version: #version.to_string(),
846                        description: #description,
847                    },
848                    servers: #servers_code,
849                    channels: #channels_code,
850                    operations: #operations_code,
851                    components: #components_code,
852                }
853            }
854        }
855    };
856
857    TokenStream::from(expanded)
858}
859
860#[cfg(test)]
861mod tests {
862    #[test]
863    fn test_placeholder() {
864        // Macro expansion tests will go here
865    }
866}