acton_macro/lib.rs
1/*
2 * Copyright (c) 2024. Govcraft
3 *
4 * Licensed under either of
5 * * Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 * * MIT license: http://opensource.org/licenses/MIT
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the applicable License for the specific language governing permissions and
14 * limitations under that License.
15 */
16#![forbid(unsafe_code)]
17
18//! Acton Macro Library
19//!
20//! This library provides procedural macros for the Acton actor framework.
21//! It includes macros to derive common traits and boilerplate code for Acton messages
22//! and actors.
23//!
24//! # Message Macro
25//!
26//! The [`acton_message`] macro simplifies creating message types for actor communication:
27//!
28//! ```ignore
29//! // Basic message for internal actor communication
30//! #[acton_message]
31//! pub struct Ping;
32//!
33//! // IPC-enabled message with serialization support
34//! #[acton_message(ipc)]
35//! pub struct Request {
36//! pub query: String,
37//! }
38//! ```
39//!
40//! # Actor Macro
41//!
42//! The [`acton_actor`] macro simplifies creating actor state types:
43//!
44//! ```ignore
45//! #[acton_actor]
46//! pub struct Counter {
47//! count: i32,
48//! }
49//! ```
50//!
51//! # Main Entry Point
52//!
53//! The [`acton_main`] macro provides a convenient entry point for Acton applications:
54//!
55//! ```ignore
56//! use acton_reactive::prelude::*;
57//!
58//! #[acton_main]
59//! async fn main() {
60//! let mut app = ActonApp::launch_async().await;
61//! // ... your application logic
62//! app.shutdown_all().await;
63//! }
64//! ```
65
66use proc_macro::TokenStream;
67
68use quote::quote;
69use syn::{parse_macro_input, DeriveInput, ItemFn};
70
71fn has_derive(input: &DeriveInput, trait_name: &str) -> bool {
72 input.attrs.iter().any(|attr| {
73 if attr.path().is_ident("derive") {
74 let mut found = false;
75 let _ = attr.parse_nested_meta(|meta| {
76 if meta.path.is_ident(trait_name) {
77 found = true;
78 }
79 Ok(())
80 });
81 found
82 } else {
83 false
84 }
85 })
86}
87
88
89/// Configuration options parsed from `#[acton_message(...)]` attributes.
90#[derive(Default)]
91struct MessageConfig {
92 /// Enable serde serialization for IPC support.
93 ipc: bool,
94}
95
96impl MessageConfig {
97 /// Parse configuration from attribute tokens.
98 fn parse(attr: &TokenStream) -> Self {
99 let mut config = Self::default();
100
101 // Parse the attribute stream to look for known options
102 let attr_string = attr.to_string();
103 for part in attr_string.split(',') {
104 let trimmed = part.trim();
105 if trimmed == "ipc" {
106 config.ipc = true;
107 }
108 }
109
110 config
111 }
112}
113
114/// Configuration options parsed from `#[acton_actor(...)]` attributes.
115#[derive(Default)]
116struct ActorConfig {
117 /// Skip deriving Default (user will implement it manually).
118 no_default: bool,
119}
120
121impl ActorConfig {
122 /// Parse configuration from attribute tokens.
123 fn parse(attr: &TokenStream) -> Self {
124 let mut config = Self::default();
125
126 // Parse the attribute stream to look for known options
127 let attr_string = attr.to_string();
128 for part in attr_string.split(',') {
129 let trimmed = part.trim();
130 if trimmed == "no_default" {
131 config.no_default = true;
132 }
133 }
134
135 config
136 }
137}
138
139/// A procedural macro to derive the necessary traits for an Acton message.
140///
141/// This macro automatically implements the traits required for a type to be used
142/// as a message in the Acton actor framework. It ensures compile-time verification
143/// that the message type satisfies `Send + Sync` bounds.
144///
145/// # Basic Usage
146///
147/// ```ignore
148/// use acton_macro::acton_message;
149///
150/// #[acton_message]
151/// pub struct Ping;
152///
153/// #[acton_message]
154/// pub struct Increment {
155/// pub amount: u32,
156/// }
157/// ```
158///
159/// This expands to:
160/// - `#[derive(Clone, Debug)]` (if not already present)
161/// - A compile-time assertion that the type is `Send + Sync + 'static`
162///
163/// # IPC Support
164///
165/// For messages that need to cross process boundaries via IPC, use the `ipc` option:
166///
167/// ```ignore
168/// use acton_macro::acton_message;
169///
170/// #[acton_message(ipc)]
171/// pub struct Request {
172/// pub query: String,
173/// }
174/// ```
175///
176/// This additionally derives `serde::Serialize` and `serde::Deserialize`.
177///
178/// **Note:** The `ipc` option requires `serde` to be available in scope. When using
179/// `acton-reactive` with the `ipc` feature enabled, serde is automatically available.
180#[proc_macro_attribute]
181pub fn acton_message(attr: TokenStream, item: TokenStream) -> TokenStream {
182 // Parse configuration from attributes
183 let config = MessageConfig::parse(&attr);
184
185 // Parse the input tokens into a syntax tree.
186 let input = parse_macro_input!(item as DeriveInput);
187
188 // Get the name and generics of the struct.
189 let name = &input.ident;
190 let generics = &input.generics;
191 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
192
193 // Determine which traits need to be derived
194 let need_clone = !has_derive(&input, "Clone");
195 let need_debug = !has_derive(&input, "Debug");
196
197 // Build the list of traits to derive
198 let derives = {
199 let mut traits = Vec::new();
200 if need_clone {
201 traits.push(quote!(Clone));
202 }
203 if need_debug {
204 traits.push(quote!(Debug));
205 }
206 if config.ipc {
207 // Only add serde derives if not already present
208 if !has_derive(&input, "Serialize") {
209 traits.push(quote!(serde::Serialize));
210 }
211 if !has_derive(&input, "Deserialize") {
212 traits.push(quote!(serde::Deserialize));
213 }
214 }
215 if traits.is_empty() {
216 quote!()
217 } else {
218 quote!(#[derive(#(#traits),*)])
219 }
220 };
221
222 // Generate a unique identifier for the static assertion to avoid conflicts
223 let assert_ident = quote::format_ident!("_AssertActonMessage_{}", name);
224
225 let expanded = quote! {
226 #derives
227 #input
228
229 // Compile-time assertion that the message type satisfies Send + Sync + 'static.
230 // This catches invalid message types early with clear error messages.
231 #[doc(hidden)]
232 #[allow(dead_code, non_camel_case_types, non_snake_case, clippy::needless_lifetimes)]
233 const _: () = {
234 fn #assert_ident #impl_generics () #where_clause {
235 fn assert_bounds<T: Send + Sync + 'static>() {}
236 assert_bounds::<#name #ty_generics>();
237 }
238 };
239 };
240
241 // Return the generated tokens.
242 TokenStream::from(expanded)
243}
244
245/// A procedural macro to derive boilerplate traits for Acton actors.
246///
247/// This macro automatically implements the traits required for a type to be used
248/// as an actor's state (model) in the Acton framework. It provides compile-time
249/// verification that the actor type satisfies necessary bounds.
250///
251/// # Usage
252///
253/// ```ignore
254/// use acton_macro::acton_actor;
255///
256/// #[acton_actor]
257/// pub struct Counter {
258/// count: i32,
259/// }
260///
261/// #[acton_actor]
262/// pub struct ChatRoom {
263/// messages: Vec<String>,
264/// participants: Vec<String>,
265/// }
266/// ```
267///
268/// This expands to:
269/// - `#[derive(Default, Debug)]` (only traits not already present)
270/// - A compile-time assertion that the type is `Send + 'static`
271///
272/// # Options
273///
274/// ## `no_default`
275///
276/// Skip deriving `Default` when you need to implement it manually (e.g., when
277/// a field's type doesn't implement `Default`):
278///
279/// ```ignore
280/// use std::io::{stdout, Stdout};
281///
282/// #[acton_actor(no_default)]
283/// struct Printer {
284/// out: Stdout,
285/// }
286///
287/// impl Default for Printer {
288/// fn default() -> Self {
289/// Self { out: stdout() }
290/// }
291/// }
292/// ```
293///
294/// # Note
295///
296/// Actor state types must implement `Default` because actors are initialized
297/// with their default state before handlers are registered. When using
298/// `no_default`, you must provide your own `Default` implementation.
299#[proc_macro_attribute]
300pub fn acton_actor(attr: TokenStream, item: TokenStream) -> TokenStream {
301 // Parse configuration from attributes
302 let config = ActorConfig::parse(&attr);
303
304 // Parse the input tokens into a syntax tree.
305 let input = parse_macro_input!(item as DeriveInput);
306
307 // Get the name and generics of the struct.
308 let name = &input.ident;
309 let generics = &input.generics;
310 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
311
312 // Determine which traits need to be derived
313 let need_default = !config.no_default && !has_derive(&input, "Default");
314 let need_debug = !has_derive(&input, "Debug");
315
316 // Build the list of traits to derive
317 let derives = {
318 let mut traits = Vec::new();
319 if need_default {
320 traits.push(quote!(Default));
321 }
322 if need_debug {
323 traits.push(quote!(Debug));
324 }
325 if traits.is_empty() {
326 quote!()
327 } else {
328 quote!(#[derive(#(#traits),*)])
329 }
330 };
331
332 // Generate a unique identifier for the static assertion to avoid conflicts
333 let assert_ident = quote::format_ident!("_AssertActonActor_{}", name);
334
335 let expanded = quote! {
336 #derives
337 #input
338
339 // Compile-time assertion that the actor type satisfies Send + 'static.
340 // This catches invalid actor types early with clear error messages.
341 #[doc(hidden)]
342 #[allow(dead_code, non_camel_case_types, non_snake_case, clippy::needless_lifetimes)]
343 const _: () = {
344 fn #assert_ident #impl_generics () #where_clause {
345 fn assert_bounds<T: Send + 'static>() {}
346 assert_bounds::<#name #ty_generics>();
347 }
348 };
349 };
350
351 // Return the generated tokens.
352 TokenStream::from(expanded)
353}
354
355/// Entry point macro for Acton applications.
356///
357/// This macro marks an async function as the entry point for an Acton application,
358/// setting up the async runtime automatically. It is a convenience wrapper that
359/// eliminates the need to directly reference the underlying async runtime.
360///
361/// # Usage
362///
363/// ```ignore
364/// use acton_reactive::prelude::*;
365///
366/// #[acton_main]
367/// async fn main() {
368/// let mut app = ActonApp::launch_async().await;
369/// // ... your application logic
370/// app.shutdown_all().await;
371/// }
372/// ```
373///
374/// # Configuration
375///
376/// The macro supports optional configuration for the runtime:
377///
378/// - `flavor`: The runtime flavor (`"multi_thread"` or `"current_thread"`)
379/// - `worker_threads`: Number of worker threads (only for multi-threaded runtime)
380///
381/// ```ignore
382/// // Use single-threaded runtime
383/// #[acton_main(flavor = "current_thread")]
384/// async fn main() { }
385///
386/// // Specify worker thread count
387/// #[acton_main(worker_threads = 4)]
388/// async fn main() { }
389/// ```
390///
391/// The default is a multi-threaded runtime with the default number of worker threads.
392#[proc_macro_attribute]
393pub fn acton_main(attr: TokenStream, item: TokenStream) -> TokenStream {
394 let input = parse_macro_input!(item as ItemFn);
395
396 let attrs = &input.attrs;
397 let vis = &input.vis;
398 let sig = &input.sig;
399 let body = &input.block;
400
401 // Validate that the function is async
402 if sig.asyncness.is_none() {
403 return syn::Error::new_spanned(
404 sig.fn_token,
405 "the async keyword is missing from the function declaration",
406 )
407 .to_compile_error()
408 .into();
409 }
410
411 // Validate function name is main
412 if sig.ident != "main" {
413 return syn::Error::new_spanned(
414 &sig.ident, // Keep reference to avoid moving sig.ident which is used later
415 "acton_main can only be applied to the main function",
416 )
417 .to_compile_error()
418 .into();
419 }
420
421 // Parse configuration attributes
422 let attr_string = attr.to_string();
423 let use_current_thread = attr_string.contains("current_thread");
424
425 // Extract worker_threads if specified
426 let worker_threads: Option<usize> = attr_string
427 .split(',')
428 .find(|s| s.contains("worker_threads"))
429 .and_then(|s| s.split('=').nth(1).and_then(|v| v.trim().parse().ok()));
430
431 // Generate the runtime builder based on configuration
432 let runtime_builder = if use_current_thread {
433 quote! {
434 ::acton_reactive::prelude::tokio::runtime::Builder::new_current_thread()
435 }
436 } else if let Some(threads) = worker_threads {
437 quote! {
438 ::acton_reactive::prelude::tokio::runtime::Builder::new_multi_thread()
439 .worker_threads(#threads)
440 }
441 } else {
442 quote! {
443 ::acton_reactive::prelude::tokio::runtime::Builder::new_multi_thread()
444 }
445 };
446
447 // Create the sync function signature (remove async)
448 let fn_name = &sig.ident;
449 let fn_inputs = &sig.inputs;
450 let fn_output = &sig.output;
451
452 let expanded = quote! {
453 #(#attrs)*
454 #vis fn #fn_name(#fn_inputs) #fn_output {
455 #runtime_builder
456 .enable_all()
457 .build()
458 .expect("Failed to build Acton runtime")
459 .block_on(async #body)
460 }
461 };
462
463 TokenStream::from(expanded)
464}