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 = ¶m.name;
538 let param_desc = if let Some(d) = ¶m.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) = ¶m.schema_type {
546 let format_field = if let Some(fmt) = ¶m.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}