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        name: String,
214        summary: Option<String>,
215        description: Option<String>,
216        title: Option<String>,
217        content_type: Option<String>,
218        triggers_binary: bool,
219    }
220
221    // Parse enum variants or struct
222    let messages = match &input.data {
223        Data::Enum(data_enum) => {
224            let mut message_metas = Vec::new();
225
226            for variant in &data_enum.variants {
227                let variant_name = &variant.ident;
228
229                // Check for serde(rename) attribute on variant
230                let message_name = extract_serde_rename(&variant.attrs)
231                    .unwrap_or_else(|| variant_name.to_string());
232
233                // Extract asyncapi metadata
234                let asyncapi_meta = extract_asyncapi_meta(&variant.attrs);
235
236                message_metas.push(MessageMeta {
237                    name: message_name,
238                    summary: asyncapi_meta.summary,
239                    description: asyncapi_meta.description,
240                    title: asyncapi_meta.title,
241                    content_type: asyncapi_meta.content_type,
242                    triggers_binary: asyncapi_meta.triggers_binary,
243                });
244            }
245
246            message_metas
247        }
248        Data::Struct(_) => {
249            // For structs, extract metadata from the struct itself
250            let asyncapi_meta = extract_asyncapi_meta(&input.attrs);
251
252            vec![MessageMeta {
253                name: name.to_string(),
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        Data::Union(_) => {
262            return syn::Error::new_spanned(name, "ToAsyncApiMessage cannot be derived for unions")
263                .to_compile_error()
264                .into();
265        }
266    };
267
268    let message_count = messages.len();
269    let message_literals = messages.iter().map(|m| m.name.as_str());
270
271    // Prepare metadata for message generation
272    let message_names_for_gen = messages.iter().map(|m| m.name.as_str());
273    let message_titles = messages.iter().map(|m| {
274        if let Some(ref title) = m.title {
275            quote! { Some(#title.to_string()) }
276        } else {
277            let name = &m.name;
278            quote! { Some(#name.to_string()) }
279        }
280    });
281    let message_summaries = messages.iter().map(|m| {
282        if let Some(ref summary) = m.summary {
283            quote! { Some(#summary.to_string()) }
284        } else {
285            quote! { None }
286        }
287    });
288    let message_descriptions = messages.iter().map(|m| {
289        if let Some(ref desc) = m.description {
290            quote! { Some(#desc.to_string()) }
291        } else {
292            quote! { None }
293        }
294    });
295    let message_content_types = messages.iter().map(|m| {
296        if let Some(ref ct) = m.content_type {
297            quote! { Some(#ct.to_string()) }
298        } else if m.triggers_binary {
299            quote! { Some("application/octet-stream".to_string()) }
300        } else {
301            quote! { Some("application/json".to_string()) }
302        }
303    });
304
305    let tag_info = if let Some(tag) = tag_field {
306        quote! {
307            Some(#tag)
308        }
309    } else {
310        quote! { None }
311    };
312
313    let expanded = quote! {
314        impl #name {
315            /// Get AsyncAPI message names for this type
316            pub fn asyncapi_message_names() -> Vec<&'static str> {
317                vec![#(#message_literals),*]
318            }
319
320            /// Get the number of messages in this type
321            pub fn asyncapi_message_count() -> usize {
322                #message_count
323            }
324
325            /// Get the serde tag field name if this is a tagged enum
326            pub fn asyncapi_tag_field() -> Option<&'static str> {
327                #tag_info
328            }
329
330            /// Generate AsyncAPI Message objects with JSON schemas
331            ///
332            /// This method requires that the type implements `schemars::JsonSchema`.
333            pub fn asyncapi_messages() -> Vec<asyncapi_rust::Message>
334            where
335                Self: schemars::JsonSchema,
336            {
337                use schemars::schema_for;
338
339                let schema = schema_for!(Self);
340
341                // Convert schemars RootSchema to our Schema type
342                let schema_json = serde_json::to_value(&schema)
343                    .expect("Failed to serialize schema");
344
345                let payload_schema: asyncapi_rust::Schema = serde_json::from_value(schema_json)
346                    .expect("Failed to deserialize schema");
347
348                // Create messages with metadata
349                vec![#(asyncapi_rust::Message {
350                    name: Some(#message_names_for_gen.to_string()),
351                    title: #message_titles,
352                    summary: #message_summaries,
353                    description: #message_descriptions,
354                    content_type: #message_content_types,
355                    payload: Some(payload_schema.clone()),
356                }),*]
357            }
358        }
359    };
360
361    TokenStream::from(expanded)
362}
363
364/// Derive macro for generating complete AsyncAPI specification
365///
366/// # Example
367///
368/// ```rust,ignore
369/// use asyncapi_rust::AsyncApi;
370///
371/// #[derive(AsyncApi)]
372/// #[asyncapi(
373///     title = "Chat API",
374///     version = "1.0.0",
375///     description = "A real-time chat API"
376/// )]
377/// struct ChatApi;
378/// ```
379#[proc_macro_derive(
380    AsyncApi,
381    attributes(
382        asyncapi,
383        asyncapi_server,
384        asyncapi_channel,
385        asyncapi_operation,
386        asyncapi_messages
387    )
388)]
389pub fn derive_asyncapi(input: TokenStream) -> TokenStream {
390    let input = parse_macro_input!(input as DeriveInput);
391    let name = &input.ident;
392
393    // Extract asyncapi spec metadata
394    let spec_meta = extract_asyncapi_spec_meta(&input.attrs);
395
396    // Validate required fields
397    let title = match spec_meta.title {
398        Some(t) => t,
399        None => {
400            return syn::Error::new_spanned(
401                name,
402                "AsyncApi requires a title attribute: #[asyncapi(title = \"...\")]",
403            )
404            .to_compile_error()
405            .into();
406        }
407    };
408
409    let version = match spec_meta.version {
410        Some(v) => v,
411        None => {
412            return syn::Error::new_spanned(
413                name,
414                "AsyncApi requires a version attribute: #[asyncapi(version = \"...\")]",
415            )
416            .to_compile_error()
417            .into();
418        }
419    };
420
421    let description = if let Some(desc) = spec_meta.description {
422        quote! { Some(#desc.to_string()) }
423    } else {
424        quote! { None }
425    };
426
427    // Generate servers
428    let servers_code = if spec_meta.servers.is_empty() {
429        quote! { None }
430    } else {
431        let server_entries = spec_meta.servers.iter().map(|server| {
432            let name = &server.name;
433            let host = &server.host;
434            let protocol = &server.protocol;
435            let pathname = if let Some(p) = &server.pathname {
436                quote! { Some(#p.to_string()) }
437            } else {
438                quote! { None }
439            };
440            let desc = if let Some(d) = &server.description {
441                quote! { Some(#d.to_string()) }
442            } else {
443                quote! { None }
444            };
445
446            // Generate server variables
447            let variables = if server.variables.is_empty() {
448                quote! { None }
449            } else {
450                let var_entries = server.variables.iter().map(|var| {
451                    let var_name = &var.name;
452                    let var_desc = if let Some(d) = &var.description {
453                        quote! { Some(#d.to_string()) }
454                    } else {
455                        quote! { None }
456                    };
457                    let var_default = if let Some(d) = &var.default {
458                        quote! { Some(#d.to_string()) }
459                    } else {
460                        quote! { None }
461                    };
462                    let var_enum = if var.enum_values.is_empty() {
463                        quote! { None }
464                    } else {
465                        let enum_vals = &var.enum_values;
466                        quote! { Some(vec![#(#enum_vals.to_string()),*]) }
467                    };
468                    let var_examples = if var.examples.is_empty() {
469                        quote! { None }
470                    } else {
471                        let examples = &var.examples;
472                        quote! { Some(vec![#(#examples.to_string()),*]) }
473                    };
474
475                    quote! {
476                        server_variables.insert(
477                            #var_name.to_string(),
478                            asyncapi_rust::ServerVariable {
479                                description: #var_desc,
480                                default: #var_default,
481                                enum_values: #var_enum,
482                                examples: #var_examples,
483                            }
484                        );
485                    }
486                });
487
488                quote! {
489                    {
490                        let mut server_variables = std::collections::HashMap::new();
491                        #(#var_entries)*
492                        Some(server_variables)
493                    }
494                }
495            };
496
497            quote! {
498                servers.insert(
499                    #name.to_string(),
500                    asyncapi_rust::Server {
501                        host: #host.to_string(),
502                        protocol: #protocol.to_string(),
503                        pathname: #pathname,
504                        description: #desc,
505                        variables: #variables,
506                    }
507                );
508            }
509        });
510
511        quote! {
512            {
513                let mut servers = std::collections::HashMap::new();
514                #(#server_entries)*
515                Some(servers)
516            }
517        }
518    };
519
520    // Generate channels
521    let channels_code = if spec_meta.channels.is_empty() {
522        quote! { None }
523    } else {
524        let channel_entries = spec_meta.channels.iter().map(|channel| {
525            let name = &channel.name;
526            let address = if let Some(addr) = &channel.address {
527                quote! { Some(#addr.to_string()) }
528            } else {
529                quote! { None }
530            };
531
532            // Generate channel parameters
533            let parameters = if channel.parameters.is_empty() {
534                quote! { None }
535            } else {
536                let param_entries = channel.parameters.iter().map(|param| {
537                    let param_name = &param.name;
538                    let param_desc = if let Some(d) = &param.description {
539                        quote! { Some(#d.to_string()) }
540                    } else {
541                        quote! { None }
542                    };
543
544                    // Build schema from schema_type and format
545                    let schema = if let Some(schema_type) = &param.schema_type {
546                        let format_field = if let Some(fmt) = &param.format {
547                            quote! {
548                                additional.insert("format".to_string(), serde_json::json!(#fmt));
549                            }
550                        } else {
551                            quote! {}
552                        };
553
554                        quote! {
555                            {
556                                let mut additional = std::collections::HashMap::new();
557                                #format_field
558                                Some(asyncapi_rust::Schema::Object(Box::new(asyncapi_rust::SchemaObject {
559                                    schema_type: Some(serde_json::json!(#schema_type)),
560                                    properties: None,
561                                    required: None,
562                                    description: None,
563                                    title: None,
564                                    enum_values: None,
565                                    const_value: None,
566                                    items: None,
567                                    additional_properties: None,
568                                    one_of: None,
569                                    any_of: None,
570                                    all_of: None,
571                                    additional,
572                                })))
573                            }
574                        }
575                    } else {
576                        quote! { None }
577                    };
578
579                    quote! {
580                        channel_parameters.insert(
581                            #param_name.to_string(),
582                            asyncapi_rust::Parameter {
583                                description: #param_desc,
584                                schema: #schema,
585                            }
586                        );
587                    }
588                });
589
590                quote! {
591                    {
592                        let mut channel_parameters = std::collections::HashMap::new();
593                        #(#param_entries)*
594                        Some(channel_parameters)
595                    }
596                }
597            };
598
599            quote! {
600                channels.insert(
601                    #name.to_string(),
602                    asyncapi_rust::Channel {
603                        address: #address,
604                        messages: None,
605                        parameters: #parameters,
606                    }
607                );
608            }
609        });
610
611        quote! {
612            {
613                let mut channels = std::collections::HashMap::new();
614                #(#channel_entries)*
615                Some(channels)
616            }
617        }
618    };
619
620    // Generate operations
621    let operations_code = if spec_meta.operations.is_empty() {
622        quote! { None }
623    } else {
624        let operation_entries = spec_meta.operations.iter().map(|operation| {
625            let name = &operation.name;
626            let channel_ref = &operation.channel;
627            let action = &operation.action;
628
629            // Convert action string to OperationAction enum
630            let action_enum = if action == "send" {
631                quote! { asyncapi_rust::OperationAction::Send }
632            } else if action == "receive" {
633                quote! { asyncapi_rust::OperationAction::Receive }
634            } else {
635                return syn::Error::new_spanned(
636                    name,
637                    format!("Invalid action '{}', must be 'send' or 'receive'", action),
638                )
639                .to_compile_error();
640            };
641
642            quote! {
643                operations.insert(
644                    #name.to_string(),
645                    asyncapi_rust::Operation {
646                        action: #action_enum,
647                        channel: asyncapi_rust::ChannelRef {
648                            reference: format!("#/channels/{}", #channel_ref),
649                        },
650                        messages: None,
651                    }
652                );
653            }
654        });
655
656        quote! {
657            {
658                let mut operations = std::collections::HashMap::new();
659                #(#operation_entries)*
660                Some(operations)
661            }
662        }
663    };
664
665    // Generate components with messages
666    let components_code = if spec_meta.message_types.is_empty() {
667        quote! { None }
668    } else {
669        let message_calls = spec_meta.message_types.iter().map(|type_name| {
670            quote! {
671                // Call asyncapi_messages() for this type and add to messages map
672                for msg in #type_name::asyncapi_messages() {
673                    if let Some(ref name) = msg.name {
674                        messages.insert(name.clone(), msg.clone());
675                    }
676                }
677            }
678        });
679
680        quote! {
681            {
682                let mut messages = std::collections::HashMap::new();
683                #(#message_calls)*
684                Some(asyncapi_rust::Components {
685                    messages: if messages.is_empty() { None } else { Some(messages) },
686                    schemas: None,
687                })
688            }
689        }
690    };
691
692    let expanded = quote! {
693        impl #name {
694            /// Generate the AsyncAPI specification
695            ///
696            /// Returns an AsyncApiSpec with Info, Servers, Channels, and Operations
697            /// sections populated from attributes.
698            pub fn asyncapi_spec() -> asyncapi_rust::AsyncApiSpec {
699                asyncapi_rust::AsyncApiSpec {
700                    asyncapi: "3.0.0".to_string(),
701                    info: asyncapi_rust::Info {
702                        title: #title.to_string(),
703                        version: #version.to_string(),
704                        description: #description,
705                    },
706                    servers: #servers_code,
707                    channels: #channels_code,
708                    operations: #operations_code,
709                    components: #components_code,
710                }
711            }
712        }
713    };
714
715    TokenStream::from(expanded)
716}
717
718#[cfg(test)]
719mod tests {
720    #[test]
721    fn test_placeholder() {
722        // Macro expansion tests will go here
723    }
724}