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