spring_macros/
lib.rs

1//! [![spring-rs](https://img.shields.io/github/stars/spring-rs/spring-rs)](https://spring-rs.github.io)
2#![doc(html_favicon_url = "https://spring-rs.github.io/favicon.ico")]
3#![doc(html_logo_url = "https://spring-rs.github.io/logo.svg")]
4
5mod auto;
6mod cache;
7mod config;
8mod problem_details;
9mod inject;
10mod job;
11mod middlewares;
12mod nest;
13mod route;
14#[cfg(feature = "socket_io")]
15mod socketioxide;
16mod stream;
17mod utils;
18
19#[cfg(feature = "sa-token")]
20mod sa_token;
21
22use proc_macro::TokenStream;
23use syn::DeriveInput;
24
25/// Creates resource handler.
26///
27/// # Syntax
28/// ```plain
29/// #[route("path", method="HTTP_METHOD"[, attributes])]
30/// ```
31///
32/// # Attributes
33/// - `"path"`: Raw literal string with path for which to register handler.
34/// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string,
35///   "GET", "POST" for example.
36///
37/// # Examples
38/// ```
39/// # use spring_web::axum::response::IntoResponse;
40/// # use spring_macros::route;
41/// #[route("/test", method = "GET", method = "HEAD")]
42/// async fn example() -> impl IntoResponse {
43///     "hello world"
44/// }
45/// ```
46#[proc_macro_attribute]
47pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
48    route::with_method(None, args, input, false)
49}
50
51/// Creates openapi resource handler.
52///
53/// # Syntax
54/// ```plain
55/// #[api_route("path", method="HTTP_METHOD"[, attributes])]
56/// ```
57///
58/// # Attributes
59/// - `"path"`: Raw literal string with path for which to register handler.
60/// - `method = "HTTP_METHOD"`: Registers HTTP method. Upper-case string,
61///   "GET", "POST" for example.
62///
63/// # Examples
64/// ```
65/// # use spring_web::axum::response::IntoResponse;
66/// # use spring_macros::api_route;
67/// #[api_route("/test", method = "GET", method = "HEAD")]
68/// async fn example() -> impl IntoResponse {
69///     "hello world"
70/// }
71/// ```
72#[proc_macro_attribute]
73pub fn api_route(args: TokenStream, input: TokenStream) -> TokenStream {
74    route::with_method(None, args, input, true)
75}
76
77/// Creates resource handler.
78///
79/// # Syntax
80/// ```plain
81/// #[routes]
82/// #[<method>("path", ...)]
83/// #[<method>("path", ...)]
84/// ...
85/// ```
86///
87/// # Attributes
88/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
89/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
90///
91/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
92///
93/// # Examples
94/// ```
95/// # use spring_web::axum::response::IntoResponse;
96/// # use spring_macros::routes;
97/// #[routes]
98/// #[get("/test")]
99/// #[get("/test2")]
100/// #[delete("/test")]
101/// async fn example() -> impl IntoResponse {
102///     "hello world"
103/// }
104/// ```
105#[proc_macro_attribute]
106pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
107    route::with_methods(input, false)
108}
109
110/// Creates openapi resource handler.
111///
112/// # Syntax
113/// ```plain
114/// #[api_routes]
115/// #[<method>("path", ...)]
116/// #[<method>("path", ...)]
117/// ...
118/// ```
119///
120/// # Attributes
121/// The `api_routes` macro itself has no parameters, but allows specifying the attribute macros for
122/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
123///
124/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
125///
126/// # Examples
127/// ```
128/// # use spring_web::axum::response::IntoResponse;
129/// # use spring_macros::api_routes;
130/// #[api_routes]
131/// #[get("/test")]
132/// #[get("/test2")]
133/// #[delete("/test")]
134/// async fn example() -> impl IntoResponse {
135///     "hello world"
136/// }
137/// ```
138#[proc_macro_attribute]
139pub fn api_routes(_: TokenStream, input: TokenStream) -> TokenStream {
140    route::with_methods(input, true)
141}
142
143macro_rules! method_macro {
144    ($variant:ident, $method:ident, $openapi:expr) => {
145        ///
146        /// # Syntax
147        /// ```plain
148        #[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
149        /// ```
150        ///
151        /// # Attributes
152        /// - `"path"`: Raw literal string with path for which to register handler.
153        ///
154        /// # Examples
155        /// ```
156        /// # use spring_web::axum::response::IntoResponse;
157        #[doc = concat!("# use spring_macros::", stringify!($method), ";")]
158        #[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
159        /// async fn example() -> impl IntoResponse {
160        ///     "hello world"
161        /// }
162        /// ```
163        #[proc_macro_attribute]
164        pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
165            route::with_method(Some(route::Method::$variant), args, input, $openapi)
166        }
167    };
168}
169
170method_macro!(Get, get, false);
171method_macro!(Post, post, false);
172method_macro!(Put, put, false);
173method_macro!(Delete, delete, false);
174method_macro!(Head, head, false);
175method_macro!(Options, options, false);
176method_macro!(Trace, trace, false);
177method_macro!(Patch, patch, false);
178
179method_macro!(Get, get_api, true);
180method_macro!(Post, post_api, true);
181method_macro!(Put, put_api, true);
182method_macro!(Delete, delete_api, true);
183method_macro!(Head, head_api, true);
184method_macro!(Options, options_api, true);
185method_macro!(Trace, trace_api, true);
186method_macro!(Patch, patch_api, true);
187
188/// Prepends a path prefix to all handlers using routing macros inside the attached module.
189///
190/// # Syntax
191///
192/// ```
193/// # use spring_macros::nest;
194/// #[nest("/prefix")]
195/// mod api {
196///     // ...
197/// }
198/// ```
199///
200/// # Arguments
201///
202/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
203///
204/// # Example
205///
206/// ```
207/// # use spring_macros::{nest, get};
208/// # use spring_web::axum::response::IntoResponse;
209/// #[nest("/api")]
210/// mod api {
211///     # use super::*;
212///     #[get("/hello")]
213///     pub async fn hello() -> impl IntoResponse {
214///         // this has path /api/hello
215///         "Hello, world!"
216///     }
217/// }
218/// # fn main() {}
219/// ```
220#[proc_macro_attribute]
221pub fn nest(args: TokenStream, input: TokenStream) -> TokenStream {
222    nest::with_nest(args, input)
223}
224
225/// Applies middleware layers to all route handlers within a module.
226///
227/// # Syntax
228/// ```plain
229/// #[middlewares(middleware1, middleware2, ...)]
230/// mod module_name {
231///     // route handlers
232/// }
233/// ```
234///
235/// # Arguments
236/// - `middleware1`, `middleware2`, etc. - Middleware expressions that will be applied to all routes in the module
237///
238/// This macro generates a router function that applies the specified middleware
239/// to all route handlers defined within the module.
240#[proc_macro_attribute]
241pub fn middlewares(args: TokenStream, input: TokenStream) -> TokenStream {
242    middlewares::middlewares(args, input)
243}
244
245fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
246    let compile_err = TokenStream::from(err.to_compile_error());
247    item.extend(compile_err);
248    item
249}
250
251/// Job
252///
253macro_rules! job_macro {
254    ($variant:ident, $job_type:ident, $example:literal) => {
255        ///
256        /// # Syntax
257        /// ```plain
258        #[doc = concat!("#[", stringify!($job_type), "(", $example, ")]")]
259        /// ```
260        ///
261        /// # Attributes
262        /// - `"path"`: Raw literal string with path for which to register handler.
263        ///
264        /// # Examples
265        /// ```
266        /// # use spring_web::axum::response::IntoResponse;
267        #[doc = concat!("# use spring_macros::", stringify!($job_type), ";")]
268        #[doc = concat!("#[", stringify!($job_type), "(", stringify!($example), ")]")]
269        /// async fn example() {
270        ///     println!("hello world");
271        /// }
272        /// ```
273        #[proc_macro_attribute]
274        pub fn $job_type(args: TokenStream, input: TokenStream) -> TokenStream {
275            job::with_job(job::JobType::$variant, args, input)
276        }
277    };
278}
279
280job_macro!(OneShot, one_shot, 60);
281job_macro!(FixDelay, fix_delay, 60);
282job_macro!(FixRate, fix_rate, 60);
283job_macro!(Cron, cron, "1/10 * * * * *");
284
285/// Auto config
286/// ```diff
287///  use spring_macros::auto_config;
288///  use spring_web::{WebPlugin, WebConfigurator};
289///  use spring_job::{JobPlugin, JobConfigurator};
290///  use spring_boot::app::App;
291/// +#[auto_config(WebConfigurator, JobConfigurator)]
292///  #[tokio::main]
293///  async fn main() {
294///      App::new()
295///         .add_plugin(WebPlugin)
296///         .add_plugin(JobPlugin)
297/// -       .add_router(router())
298/// -       .add_jobs(jobs())
299///         .run()
300///         .await
301///  }
302/// ```
303///
304#[proc_macro_attribute]
305pub fn auto_config(args: TokenStream, input: TokenStream) -> TokenStream {
306    auto::config(args, input)
307}
308
309/// stream macro
310#[proc_macro_attribute]
311pub fn stream_listener(args: TokenStream, input: TokenStream) -> TokenStream {
312    stream::listener(args, input)
313}
314
315/// Configurable
316#[proc_macro_derive(Configurable, attributes(config_prefix))]
317pub fn derive_config(input: TokenStream) -> TokenStream {
318    let input = syn::parse_macro_input!(input as DeriveInput);
319
320    config::expand_derive(input)
321        .unwrap_or_else(syn::Error::into_compile_error)
322        .into()
323}
324
325/// Injectable Servcie
326#[proc_macro_derive(Service, attributes(service, inject))]
327pub fn derive_service(input: TokenStream) -> TokenStream {
328    let input = syn::parse_macro_input!(input as DeriveInput);
329
330    inject::expand_derive(input)
331        .unwrap_or_else(syn::Error::into_compile_error)
332        .into()
333}
334
335/// ProblemDetails derive macro
336///
337/// Derives both `HttpStatusCode` and `ToProblemDetails` traits for error enums.
338/// This macro automatically generates implementations for converting error variants
339/// to HTTP status codes and RFC 7807 Problem Details responses.
340///
341/// Each variant must have a `#[status_code(code)]` attribute.
342/// 
343/// ## Supported Attributes
344/// 
345/// - `#[status_code(code)]` - **Required**: HTTP status code (e.g., 400, 404, 500)
346/// - `#[problem_type("uri")]` - **Optional**: Custom problem type URI
347/// - `#[title("title")]` - **Optional**: Custom problem title
348/// - `#[detail("detail")]` - **Optional**: Custom problem detail message
349/// - `#[instance("uri")]` - **Optional**: Problem instance URI
350///
351/// ## Title Compatibility
352/// 
353/// The `title` field can be automatically derived from the `#[error("...")]` attribute
354/// if no explicit `#[title("...")]` is provided. This provides compatibility with
355/// `thiserror::Error` and reduces duplication.
356///
357/// ## Basic Example
358/// ```rust,ignore
359/// use spring_web::ProblemDetails;
360///
361/// #[derive(ProblemDetails)]
362/// pub enum ApiError {
363///     #[status_code(400)]
364///     ValidationError,
365///     #[status_code(404)]
366///     NotFound,
367///     #[status_code(500)]
368///     InternalError,
369/// }
370/// ```
371///
372/// ## Advanced Example with Custom Attributes
373/// ```rust,ignore
374/// #[derive(ProblemDetails)]
375/// pub enum ApiError {
376///     // Explicit title
377///     #[status_code(400)]
378///     #[title("Input Validation Failed")]
379///     #[detail("The provided input data is invalid")]
380///     #[error("Validation error")]
381///     ValidationError,
382///     
383///     // Title derived from error attribute
384///     #[status_code(422)]
385///     #[detail("Request data failed validation")]
386///     #[error("Validation Failed")]  // This becomes the title
387///     ValidationFailed,
388///     
389///     // Full customization
390///     #[status_code(404)]
391///     #[problem_type("https://api.example.com/problems/not-found")]
392///     #[title("Resource Not Found")]
393///     #[detail("The requested resource could not be found")]
394///     #[instance("/users/123")]
395///     #[error("Not found")]
396///     NotFound,
397/// }
398/// ```
399///
400/// This will automatically implement:
401/// - `HttpStatusCode` trait for getting HTTP status codes
402/// - `ToProblemDetails` trait for converting to Problem Details responses
403/// - OpenAPI integration for documentation generation
404#[proc_macro_derive(ProblemDetails, attributes(status_code, problem_type, title, detail, instance))]
405pub fn derive_problem_details(input: TokenStream) -> TokenStream {
406    let input = syn::parse_macro_input!(input as DeriveInput);
407
408    problem_details::expand_derive(input)
409        .unwrap_or_else(syn::Error::into_compile_error)
410        .into()
411}
412
413/// `#[cache]` - Transparent Redis-based caching for async functions.
414///
415/// This macro wraps an async function to automatically cache its result
416/// in Redis. It checks for a cached value before executing the function.
417/// If a cached result is found, it is deserialized and returned directly.
418/// Otherwise, the function runs normally and its result is stored in Redis.
419///
420/// # Syntax
421/// ```plain
422/// #[cache("key_pattern", expire = <seconds>, condition = <bool_expr>, unless = <bool_expr>)]
423/// ```
424///
425/// # Attributes
426/// - `"key_pattern"` (**required**):
427///   A format string used to generate the cache key. Function arguments can be interpolated using standard `format!` syntax.
428/// - `expire = <integer>` (**optional**):
429///   The number of seconds before the cached value expires. If omitted, the key will be stored without expiration.
430/// - `condition = <expression>` (**optional**):
431///   A boolean expression evaluated **before** executing the function.
432///   If this evaluates to `false`, caching is completely bypassed — no lookup and no insertion.
433///   The expression can access function parameters directly.
434/// - `unless = <expression>` (**optional**):
435///   A boolean expression evaluated **after** executing the function.
436///   If this evaluates to `true`, the result will **not** be written to the cache.
437///   The expression can access both parameters and a `result` variable (the return value).
438///   NOTE: If your function returns Result<T, E>, the `result` variable in unless refers to the inner Ok value (T), not the entire Result.
439///   This allows you to write expressions like result.is_none() for Result<Option<_>, _> functions.
440///
441/// # Function Requirements
442/// - Must be an `async fn`
443/// - Can return either a `Result<T, E>` or a plain value `T`
444/// - The return type must implement `serde::Serialize` and `serde::Deserialize`
445/// - Generics, attributes, and visibility will be preserved
446///
447/// # Example
448/// ```rust
449/// use spring_macros::cache;
450///
451/// #[derive(serde::Serialize, serde::Deserialize)]
452/// struct User {
453///     id: u64,
454///     name: String,
455/// }
456///
457/// struct MyError;
458///
459/// #[cache("user:{user_id}", expire = 600, condition = user_id % 2 == 0, unless = result.is_none())]
460/// async fn get_user(user_id: u64) -> Result<Option<User>, MyError> {
461///     // Fetch user from database
462///     unimplemented!("do something")
463/// }
464/// ```
465#[proc_macro_attribute]
466pub fn cache(args: TokenStream, input: TokenStream) -> TokenStream {
467    cache::cache(args, input)
468}
469
470#[cfg(feature = "socket_io")]
471/// Marks a function as a SocketIO connection handler
472///
473/// # Examples
474/// ```
475/// # use spring_web::socketioxide::extract::{SocketRef, Data};
476/// # use spring_web::rmpv::Value;
477/// # use spring_macros::on_connection;
478/// #[on_connection]
479/// async fn on_connection(socket: SocketRef, Data(data): Data<Value>) {
480///     // Handle connection
481/// }
482/// ```
483#[proc_macro_attribute]
484pub fn on_connection(args: TokenStream, input: TokenStream) -> TokenStream {
485    socketioxide::on_connection(args, input)
486}
487
488#[cfg(feature = "socket_io")]
489/// Marks a function as a SocketIO disconnection handler
490///
491/// # Examples
492/// ```
493/// # use spring_web::socketioxide::extract::SocketRef;
494/// # use spring_macros::on_disconnect;
495/// #[on_disconnect]
496/// async fn on_disconnect(socket: SocketRef) {
497///     // Handle disconnection
498/// }
499/// ```
500#[proc_macro_attribute]
501pub fn on_disconnect(args: TokenStream, input: TokenStream) -> TokenStream {
502    socketioxide::on_disconnect(args, input)
503}
504
505#[cfg(feature = "socket_io")]
506/// Marks a function as a SocketIO message subscription handler
507///
508/// # Examples
509/// ```
510/// # use spring_web::socketioxide::extract::{SocketRef, Data};
511/// # use spring_macros::subscribe_message;
512/// # use spring_web::rmpv::Value;
513/// #[subscribe_message("message")]
514/// async fn message(socket: SocketRef, Data(data): Data<Value>) {
515///     // Handle message
516/// }
517/// ```
518#[proc_macro_attribute]
519pub fn subscribe_message(args: TokenStream, input: TokenStream) -> TokenStream {
520    socketioxide::subscribe_message(args, input)
521}
522
523#[cfg(feature = "socket_io")]
524/// Marks a function as a SocketIO fallback handler
525///
526/// # Examples
527/// ```
528/// # use spring_web::socketioxide::extract::{SocketRef, Data};
529/// # use spring_web::rmpv::Value;
530/// # use spring_macros::on_fallback;
531/// #[on_fallback]
532/// async fn on_fallback(socket: SocketRef, Data(data): Data<Value>) {
533///     // Handle fallback
534/// }
535/// ```
536#[proc_macro_attribute]
537pub fn on_fallback(args: TokenStream, input: TokenStream) -> TokenStream {
538    socketioxide::on_fallback(args, input)
539}
540
541// ============================================================================
542// Sa-Token authentication macros
543// ============================================================================
544
545#[cfg(feature = "sa-token")]
546/// Check login status
547///
548/// Returns 401 Unauthorized if user is not logged in.
549///
550/// # Example
551/// ```rust,ignore
552/// #[sa_check_login]
553/// async fn user_info() -> Result<impl IntoResponse> {
554///     Ok("User info")
555/// }
556/// ```
557#[proc_macro_attribute]
558pub fn sa_check_login(attr: TokenStream, input: TokenStream) -> TokenStream {
559    sa_token::sa_check_login_impl(attr, input)
560}
561
562#[cfg(feature = "sa-token")]
563/// Check user role
564///
565/// Returns 401 if not logged in, 403 Forbidden if user doesn't have the required role.
566///
567/// # Example
568/// ```rust,ignore
569/// #[sa_check_role("admin")]
570/// async fn admin_panel() -> Result<impl IntoResponse> {
571///     Ok("Admin panel")
572/// }
573/// ```
574#[proc_macro_attribute]
575pub fn sa_check_role(attr: TokenStream, input: TokenStream) -> TokenStream {
576    sa_token::sa_check_role_impl(attr, input)
577}
578
579#[cfg(feature = "sa-token")]
580/// Check user permission
581///
582/// Returns 401 if not logged in, 403 Forbidden if user doesn't have the required permission.
583///
584/// # Example
585/// ```rust,ignore
586/// #[sa_check_permission("user:delete")]
587/// async fn delete_user() -> Result<impl IntoResponse> {
588///     Ok("User deleted")
589/// }
590/// ```
591#[proc_macro_attribute]
592pub fn sa_check_permission(attr: TokenStream, input: TokenStream) -> TokenStream {
593    sa_token::sa_check_permission_impl(attr, input)
594}
595
596#[cfg(feature = "sa-token")]
597/// Check multiple roles with AND logic
598///
599/// User must have ALL specified roles to access.
600///
601/// # Example
602/// ```rust,ignore
603/// #[sa_check_roles_and("admin", "super")]
604/// async fn super_admin() -> Result<impl IntoResponse> {
605///     Ok("Super admin")
606/// }
607/// ```
608#[proc_macro_attribute]
609pub fn sa_check_roles_and(attr: TokenStream, input: TokenStream) -> TokenStream {
610    sa_token::sa_check_roles_and_impl(attr, input)
611}
612
613#[cfg(feature = "sa-token")]
614/// Check multiple roles with OR logic
615///
616/// User must have ANY of the specified roles to access.
617///
618/// # Example
619/// ```rust,ignore
620/// #[sa_check_roles_or("admin", "manager")]
621/// async fn management() -> Result<impl IntoResponse> {
622///     Ok("Management area")
623/// }
624/// ```
625#[proc_macro_attribute]
626pub fn sa_check_roles_or(attr: TokenStream, input: TokenStream) -> TokenStream {
627    sa_token::sa_check_roles_or_impl(attr, input)
628}
629
630#[cfg(feature = "sa-token")]
631/// Check multiple permissions with AND logic
632///
633/// User must have ALL specified permissions to access.
634///
635/// # Example
636/// ```rust,ignore
637/// #[sa_check_permissions_and("user:read", "user:write")]
638/// async fn user_rw() -> Result<impl IntoResponse> {
639///     Ok("User read/write")
640/// }
641/// ```
642#[proc_macro_attribute]
643pub fn sa_check_permissions_and(attr: TokenStream, input: TokenStream) -> TokenStream {
644    sa_token::sa_check_permissions_and_impl(attr, input)
645}
646
647#[cfg(feature = "sa-token")]
648/// Check multiple permissions with OR logic
649///
650/// User must have ANY of the specified permissions to access.
651///
652/// # Example
653/// ```rust,ignore
654/// #[sa_check_permissions_or("admin:*", "user:delete")]
655/// async fn delete() -> Result<impl IntoResponse> {
656///     Ok("Delete operation")
657/// }
658/// ```
659#[proc_macro_attribute]
660pub fn sa_check_permissions_or(attr: TokenStream, input: TokenStream) -> TokenStream {
661    sa_token::sa_check_permissions_or_impl(attr, input)
662}
663
664#[cfg(feature = "sa-token")]
665/// Ignore authentication for this endpoint
666///
667/// This macro marks an endpoint to skip authentication checks,
668/// even if it's under a path that normally requires authentication.
669///
670/// # Example
671/// ```rust,ignore
672/// #[sa_ignore]
673/// async fn public_endpoint() -> impl IntoResponse {
674///     "This endpoint is public"
675/// }
676/// ```
677#[proc_macro_attribute]
678pub fn sa_ignore(attr: TokenStream, input: TokenStream) -> TokenStream {
679    sa_token::sa_ignore_impl(attr, input)
680}