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