Skip to main content

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/// #[handler(METHOD, "/path", middleware = [mw_fn, factory("arg")])]
23/// ```
24///
25/// Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
26///
27/// Path parameters expressed as `{name}` in the route string are automatically
28/// extracted. Declare a matching function parameter by name and the macro rewrites
29/// the signature to use `axum::extract::Path` under the hood. Undeclared path
30/// params are captured but silently ignored (partial extraction).
31///
32/// Handler-level middleware is attached with `#[middleware(...)]` on the
33/// function, separate from the route registration attribute.
34#[proc_macro_attribute]
35pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
36    handler::expand(attr.into(), item.into())
37        .unwrap_or_else(|e| e.to_compile_error())
38        .into()
39}
40
41/// Generates the application entry point from an async `main` function.
42///
43/// The decorated function must be named `main`, be `async`, and accept exactly
44/// two parameters: an `AppBuilder` and a config type that implements
45/// `modo::config::FromEnv` (or `Default`).
46///
47/// The macro wraps the body in a multi-threaded Tokio runtime, initialises a
48/// `tracing_subscriber` with an `RUST_LOG` environment filter, loads the config
49/// via `modo::config::load_or_default`, and exits with code 1 on error.
50///
51/// # Optional attribute
52///
53/// `static_assets = "path/"` — embeds the given folder as static files using
54/// `rust_embed`. Requires the `static-embed` feature on `modo-macros`.
55#[proc_macro_attribute]
56pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
57    main_macro::expand(attr.into(), item.into())
58        .unwrap_or_else(|e| e.to_compile_error())
59        .into()
60}
61
62/// Groups handlers under a shared URL prefix and optional middleware.
63///
64/// # Syntax
65///
66/// ```text
67/// #[module(prefix = "/api/v1")]
68/// #[module(prefix = "/api/v1", middleware = [auth_required, require_role("admin")])]
69/// mod my_module { ... }
70/// ```
71///
72/// All `#[handler]` attributes inside the module are automatically rewritten to
73/// include `module = "module_name"` so they are grouped correctly at startup.
74/// The module is registered via `inventory` and collected by `AppBuilder`.
75#[proc_macro_attribute]
76pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
77    module::expand(attr.into(), item.into())
78        .unwrap_or_else(|e| e.to_compile_error())
79        .into()
80}
81
82/// Registers a sync function as the application-wide custom error handler.
83///
84/// The function must be sync (not `async`) and must have exactly two
85/// parameters: `(modo::Error, &modo::ErrorContext)`. It must return
86/// `axum::response::Response`.
87///
88/// Only one error handler may be registered per binary. The handler receives
89/// every `modo::Error` that propagates out of a route and can inspect the
90/// request context (method, URI, headers) to produce a suitable response.
91#[proc_macro_attribute]
92pub fn error_handler(attr: TokenStream, item: TokenStream) -> TokenStream {
93    error_handler::expand(attr.into(), item.into())
94        .unwrap_or_else(|e| e.to_compile_error())
95        .into()
96}
97
98/// Derives the `modo::sanitize::Sanitize` trait for a named-field struct.
99///
100/// Annotate fields with `#[clean(...)]` to apply one or more sanitization rules
101/// in order. Available rules:
102///
103/// - `trim` — strip leading and trailing whitespace
104/// - `lowercase` / `uppercase` — convert ASCII case
105/// - `strip_html_tags` — remove HTML tags
106/// - `collapse_whitespace` — replace runs of whitespace with a single space
107/// - `truncate = N` — keep at most `N` characters
108/// - `normalize_email` — lowercase and trim an email address
109/// - `custom = "path::to::fn"` — call a `fn(String) -> String` function
110///
111/// Fields of type `Option<String>` are sanitized only when `Some`.
112/// Fields with no `#[clean]` attribute are left untouched.
113///
114/// The macro also registers a `SanitizerRegistration` entry via `inventory`
115/// so extractors can invoke `Sanitize::sanitize` automatically.
116#[proc_macro_derive(Sanitize, attributes(clean))]
117pub fn derive_sanitize(input: TokenStream) -> TokenStream {
118    sanitize::expand(input.into())
119        .unwrap_or_else(|e| e.to_compile_error())
120        .into()
121}
122
123/// Derives the `modo::validate::Validate` trait for a named-field struct.
124///
125/// Annotate fields with `#[validate(...)]` to declare one or more rules.
126/// Available rules:
127///
128/// - `required` — field must not be `None` (for `Option`) or empty (for `String`)
129/// - `min_length = N` / `max_length = N` — minimum/maximum character count for strings
130/// - `email` — basic email format check
131/// - `min = V` / `max = V` — numeric range for comparable types
132/// - `custom = "path::to::fn"` — call a `fn(&T) -> Result<(), String>` function
133///
134/// Each rule accepts an optional `(message = "...")` override. A field-level
135/// `message = "..."` key acts as a fallback for all rules on that field.
136///
137/// `validate()` returns `Ok(())` or `Err(modo::Error)` containing all
138/// collected error messages keyed by field name.
139#[proc_macro_derive(Validate, attributes(validate))]
140pub fn derive_validate(input: TokenStream) -> TokenStream {
141    validate::expand(input.into())
142        .unwrap_or_else(|e| e.to_compile_error())
143        .into()
144}
145
146/// Translates a localisation key using the i18n runtime.
147///
148/// # Syntax
149///
150/// ```text
151/// t!(i18n, "key")
152/// t!(i18n, "key", name = expr, count = expr)
153/// ```
154///
155/// The first argument is an expression that resolves to the i18n context
156/// (typically `&i18n` extracted from a handler parameter). The second argument
157/// is a string literal key. Additional `name = value` pairs are substituted
158/// into the translation string.
159///
160/// When a `count` variable is present the macro calls `t_plural` instead of
161/// `t` to select the correct plural form.
162///
163/// Requires the `i18n` feature on `modo`.
164#[proc_macro]
165pub fn t(input: TokenStream) -> TokenStream {
166    t_macro::expand(input.into())
167        .unwrap_or_else(|e| e.to_compile_error())
168        .into()
169}
170
171/// Derives `IntoResponse` and `ViewRender` for a struct, linking it to a
172/// MiniJinja template.
173///
174/// # Syntax
175///
176/// ```text
177/// #[view("templates/page.html")]
178/// #[view("templates/page.html", htmx = "templates/partial.html")]
179/// ```
180///
181/// The macro derives `serde::Serialize` on the struct and implements
182/// `axum::response::IntoResponse` by serializing the struct as the template
183/// context and rendering the template. When the optional `htmx` template path
184/// is provided, HTMX requests render the partial instead of the full page.
185///
186/// Requires the `templates` feature on `modo`.
187#[proc_macro_attribute]
188pub fn view(attr: TokenStream, item: TokenStream) -> TokenStream {
189    view::expand(attr.into(), item.into())
190        .unwrap_or_else(|e| e.to_compile_error())
191        .into()
192}
193
194/// Registers a function as a named MiniJinja template function.
195///
196/// # Syntax
197///
198/// ```text
199/// #[template_function]               // uses the Rust function name
200/// #[template_function(name = "fn_name")]  // explicit template name
201/// ```
202///
203/// The function is submitted via `inventory` and registered into the
204/// MiniJinja environment when the `TemplateEngine` service starts.
205///
206/// Requires the `templates` feature on `modo`.
207#[proc_macro_attribute]
208pub fn template_function(attr: TokenStream, item: TokenStream) -> TokenStream {
209    template_function::expand(attr.into(), item.into())
210        .unwrap_or_else(|e| e.to_compile_error())
211        .into()
212}
213
214/// Registers a function as a named MiniJinja template filter.
215///
216/// # Syntax
217///
218/// ```text
219/// #[template_filter]               // uses the Rust function name
220/// #[template_filter(name = "filter_name")]  // explicit filter name
221/// ```
222///
223/// The function is submitted via `inventory` and registered into the
224/// MiniJinja environment when the `TemplateEngine` service starts.
225///
226/// Requires the `templates` feature on `modo`.
227#[proc_macro_attribute]
228pub fn template_filter(attr: TokenStream, item: TokenStream) -> TokenStream {
229    template_filter::expand(attr.into(), item.into())
230        .unwrap_or_else(|e| e.to_compile_error())
231        .into()
232}