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