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
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}