Skip to main content

sword_macros/
lib.rs

1#![allow(irrefutable_let_patterns)]
2
3mod controllers;
4
5mod core;
6mod errors;
7mod interceptor_derive;
8mod shared;
9mod interceptor {
10    mod parse;
11    pub use parse::InterceptorArgs;
12}
13
14use proc_macro::TokenStream;
15use quote::quote;
16use syn::{DeriveInput, parse_macro_input};
17
18#[cfg(feature = "web-controllers")]
19#[proc_macro_attribute]
20pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
21    controllers::web::attributes::attribute("GET", attr, item)
22}
23
24#[cfg(feature = "web-controllers")]
25#[proc_macro_attribute]
26pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
27    controllers::web::attributes::attribute("POST", attr, item)
28}
29
30#[cfg(feature = "web-controllers")]
31#[proc_macro_attribute]
32pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
33    controllers::web::attributes::attribute("PUT", attr, item)
34}
35
36#[cfg(feature = "web-controllers")]
37#[proc_macro_attribute]
38pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
39    controllers::web::attributes::attribute("DELETE", attr, item)
40}
41
42#[cfg(feature = "web-controllers")]
43#[proc_macro_attribute]
44pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
45    controllers::web::attributes::attribute("PATCH", attr, item)
46}
47
48#[cfg(feature = "web-controllers")]
49#[proc_macro_attribute]
50pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream {
51    controllers::web::attributes::attribute("HEAD", attr, item)
52}
53
54#[cfg(feature = "web-controllers")]
55#[proc_macro_attribute]
56pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream {
57    controllers::web::attributes::attribute("OPTIONS", attr, item)
58}
59
60#[cfg(feature = "web-controllers")]
61#[proc_macro_attribute]
62pub fn trace(attr: TokenStream, item: TokenStream) -> TokenStream {
63    controllers::web::attributes::attribute("TRACE", attr, item)
64}
65
66#[cfg(feature = "web-controllers")]
67#[proc_macro_attribute]
68pub fn connect(attr: TokenStream, item: TokenStream) -> TokenStream {
69    controllers::web::attributes::attribute("CONNECT", attr, item)
70}
71
72/// Defines a Sword controller.
73/// Route handlers are declared directly inside the `impl` block using method attributes
74/// such as `#[get]`, `#[post]`, `#[put]`, `#[patch]`, `#[delete]`, `#[head]`, `#[options]`, `#[trace]`, and `#[connect]`.
75///
76/// ### Parameters
77/// - `kind`: Controller kind. Use `Controller::Web`, `Controller::SocketIo`, `Controller::Grpc`, or `Controller::EventHandler`.
78/// - `path`: Required when `kind = Controller::Web`.
79/// - `namespace`: Required when `kind = Controller::SocketIo`.
80/// - `service`: Required when `kind = Controller::Grpc`.
81/// - `source`: Required when `kind = Controller::EventHandler`. Use `EventSource::Memory`.
82///
83/// ### Usage
84/// ```rust,ignore
85/// #[controller(kind = Controller::Web, path = "/base_path")]
86/// struct MyController {}
87///
88/// impl MyController {
89///     #[get("/sub_path")]
90///     async fn my_handler(&self) -> WebResult {
91///        Ok(JsonResponse::Ok().message("Hello from MyController"))
92///     }
93/// }
94/// ```
95///
96/// ```rust,ignore
97/// #[controller(kind = Controller::SocketIo, namespace = "/chat")]
98/// struct ChatController;
99///
100/// impl ChatController {
101///     #[on("connection")]
102///     async fn on_connect(&self, _ctx: SocketContext) {}
103/// }
104/// ```
105#[proc_macro_attribute]
106pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
107    controllers::expand_controller(attr, item).unwrap_or_else(|err| err.to_compile_error().into())
108}
109
110/// Derive macro for creating interceptors.
111///
112/// Generates implementations for the `Interceptor` trait.
113///
114/// # Usage
115/// ```rust,ignore
116/// use sword::prelude::*;
117///
118/// #[derive(Interceptor)]
119/// struct MyInterceptor;
120///
121/// // then implement some Interceptor trait variants
122/// // depending on the controller kind (e.g. OnRequest, OnConnect.)
123/// ```
124#[proc_macro_derive(Interceptor)]
125pub fn derive_interceptor(input: TokenStream) -> TokenStream {
126    interceptor_derive::derive_interceptor(input)
127        .unwrap_or_else(|err| err.to_compile_error().into())
128}
129
130/// Marks a route or controller with one or more interceptors.
131/// This macro can be used to apply an `Interceptor` to different controller kinds,
132/// such as web controllers or Socket.IO controllers.
133#[proc_macro_attribute]
134pub fn interceptor(attr: TokenStream, item: TokenStream) -> TokenStream {
135    let _ = attr;
136    item
137}
138
139/// Defines a configuration struct for the application.
140/// This macro generates the necessary code to deserialize the struct from
141/// the configuration toml file.
142///
143/// The struct must derive `Deserialize` from `serde`.
144///
145/// ### Parameters
146/// - `key`: The key in the configuration file where the struct is located.
147///
148/// ### Usage
149///
150/// ```rust,ignore
151/// #[config(key = "my-section")]
152/// #[derive(Debug, Deserialize)]
153/// struct MyConfig {
154///     my_key: String,
155/// }
156/// ```
157#[proc_macro_attribute]
158pub fn config(args: TokenStream, item: TokenStream) -> TokenStream {
159    let input = parse_macro_input!(item as DeriveInput);
160
161    match core::config::expand_config_struct(args, &input) {
162        Ok(tokens) => tokens,
163        Err(err) => err.to_compile_error().into(),
164    }
165}
166
167/// Marks a struct as injectable.
168///
169/// This macro generates the necessary code to register the struct
170/// in the dependency injection container. It can be used with or without
171/// parameters.
172///
173/// ### Parameters
174///
175/// - `kind`: (Optional) Specifies the kind of injectable.
176///   It can be either `provider` or `component`.
177///
178///  `provider`: The struct that has to be instantiated manually and
179///   registered in the container. The struct will be treated as a singleton by default.
180///
181///  `component`: The struct will be instantiated automatically by the container
182///   based on its dependencies. It's also treated as a singleton by default.
183///
184///   By default, if no kind is provided, it will be treated as `component`.
185///
186/// - `no_derive_clone`: (Optional) If provided, the struct will not derive the `Clone` automatically.
187///   By default, the struct will derive `Clone` if all its fields implement `Clone`.
188///
189/// ### Usage of `#[injectable]` without parameters (same as #[injectable(component)])
190///
191/// ```rust,ignore
192/// #[injectable]
193/// pub struct TaskRepository {
194///     db: Database,
195/// }
196///
197/// impl TaskRepository {
198///     pub async fn create(&self, task: Value) {
199///         self.db.insert("tasks", task).await;
200///     }
201///
202///     pub async fn find_all(&self) -> Option<Vec<Value>> {
203///         self.db.get_all("tasks").await
204///     }
205/// }
206/// ```
207///
208/// ### Usage of `#[injectable(provider)]` with parameters
209///
210/// ```rust,ignore
211/// #[injectable(provider)]
212/// pub struct Database {
213///     db: Store,
214/// }
215///
216/// impl Database {
217///     pub async fn new(db_conf: DatabaseConfig) -> Self {
218///         let db = Arc::new(RwLock::new(HashMap::new()));
219///
220///         db.write().await.insert(db_conf.collection_name, Vec::new());
221///
222///         Self { db }
223///     }
224///
225///     pub async fn insert(&self, table: &'static str, record: Value) {
226///         let mut db = self.db.write().await;
227///
228///         if let Some(table_data) = db.get_mut(table) {
229///             table_data.push(record);
230///         }
231///     }
232///
233///     pub async fn get_all(&self, table: &'static str) -> Option<Vec<Value>> {
234///         let db = self.db.read().await;
235///
236///         db.get(table).cloned()
237///     }
238/// }
239/// ```
240#[proc_macro_attribute]
241pub fn injectable(attr: TokenStream, item: TokenStream) -> TokenStream {
242    core::injectable::expand_injectable(attr, item)
243        .unwrap_or_else(|err| err.to_compile_error().into())
244}
245
246/// Derive macro for HTTP error enums.
247///
248/// Generates implementations for:
249/// - `From<Self> for JsonResponse` - Converts error to JSON response
250/// - `IntoResponse` - Allows returning error directly from handlers
251///
252/// **Note**: Use with `thiserror::Error` for `Display`, `Error`, and `#[from]`.
253///
254/// # Attributes
255///
256/// Enum-level defaults can be declared with `#[http_error(...)]` and overridden per
257/// variant with `#[http(...)]`.
258///
259/// **For direct responses:**
260/// - `code = <u16>`: HTTP status code (required)
261/// - `message = "<string>"`: Static client message (optional)
262/// - `message = <field>`: Uses a named field as the client message (optional)
263/// - `error = <field>`: Single error field to include (optional, named fields only)
264/// - `errors = <field>`: Multiple errors field to include (optional, named fields only)
265///
266/// **For delegation:**
267/// - `transparent`: Delegate to inner type's `From<T> for Json` (for wrapping other `HttpError` types)
268///
269/// **Tracing:**
270/// - `tracing = <level>` inside `#[http_error(...)]` or `#[http(...)]`
271/// - `#[tracing(level)]`: Backward-compatible shorthand at variant level
272///   - `level`: One of `trace`, `debug`, `info`, `warn`, `error`
273///   - Uses the internal `thiserror::Error` display for the `error` log field
274///   - Logs variant fields as structured tracing fields when available
275///   - Compatible with `RUST_LOG` for filtering
276///   - Not allowed with `transparent` variants
277///
278/// ### Tracing Output
279/// The generated logs include:
280/// - `error`: The internal `thiserror` display string
281/// - `error_type`: The variant name as string
282/// - `status_code`: The HTTP status code
283/// - For named variants: Each field as `field_name = ?field_value`
284/// - For unnamed variants (single field): `inner = ?field`
285/// - Unit variants: `error`, `error_type`, and `status_code`
286///
287/// # Example
288///
289/// ```rust,ignore
290/// use sword::prelude::*;
291/// use thiserror::Error;
292///
293/// #[derive(Debug, Error, HttpError)]
294/// #[http_error(code = 500, tracing = error, message = "Internal server error")]
295/// pub enum ApiError {
296///     #[error("Not found")]
297///     #[http(code = 404, message = "Not found", tracing = info)]
298///     NotFound,
299///
300///     #[error("Conflict on field {field}: {value}")]
301///     #[http(code = 409, message = client_message, error = detail)]
302///     Conflict {
303///         client_message: String,
304///         field: String,
305///         value: String,
306///         detail: serde_json::Value,
307///     },
308///
309///     #[error("IO Error: {0}")]
310///     Io(#[from] std::io::Error),
311///
312///     #[error("Auth Error: {0}")]
313///     #[http(transparent)]  // Delegates to other "HttpError" derivation
314///     Auth(#[from] AuthError),
315/// }
316/// ```
317#[proc_macro_derive(HttpError, attributes(http, http_error, tracing))]
318#[cfg(feature = "web-controllers")]
319pub fn derive_http_error(input: TokenStream) -> TokenStream {
320    let input = parse_macro_input!(input as DeriveInput);
321
322    match errors::derive_http_error(input) {
323        Ok(tokens) => tokens.into(),
324        Err(err) => err.to_compile_error().into(),
325    }
326}
327
328/// Derive macro for gRPC error enums.
329///
330/// Generates:
331/// - `From<Self> for tonic::Status`
332///
333/// Enum-level defaults can be declared with `#[grpc_error(...)]` and overridden per
334/// variant with `#[grpc(...)]`.
335///
336/// Supported attributes:
337/// - `code = "invalid_argument"`
338/// - `message = "custom text"`
339/// - `message = field_name`
340/// - `transparent` (variant-only)
341/// - `tracing = <level>` inside `#[grpc_error(...)]` or `#[grpc(...)]`
342/// - `#[tracing(level)]`: backward-compatible shorthand at variant level
343///
344/// gRPC code values accepted by `#[grpc(code = "...")]`:
345///
346/// - `ok`
347/// - `cancelled`
348/// - `unknown`
349/// - `invalid_argument`
350/// - `deadline_exceeded`
351/// - `not_found`
352/// - `already_exists`
353/// - `permission_denied`
354/// - `resource_exhausted`
355/// - `failed_precondition`
356/// - `aborted`
357/// - `out_of_range`
358/// - `unimplemented`
359/// - `internal`
360/// - `unavailable`
361/// - `data_loss`
362/// - `unauthenticated`
363///
364/// # Example
365///
366/// ```rust,ignore
367/// use sword::prelude::*;
368/// use thiserror::Error;
369///
370/// #[derive(Debug, Error, GrpcError)]
371/// #[grpc_error(code = "internal", tracing = error)]
372/// enum UserError {
373///     #[grpc(code = "not_found", tracing = info)]
374///     #[error("User not found: {id}")]
375///     NotFound { id: String },
376///
377///     #[grpc(code = "invalid_argument", message = client_message)]
378///     #[error("Validation error: {internal}")]
379///     Validation {
380///         client_message: String,
381///         internal: String,
382///     },
383///
384///     #[grpc(transparent)]
385///     #[error("Database error: {0}")]
386///     Database(#[from] anyhow::Error),
387/// }
388/// ```
389#[proc_macro_derive(GrpcError, attributes(grpc, grpc_error, tracing))]
390pub fn derive_grpc_error(input: TokenStream) -> TokenStream {
391    let input = parse_macro_input!(input as DeriveInput);
392
393    match errors::derive_grpc_error(input) {
394        Ok(tokens) => tokens.into(),
395        Err(err) => err.to_compile_error().into(),
396    }
397}
398
399/// ### This is just a re-export of `tokio::main` to simplify the initial setup of
400/// ### Sword, you can use your own version of tokio adding it to your
401/// ### `Cargo.toml`, we are providing this initial base by default
402///
403/// ---
404///
405/// Marks async function to be executed by the selected runtime. This macro
406/// helps set up a `Runtime` without requiring the user to use
407/// [Runtime](../tokio/runtime/struct.Runtime.html) or
408/// [Builder](../tokio/runtime/struct.Builder.html) directly.
409///
410/// Note: This macro is designed to be simplistic and targets applications that
411/// do not require a complex setup. If the provided functionality is not
412/// sufficient, you may be interested in using
413/// [Builder](../tokio/runtime/struct.Builder.html), which provides a more
414/// powerful interface.
415///
416/// Note: This macro can be used on any function and not just the `main`
417/// function. Using it on a non-main function makes the function behave as if it
418/// was synchronous by starting a new runtime each time it is called. If the
419/// function is called often, it is preferable to create the runtime using the
420/// runtime builder so the runtime can be reused across calls.
421///
422/// # Non-worker async function
423///
424/// Note that the async function marked with this macro does not run as a
425/// worker. The expectation is that other tasks are spawned by the function here.
426/// Awaiting on other futures from the function provided here will not
427/// perform as fast as those spawned as workers.
428///
429/// # Multi-threaded runtime
430///
431/// To use the multi-threaded runtime, the macro can be configured using
432///
433/// ```rust,ignore
434/// #[tokio::main(flavor = "multi_thread", worker_threads = 10)]
435/// # async fn main() {}
436/// ```
437///
438/// The `worker_threads` option configures the number of worker threads, and
439/// defaults to the number of cpus on the system. This is the default flavor.
440///
441/// Note: The multi-threaded runtime requires the `rt-multi-thread` feature
442/// flag.
443///
444/// # Current thread runtime
445///
446/// To use the single-threaded runtime known as the `current_thread` runtime,
447/// the macro can be configured using
448///
449/// ```rust,ignore
450/// #[tokio::main(flavor = "current_thread")]
451/// # async fn main() {}
452/// ```
453///
454/// ## Function arguments:
455///
456/// Arguments are allowed for any functions aside from `main` which is special
457///
458/// ## Usage
459///
460/// ### Using the multi-thread runtime
461///
462/// ```ignore
463/// #[tokio::main]
464/// async fn main() {
465///     println!("Hello world");
466/// }
467/// ```
468///
469/// Equivalent code not using `#[tokio::main]`
470///
471/// ```ignore
472/// fn main() {
473///     tokio::runtime::Builder::new_multi_thread()
474///         .enable_all()
475///         .build()
476///         .unwrap()
477///         .block_on(async {
478///             println!("Hello world");
479///         })
480/// }
481/// ```
482///
483/// ### Using current thread runtime
484///
485/// The basic scheduler is single-threaded.
486///
487/// ```ignore
488/// #[tokio::main(flavor = "current_thread")]
489/// async fn main() {
490///     println!("Hello world");
491/// }
492/// ```
493///
494/// Equivalent code not using `#[tokio::main]`
495///
496/// ```ignore
497/// fn main() {
498///     tokio::runtime::Builder::new_current_thread()
499///         .enable_all()
500///         .build()
501///         .unwrap()
502///         .block_on(async {
503///             println!("Hello world");
504///         })
505/// }
506/// ```
507///
508/// ### Set number of worker threads
509///
510/// ```ignore
511/// #[tokio::main(worker_threads = 2)]
512/// async fn main() {
513///     println!("Hello world");
514/// }
515/// ```
516///
517/// Equivalent code not using `#[tokio::main]`
518///
519/// ```ignore
520/// fn main() {
521///     tokio::runtime::Builder::new_multi_thread()
522///         .worker_threads(2)
523///         .enable_all()
524///         .build()
525///         .unwrap()
526///         .block_on(async {
527///             println!("Hello world");
528///         })
529/// }
530/// ```
531///
532/// ### Configure the runtime to start with time paused
533///
534/// ```ignore
535/// #[tokio::main(flavor = "current_thread", start_paused = true)]
536/// async fn main() {
537///     println!("Hello world");
538/// }
539/// ```
540///
541/// Equivalent code not using `#[tokio::main]`
542///
543/// ```ignore
544/// fn main() {
545///     tokio::runtime::Builder::new_current_thread()
546///         .enable_all()
547///         .start_paused(true)
548///         .build()
549///         .unwrap()
550///         .block_on(async {
551///             println!("Hello world");
552///         })
553/// }
554/// ```
555///
556/// Note that `start_paused` requires the `test-util` feature to be enabled.
557///
558/// ### Rename package
559///
560/// ```ignore
561/// use tokio as tokio1;
562///
563/// #[tokio1::main(crate = "tokio1")]
564/// async fn main() {
565///     println!("Hello world");
566/// }
567/// ```
568///
569/// Equivalent code not using `#[tokio::main]`
570///
571/// ```ignore
572/// use tokio as tokio1;
573///
574/// fn main() {
575///     tokio1::runtime::Builder::new_multi_thread()
576///         .enable_all()
577///         .build()
578///         .unwrap()
579///         .block_on(async {
580///             println!("Hello world");
581///         })
582/// }
583/// ```
584///
585/// ### Configure unhandled panic behavior
586///
587/// Available options are `shutdown_runtime` and `ignore`. For more details, see
588/// [`Builder::unhandled_panic`].
589///
590/// This option is only compatible with the `current_thread` runtime.
591///
592/// ```no_run, ignore
593/// # #![allow(unknown_lints, unexpected_cfgs)]
594/// #[cfg(tokio_unstable)]
595/// #[tokio::main(flavor = "current_thread", unhandled_panic = "shutdown_runtime")]
596/// async fn main() {
597///     let _ = tokio::spawn(async {
598///         panic!("This panic will shutdown the runtime.");
599///     }).await;
600/// }
601/// # #[cfg(not(tokio_unstable))]
602/// # fn main() { }
603/// ```
604///
605/// Equivalent code not using `#[tokio::main]`
606///
607/// ```no_run, ignore
608/// # #![allow(unknown_lints, unexpected_cfgs)]
609/// #[cfg(tokio_unstable)]
610/// fn main() {
611///     tokio::runtime::Builder::new_current_thread()
612///         .enable_all()
613///         .unhandled_panic(UnhandledPanic::ShutdownRuntime)
614///         .build()
615///         .unwrap()
616///         .block_on(async {
617///             let _ = tokio::spawn(async {
618///                 panic!("This panic will shutdown the runtime.");
619///             }).await;
620///         })
621/// }
622/// # #[cfg(not(tokio_unstable))]
623/// # fn main() { }
624/// ```
625///
626/// **Note**: This option depends on Tokio's [unstable API][unstable]. See [the
627/// documentation on unstable features][unstable] for details on how to enable
628/// Tokio's unstable features.
629///
630/// [`Builder::unhandled_panic`]: ../tokio/runtime/struct.Builder.html#method.unhandled_panic
631/// [unstable]: ../tokio/index.html#unstable-features
632#[proc_macro_attribute]
633pub fn main(_args: TokenStream, item: TokenStream) -> TokenStream {
634    let input = parse_macro_input!(item as syn::ItemFn);
635
636    let fn_body = input.block.clone();
637    let fn_attrs = input.attrs.clone();
638    let fn_vis = input.vis.clone();
639    let _fn_sig = input.sig;
640
641    #[allow(unused)]
642    let mut output = quote! {};
643
644    if cfg!(feature = "hot-reload") {
645        output = quote! {
646            async fn __internal_main() {
647                #fn_body
648            }
649
650            #(#fn_attrs)*
651            #fn_vis fn main() {
652                ::sword::internal::tokio_runtime::Builder::new_multi_thread()
653                    .enable_all()
654                    .build()
655                    .unwrap_or_else(|err| {
656                        ::sword::internal::core::sword_error!(
657                            title: "Failed to build Tokio runtime",
658                            reason: err,
659                            context: {
660                                "source" => "#[sword::main]",
661                            },
662                        )
663                    })
664                    .block_on(::sword::internal::dioxus_devtools::serve_subsecond(__internal_main))
665            }
666        };
667    } else {
668        output = quote! {
669            #(#fn_attrs)*
670            #fn_vis fn main() {
671                ::sword::internal::tokio_runtime::Builder::new_multi_thread()
672                    .enable_all()
673                    .build()
674                    .unwrap_or_else(|err| {
675                        ::sword::internal::core::sword_error!(
676                            title: "Failed to build Tokio runtime",
677                            reason: err,
678                            context: {
679                                "source" => "#[sword::main]",
680                            },
681                        )
682                    })
683                    .block_on( async #fn_body )
684            }
685        };
686    }
687
688    output.into()
689}
690
691#[cfg(feature = "event-handlers")]
692/// Defines an event struct for use with the event queue.
693///
694/// Generates the `Event` trait implementation and derives `Clone`.
695///
696/// # Example
697///
698/// ```rust,ignore
699/// #[event(key = "mail.send.failed")]
700/// struct MailFailedEvent {
701///     to: String,
702///     error: String,
703/// }
704/// ```
705#[proc_macro_attribute]
706pub fn event(args: TokenStream, item: TokenStream) -> TokenStream {
707    core::event::expand_event_struct(args, item).unwrap_or_else(|err| err.to_compile_error().into())
708}
709
710#[cfg(feature = "event-handlers")]
711/// Registers an event handler method for an `EventHandler` controller.
712///
713/// The attribute takes the full event key as argument, which must match
714/// the `key` of the event being published via `EventPublisher`.
715///
716/// # Example
717///
718/// ```rust,ignore
719/// #[event(key = "mail.send.failed")]
720/// struct MailFailedEvent {
721///     to: String,
722///     error: String,
723/// }
724///
725/// #[controller(kind = Controller::EventHandler, source = EventSource::Memory)]
726/// struct MailHandler {
727///     mailer: Arc<Mailer>,
728/// }
729///
730/// impl MailHandler {
731///     #[handle("mail.send.failed")]
732///     async fn on_failed(&self, event: MailFailedEvent) -> Result<()> {
733///         self.mailer.resend(&event.to).await?;
734///         Ok(())
735///     }
736/// }
737/// ```
738#[proc_macro_attribute]
739pub fn handle(attr: TokenStream, item: TokenStream) -> TokenStream {
740    controllers::event_handler::expand_handle(attr, item)
741        .unwrap_or_else(|err| err.to_compile_error().into())
742}
743
744#[cfg(feature = "socketio-controllers")]
745/// Unified handler attribute for Socket.IO events.
746///
747/// ### Event Types
748/// - `#[on("connection")]` - Called when a client connects
749/// - `#[on("disconnection")]` - Called when a client disconnects
750/// - `#[on("fallback")]` - Called for unhandled events
751/// - `#[on("custom_event")]` - Called for custom event names
752///
753/// ### Parameters
754/// All handlers receive `&self` and `ctx: SocketContext` which provides access to:
755/// - Socket operations via `ctx`
756/// - Message data via `ctx.try_data::<T>()`
757/// - Event name via `ctx.event()`
758/// - Acknowledgments via `ctx.ack()`
759///
760/// ### Usage
761/// ```rust,ignore
762/// #[controller(kind = Controller::SocketIo, namespace = "/chat")]
763/// pub struct ChatController { ... }
764///
765/// impl ChatController {
766///     #[on("connection")]
767///     async fn on_connect(&self, ctx: SocketContext) {
768///         println!("Client connected: {}", ctx.id());
769///     }
770///
771///     #[on("message")]
772///     async fn handle_message(&self, ctx: SocketContext) {
773///         let msg: String = ctx.try_data().unwrap();
774///         println!("Received: {}", msg);
775///     }
776/// }
777/// ```
778#[proc_macro_attribute]
779pub fn on(attr: TokenStream, item: TokenStream) -> TokenStream {
780    controllers::expand_on_handler(attr, item).unwrap_or_else(|err| err.to_compile_error().into())
781}