spring_macros/lib.rs
1//! [](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}