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(AsyncApi, attributes(asyncapi, asyncapi_server, asyncapi_channel, asyncapi_operation, asyncapi_messages))]
380pub fn derive_asyncapi(input: TokenStream) -> TokenStream {
381    let input = parse_macro_input!(input as DeriveInput);
382    let name = &input.ident;
383
384    // Extract asyncapi spec metadata
385    let spec_meta = extract_asyncapi_spec_meta(&input.attrs);
386
387    // Validate required fields
388    let title = match spec_meta.title {
389        Some(t) => t,
390        None => {
391            return syn::Error::new_spanned(
392                name,
393                "AsyncApi requires a title attribute: #[asyncapi(title = \"...\")]"
394            )
395            .to_compile_error()
396            .into();
397        }
398    };
399
400    let version = match spec_meta.version {
401        Some(v) => v,
402        None => {
403            return syn::Error::new_spanned(
404                name,
405                "AsyncApi requires a version attribute: #[asyncapi(version = \"...\")]"
406            )
407            .to_compile_error()
408            .into();
409        }
410    };
411
412    let description = if let Some(desc) = spec_meta.description {
413        quote! { Some(#desc.to_string()) }
414    } else {
415        quote! { None }
416    };
417
418    // Generate servers
419    let servers_code = if spec_meta.servers.is_empty() {
420        quote! { None }
421    } else {
422        let server_entries = spec_meta.servers.iter().map(|server| {
423            let name = &server.name;
424            let host = &server.host;
425            let protocol = &server.protocol;
426            let desc = if let Some(d) = &server.description {
427                quote! { Some(#d.to_string()) }
428            } else {
429                quote! { None }
430            };
431
432            quote! {
433                servers.insert(
434                    #name.to_string(),
435                    asyncapi_rust::Server {
436                        host: #host.to_string(),
437                        protocol: #protocol.to_string(),
438                        description: #desc,
439                    }
440                );
441            }
442        });
443
444        quote! {
445            {
446                let mut servers = std::collections::HashMap::new();
447                #(#server_entries)*
448                Some(servers)
449            }
450        }
451    };
452
453    // Generate channels
454    let channels_code = if spec_meta.channels.is_empty() {
455        quote! { None }
456    } else {
457        let channel_entries = spec_meta.channels.iter().map(|channel| {
458            let name = &channel.name;
459            let address = if let Some(addr) = &channel.address {
460                quote! { Some(#addr.to_string()) }
461            } else {
462                quote! { None }
463            };
464
465            quote! {
466                channels.insert(
467                    #name.to_string(),
468                    asyncapi_rust::Channel {
469                        address: #address,
470                        messages: None,
471                    }
472                );
473            }
474        });
475
476        quote! {
477            {
478                let mut channels = std::collections::HashMap::new();
479                #(#channel_entries)*
480                Some(channels)
481            }
482        }
483    };
484
485    // Generate operations
486    let operations_code = if spec_meta.operations.is_empty() {
487        quote! { None }
488    } else {
489        let operation_entries = spec_meta.operations.iter().map(|operation| {
490            let name = &operation.name;
491            let channel_ref = &operation.channel;
492            let action = &operation.action;
493
494            // Convert action string to OperationAction enum
495            let action_enum = if action == "send" {
496                quote! { asyncapi_rust::OperationAction::Send }
497            } else if action == "receive" {
498                quote! { asyncapi_rust::OperationAction::Receive }
499            } else {
500                return syn::Error::new_spanned(
501                    name,
502                    format!("Invalid action '{}', must be 'send' or 'receive'", action)
503                )
504                .to_compile_error();
505            };
506
507            quote! {
508                operations.insert(
509                    #name.to_string(),
510                    asyncapi_rust::Operation {
511                        action: #action_enum,
512                        channel: asyncapi_rust::ChannelRef {
513                            reference: format!("#/channels/{}", #channel_ref),
514                        },
515                        messages: None,
516                    }
517                );
518            }
519        });
520
521        quote! {
522            {
523                let mut operations = std::collections::HashMap::new();
524                #(#operation_entries)*
525                Some(operations)
526            }
527        }
528    };
529
530    // Generate components with messages
531    let components_code = if spec_meta.message_types.is_empty() {
532        quote! { None }
533    } else {
534        let message_calls = spec_meta.message_types.iter().map(|type_name| {
535            quote! {
536                // Call asyncapi_messages() for this type and add to messages map
537                for msg in #type_name::asyncapi_messages() {
538                    if let Some(ref name) = msg.name {
539                        messages.insert(name.clone(), msg.clone());
540                    }
541                }
542            }
543        });
544
545        quote! {
546            {
547                let mut messages = std::collections::HashMap::new();
548                #(#message_calls)*
549                Some(asyncapi_rust::Components {
550                    messages: if messages.is_empty() { None } else { Some(messages) },
551                    schemas: None,
552                })
553            }
554        }
555    };
556
557    let expanded = quote! {
558        impl #name {
559            /// Generate the AsyncAPI specification
560            ///
561            /// Returns an AsyncApiSpec with Info, Servers, Channels, and Operations
562            /// sections populated from attributes.
563            pub fn asyncapi_spec() -> asyncapi_rust::AsyncApiSpec {
564                asyncapi_rust::AsyncApiSpec {
565                    asyncapi: "3.0.0".to_string(),
566                    info: asyncapi_rust::Info {
567                        title: #title.to_string(),
568                        version: #version.to_string(),
569                        description: #description,
570                    },
571                    servers: #servers_code,
572                    channels: #channels_code,
573                    operations: #operations_code,
574                    components: #components_code,
575                }
576            }
577        }
578    };
579
580    TokenStream::from(expanded)
581}
582
583#[cfg(test)]
584mod tests {
585    #[test]
586    fn test_placeholder() {
587        // Macro expansion tests will go here
588    }
589}