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}