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 inject;
9mod job;
10mod middlewares;
11mod nest;
12mod route;
13mod stream;
14
15use proc_macro::TokenStream;
16use syn::DeriveInput;
17
18/// Creates resource handler, allowing multiple HTTP method guards.
19///
20/// # Syntax
21/// ```plain
22/// #[route("path", method="HTTP_METHOD"[, attributes])]
23/// ```
24///
25/// # Attributes
26/// - `"path"`: Raw literal string with path for which to register handler.
27/// - `method = "HTTP_METHOD"`: Registers HTTP method to provide guard for. Upper-case string,
28///   "GET", "POST" for example.
29///
30/// # Examples
31/// ```
32/// # use spring_web::axum::response::IntoResponse;
33/// # use spring_macros::route;
34/// #[route("/test", method = "GET", method = "HEAD")]
35/// async fn example() -> impl IntoResponse {
36///     "hello world"
37/// }
38/// ```
39#[proc_macro_attribute]
40pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
41    route::with_method(None, args, input)
42}
43
44/// Creates resource handler, allowing multiple HTTP methods and paths.
45///
46/// # Syntax
47/// ```plain
48/// #[routes]
49/// #[<method>("path", ...)]
50/// #[<method>("path", ...)]
51/// ...
52/// ```
53///
54/// # Attributes
55/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
56/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
57///
58/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
59///
60/// # Examples
61/// ```
62/// # use spring_web::axum::response::IntoResponse;
63/// # use spring_macros::routes;
64/// #[routes]
65/// #[get("/test")]
66/// #[get("/test2")]
67/// #[delete("/test")]
68/// async fn example() -> impl IntoResponse {
69///     "hello world"
70/// }
71/// ```
72#[proc_macro_attribute]
73pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
74    route::with_methods(input)
75}
76
77macro_rules! method_macro {
78    ($variant:ident, $method:ident) => {
79        ///
80        /// # Syntax
81        /// ```plain
82        #[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)]
83        /// ```
84        ///
85        /// # Attributes
86        /// - `"path"`: Raw literal string with path for which to register handler.
87        ///
88        /// # Examples
89        /// ```
90        /// # use spring_web::axum::response::IntoResponse;
91        #[doc = concat!("# use spring_macros::", stringify!($method), ";")]
92        #[doc = concat!("#[", stringify!($method), r#"("/")]"#)]
93        /// async fn example() -> impl IntoResponse {
94        ///     "hello world"
95        /// }
96        /// ```
97        #[proc_macro_attribute]
98        pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
99            route::with_method(Some(route::Method::$variant), args, input)
100        }
101    };
102}
103
104method_macro!(Get, get);
105method_macro!(Post, post);
106method_macro!(Put, put);
107method_macro!(Delete, delete);
108method_macro!(Head, head);
109method_macro!(Options, options);
110method_macro!(Trace, trace);
111method_macro!(Patch, patch);
112
113/// Prepends a path prefix to all handlers using routing macros inside the attached module.
114///
115/// # Syntax
116///
117/// ```
118/// # use spring_macros::nest;
119/// #[nest("/prefix")]
120/// mod api {
121///     // ...
122/// }
123/// ```
124///
125/// # Arguments
126///
127/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
128///
129/// # Example
130///
131/// ```
132/// # use spring_macros::{nest, get};
133/// # use spring_web::axum::response::IntoResponse;
134/// #[nest("/api")]
135/// mod api {
136///     # use super::*;
137///     #[get("/hello")]
138///     pub async fn hello() -> impl IntoResponse {
139///         // this has path /api/hello
140///         "Hello, world!"
141///     }
142/// }
143/// # fn main() {}
144/// ```
145#[proc_macro_attribute]
146pub fn nest(args: TokenStream, input: TokenStream) -> TokenStream {
147    nest::with_nest(args, input)
148}
149
150/// Applies middleware layers to all route handlers within a module.
151///
152/// # Syntax
153/// ```plain
154/// #[middlewares(middleware1, middleware2, ...)]
155/// mod module_name {
156///     // route handlers
157/// }
158/// ```
159///
160/// # Arguments
161/// - `middleware1`, `middleware2`, etc. - Middleware expressions that will be applied to all routes in the module
162///
163/// This macro generates a router function that applies the specified middleware
164/// to all route handlers defined within the module.
165#[proc_macro_attribute]
166pub fn middlewares(args: TokenStream, input: TokenStream) -> TokenStream {
167    middlewares::middlewares(args, input)
168}
169
170fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
171    let compile_err = TokenStream::from(err.to_compile_error());
172    item.extend(compile_err);
173    item
174}
175
176/// Job
177///
178macro_rules! job_macro {
179    ($variant:ident, $job_type:ident, $example:literal) => {
180        ///
181        /// # Syntax
182        /// ```plain
183        #[doc = concat!("#[", stringify!($job_type), "(", $example, ")]")]
184        /// ```
185        ///
186        /// # Attributes
187        /// - `"path"`: Raw literal string with path for which to register handler.
188        ///
189        /// # Examples
190        /// ```
191        /// # use spring_web::axum::response::IntoResponse;
192        #[doc = concat!("# use spring_macros::", stringify!($job_type), ";")]
193        #[doc = concat!("#[", stringify!($job_type), "(", stringify!($example), ")]")]
194        /// async fn example() {
195        ///     println!("hello world");
196        /// }
197        /// ```
198        #[proc_macro_attribute]
199        pub fn $job_type(args: TokenStream, input: TokenStream) -> TokenStream {
200            job::with_job(job::JobType::$variant, args, input)
201        }
202    };
203}
204
205job_macro!(OneShot, one_shot, 60);
206job_macro!(FixDelay, fix_delay, 60);
207job_macro!(FixRate, fix_rate, 60);
208job_macro!(Cron, cron, "1/10 * * * * *");
209
210/// Auto config
211/// ```diff
212///  use spring_macros::auto_config;
213///  use spring_web::{WebPlugin, WebConfigurator};
214///  use spring_job::{JobPlugin, JobConfigurator};
215///  use spring_boot::app::App;
216/// +#[auto_config(WebConfigurator, JobConfigurator)]
217///  #[tokio::main]
218///  async fn main() {
219///      App::new()
220///         .add_plugin(WebPlugin)
221///         .add_plugin(JobPlugin)
222/// -       .add_router(router())
223/// -       .add_jobs(jobs())
224///         .run()
225///         .await
226///  }
227/// ```
228///
229#[proc_macro_attribute]
230pub fn auto_config(args: TokenStream, input: TokenStream) -> TokenStream {
231    auto::config(args, input)
232}
233
234/// stream macro
235#[proc_macro_attribute]
236pub fn stream_listener(args: TokenStream, input: TokenStream) -> TokenStream {
237    stream::listener(args, input)
238}
239
240/// Configurable
241#[proc_macro_derive(Configurable, attributes(config_prefix))]
242pub fn derive_config(input: TokenStream) -> TokenStream {
243    let input = syn::parse_macro_input!(input as DeriveInput);
244
245    config::expand_derive(input)
246        .unwrap_or_else(syn::Error::into_compile_error)
247        .into()
248}
249
250/// Injectable Servcie
251#[proc_macro_derive(Service, attributes(service, inject))]
252pub fn derive_service(input: TokenStream) -> TokenStream {
253    let input = syn::parse_macro_input!(input as DeriveInput);
254
255    inject::expand_derive(input)
256        .unwrap_or_else(syn::Error::into_compile_error)
257        .into()
258}
259
260/// `#[cache]` - Transparent Redis-based caching for async functions.
261///
262/// This macro wraps an async function to automatically cache its result
263/// in Redis. It checks for a cached value before executing the function.
264/// If a cached result is found, it is deserialized and returned directly.
265/// Otherwise, the function runs normally and its result is stored in Redis.
266///
267/// # Syntax
268/// ```plain
269/// #[cache("key_pattern", expire = <seconds>, condition = <bool_expr>, unless = <bool_expr>)]
270/// ```
271///
272/// # Attributes
273/// - `"key_pattern"` (**required**):
274///   A format string used to generate the cache key. Function arguments can be interpolated using standard `format!` syntax.
275/// - `expire = <integer>` (**optional**):
276///   The number of seconds before the cached value expires. If omitted, the key will be stored without expiration.
277/// - `condition = <expression>` (**optional**):
278///   A boolean expression evaluated **before** executing the function.
279///   If this evaluates to `false`, caching is completely bypassed — no lookup and no insertion.
280///   The expression can access function parameters directly.
281/// - `unless = <expression>` (**optional**):
282///   A boolean expression evaluated **after** executing the function.
283///   If this evaluates to `true`, the result will **not** be written to the cache.
284///   The expression can access both parameters and a `result` variable (the return value).
285///   NOTE: If your function returns Result<T, E>, the `result` variable in unless refers to the inner Ok value (T), not the entire Result.
286///   This allows you to write expressions like result.is_none() for Result<Option<_>, _> functions.
287///
288/// # Function Requirements
289/// - Must be an `async fn`
290/// - Can return either a `Result<T, E>` or a plain value `T`
291/// - The return type must implement `serde::Serialize` and `serde::Deserialize`
292/// - Generics, attributes, and visibility will be preserved
293///
294/// # Example
295/// ```rust
296/// use spring_macros::cache;
297///
298/// #[derive(serde::Serialize, serde::Deserialize)]
299/// struct User {
300///     id: u64,
301///     name: String,
302/// }
303///
304/// struct MyError;
305///
306/// #[cache("user:{user_id}", expire = 600, condition = user_id % 2 == 0, unless = result.is_none())]
307/// async fn get_user(user_id: u64) -> Result<Option<User>, MyError> {
308///     // Fetch user from database
309///     unimplemented!("do something")
310/// }
311/// ```
312#[proc_macro_attribute]
313pub fn cache(args: TokenStream, input: TokenStream) -> TokenStream {
314    cache::cache(args, input)
315}