modo_macros/lib.rs
1use proc_macro::TokenStream;
2
3mod error_handler;
4mod handler;
5mod main_macro;
6mod middleware;
7mod module;
8mod sanitize;
9mod t_macro;
10mod template_filter;
11mod template_function;
12mod utils;
13mod validate;
14mod view;
15
16/// Registers an async function as an HTTP route handler.
17///
18/// # Syntax
19///
20/// ```text
21/// #[handler(METHOD, "/path")]
22/// ```
23///
24/// Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
25///
26/// Path parameters expressed as `{name}` in the route string are automatically
27/// extracted. Declare a matching function parameter by name and the macro rewrites
28/// the signature to use `axum::extract::Path` under the hood. Undeclared path
29/// params are captured but silently ignored (partial extraction).
30///
31/// Handler-level middleware is attached with a separate `#[middleware(...)]`
32/// attribute on the function. Bare paths are wrapped with
33/// `axum::middleware::from_fn`; paths followed by `(args)` are called as layer
34/// factories.
35///
36/// ```text
37/// #[handler(GET, "/items/{id}")]
38/// #[middleware(require_auth, require_role("admin"))]
39/// async fn get_item(id: String) -> modo::JsonResult<Item> { ... }
40/// ```
41#[proc_macro_attribute]
42pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
43 handler::expand(attr.into(), item.into())
44 .unwrap_or_else(|e| e.to_compile_error())
45 .into()
46}
47
48/// Generates the application entry point from an async `main` function.
49///
50/// The decorated function must be named `main`, be `async`, and accept exactly
51/// two parameters: an `AppBuilder` and a config type that implements
52/// `serde::de::DeserializeOwned + Default`.
53///
54/// The macro replaces the function with a sync `fn main()` that:
55/// - builds a multi-threaded Tokio runtime,
56/// - initialises `tracing_subscriber` using `RUST_LOG`, falling back to
57/// `"info,sqlx::query=warn"` when the environment variable is unset,
58/// - loads the config via `modo::config::load_or_default`,
59/// - runs the async body, and
60/// - exits with code 1 if an error is returned.
61///
62/// The return type annotation on the `async fn main` is not enforced by the
63/// macro; the body is wrapped in an internal `Result<(), Box<dyn std::error::Error>>`.
64///
65/// # Optional attribute
66///
67/// `static_assets = "path/"` — embeds the given folder as static files using
68/// `rust_embed`. Requires the `static-embed` feature on `modo-macros`.
69#[proc_macro_attribute]
70pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
71 main_macro::expand(attr.into(), item.into())
72 .unwrap_or_else(|e| e.to_compile_error())
73 .into()
74}
75
76/// Groups handlers under a shared URL prefix and optional middleware.
77///
78/// # Syntax
79///
80/// ```text
81/// #[module(prefix = "/api/v1")]
82/// #[module(prefix = "/api/v1", middleware = [auth_required, require_role("admin")])]
83/// mod my_module { ... }
84/// ```
85///
86/// All `#[handler]` attributes inside the module are automatically rewritten to
87/// include the module association so they are grouped correctly at startup.
88/// The module is registered via `inventory` and collected by `AppBuilder`.
89///
90/// Bare `mod foo;` declarations inside the module body are allowed. Inline
91/// nested `mod foo { ... }` blocks are not supported and will produce a compile
92/// error, because their handlers would not receive the outer module's prefix.
93#[proc_macro_attribute]
94pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
95 module::expand(attr.into(), item.into())
96 .unwrap_or_else(|e| e.to_compile_error())
97 .into()
98}
99
100/// Registers a sync function as the application-wide custom error handler.
101///
102/// The function must be sync (not `async`) and must have exactly two
103/// parameters: `(modo::Error, &modo::ErrorContext)`. It must return
104/// `axum::response::Response`.
105///
106/// Only one error handler may be registered per binary. The handler receives
107/// every `modo::Error` that propagates out of a route and can inspect the
108/// request context (method, URI, headers) to produce a suitable response.
109/// Call `err.default_response()` to delegate back to the built-in JSON rendering.
110///
111/// This attribute takes no arguments.
112#[proc_macro_attribute]
113pub fn error_handler(attr: TokenStream, item: TokenStream) -> TokenStream {
114 error_handler::expand(attr.into(), item.into())
115 .unwrap_or_else(|e| e.to_compile_error())
116 .into()
117}
118
119/// Derives the `modo::sanitize::Sanitize` trait for a named-field struct.
120///
121/// Annotate fields with `#[clean(...)]` to apply one or more sanitization rules
122/// in order. Available rules:
123///
124/// - `trim` — strip leading and trailing whitespace
125/// - `lowercase` / `uppercase` — convert ASCII case
126/// - `strip_html_tags` — remove HTML tags
127/// - `collapse_whitespace` — replace runs of whitespace with a single space
128/// - `truncate = N` — keep at most `N` characters
129/// - `normalize_email` — lowercase and trim an email address
130/// - `custom = "path::to::fn"` — call a `fn(String) -> String` function
131///
132/// Fields of type `Option<String>` are sanitized only when `Some`.
133/// Fields with no `#[clean]` attribute are left untouched.
134///
135/// The macro also registers a `SanitizerRegistration` entry via `inventory`
136/// so extractors (`JsonReq`, `FormReq`) can invoke `Sanitize::sanitize` automatically.
137///
138/// Generic structs are not supported; the derive will produce a compile error.
139#[proc_macro_derive(Sanitize, attributes(clean))]
140pub fn derive_sanitize(input: TokenStream) -> TokenStream {
141 sanitize::expand(input.into())
142 .unwrap_or_else(|e| e.to_compile_error())
143 .into()
144}
145
146/// Derives the `modo::validate::Validate` trait for a named-field struct.
147///
148/// Annotate fields with `#[validate(...)]` to declare one or more rules.
149/// Available rules:
150///
151/// - `required` — field must not be `None` (for `Option`) or empty (for `String`)
152/// - `min_length = N` / `max_length = N` — minimum/maximum character count for strings
153/// - `email` — basic email format check
154/// - `min = V` / `max = V` — numeric range for comparable types
155/// - `custom = "path::to::fn"` — call a `fn(&T) -> Result<(), String>` function
156///
157/// Each rule accepts an optional `(message = "...")` override. A field-level
158/// `message = "..."` key acts as a fallback for all rules on that field.
159///
160/// The generated `validate()` method returns `Ok(())` or `Err(modo::Error)`
161/// containing all collected error messages keyed by field name.
162#[proc_macro_derive(Validate, attributes(validate))]
163pub fn derive_validate(input: TokenStream) -> TokenStream {
164 validate::expand(input.into())
165 .unwrap_or_else(|e| e.to_compile_error())
166 .into()
167}
168
169/// Translates a localisation key using the i18n runtime.
170///
171/// # Syntax
172///
173/// ```text
174/// t!(i18n, "key")
175/// t!(i18n, "key", name = expr, count = expr)
176/// ```
177///
178/// The first argument is an expression that resolves to the i18n context
179/// (typically an `I18n` value extracted from a handler parameter). The second
180/// argument is a string literal key. Additional `name = value` pairs are
181/// substituted into the translation string.
182///
183/// When a `count` variable is present the macro calls `.t_plural` instead of
184/// `.t` to select the correct plural form.
185///
186/// Requires the `i18n` feature on `modo`.
187#[proc_macro]
188pub fn t(input: TokenStream) -> TokenStream {
189 t_macro::expand(input.into())
190 .unwrap_or_else(|e| e.to_compile_error())
191 .into()
192}
193
194/// Adds `serde::Serialize`, `axum::response::IntoResponse`, and `ViewRender`
195/// implementations to a struct, linking it to a MiniJinja template.
196///
197/// # Syntax
198///
199/// ```text
200/// #[view("templates/page.html")]
201/// #[view("templates/page.html", htmx = "templates/partial.html")]
202/// ```
203///
204/// The macro derives `serde::Serialize` on the struct and implements
205/// `axum::response::IntoResponse` by serializing the struct as the template
206/// context and rendering the named template. When the optional `htmx` path is
207/// provided, HTMX requests (`HX-Request` header present) render the partial
208/// instead of the full-page template.
209///
210/// Can only be applied to structs.
211///
212/// Requires the `templates` feature on `modo`.
213#[proc_macro_attribute]
214pub fn view(attr: TokenStream, item: TokenStream) -> TokenStream {
215 view::expand(attr.into(), item.into())
216 .unwrap_or_else(|e| e.to_compile_error())
217 .into()
218}
219
220/// Registers a function as a named MiniJinja global function.
221///
222/// # Syntax
223///
224/// ```text
225/// #[template_function] // uses the Rust function name
226/// #[template_function(name = "fn_name")] // explicit template name
227/// ```
228///
229/// The function is submitted via `inventory` and registered into the
230/// MiniJinja environment when the `TemplateEngine` service starts.
231/// The `inventory::submit!` call is guarded by `#[cfg(feature = "templates")]`
232/// in the generated code, so the function definition is always compiled but
233/// the registration only takes effect when that feature is enabled on `modo`.
234///
235/// Requires the `templates` feature on `modo`.
236#[proc_macro_attribute]
237pub fn template_function(attr: TokenStream, item: TokenStream) -> TokenStream {
238 template_function::expand(attr.into(), item.into())
239 .unwrap_or_else(|e| e.to_compile_error())
240 .into()
241}
242
243/// Registers a function as a named MiniJinja template filter.
244///
245/// # Syntax
246///
247/// ```text
248/// #[template_filter] // uses the Rust function name
249/// #[template_filter(name = "filter_name")] // explicit filter name
250/// ```
251///
252/// The function is submitted via `inventory` and registered into the
253/// MiniJinja environment when the `TemplateEngine` service starts.
254/// The `inventory::submit!` call is guarded by `#[cfg(feature = "templates")]`
255/// in the generated code, so the function definition is always compiled but
256/// the registration only takes effect when that feature is enabled on `modo`.
257///
258/// Requires the `templates` feature on `modo`.
259#[proc_macro_attribute]
260pub fn template_filter(attr: TokenStream, item: TokenStream) -> TokenStream {
261 template_filter::expand(attr.into(), item.into())
262 .unwrap_or_else(|e| e.to_compile_error())
263 .into()
264}