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/// ```
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#[proc_macro_attribute]
90pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
91    module::expand(attr.into(), item.into())
92        .unwrap_or_else(|e| e.to_compile_error())
93        .into()
94}
95
96/// Registers a sync function as the application-wide custom error handler.
97///
98/// The function must be sync (not `async`) and must have exactly two
99/// parameters: `(modo::Error, &modo::ErrorContext)`. It must return
100/// `axum::response::Response`.
101///
102/// Only one error handler may be registered per binary. The handler receives
103/// every `modo::Error` that propagates out of a route and can inspect the
104/// request context (method, URI, headers) to produce a suitable response.
105/// Call `err.default_response()` to delegate back to the built-in JSON rendering.
106#[proc_macro_attribute]
107pub fn error_handler(attr: TokenStream, item: TokenStream) -> TokenStream {
108    error_handler::expand(attr.into(), item.into())
109        .unwrap_or_else(|e| e.to_compile_error())
110        .into()
111}
112
113/// Derives the `modo::sanitize::Sanitize` trait for a named-field struct.
114///
115/// Annotate fields with `#[clean(...)]` to apply one or more sanitization rules
116/// in order. Available rules:
117///
118/// - `trim` — strip leading and trailing whitespace
119/// - `lowercase` / `uppercase` — convert ASCII case
120/// - `strip_html_tags` — remove HTML tags
121/// - `collapse_whitespace` — replace runs of whitespace with a single space
122/// - `truncate = N` — keep at most `N` characters
123/// - `normalize_email` — lowercase and trim an email address
124/// - `custom = "path::to::fn"` — call a `fn(String) -> String` function
125///
126/// Fields of type `Option<String>` are sanitized only when `Some`.
127/// Fields with no `#[clean]` attribute are left untouched.
128///
129/// The macro also registers a `SanitizerRegistration` entry via `inventory`
130/// so extractors (`JsonReq`, `FormReq`) can invoke `Sanitize::sanitize` automatically.
131#[proc_macro_derive(Sanitize, attributes(clean))]
132pub fn derive_sanitize(input: TokenStream) -> TokenStream {
133    sanitize::expand(input.into())
134        .unwrap_or_else(|e| e.to_compile_error())
135        .into()
136}
137
138/// Derives the `modo::validate::Validate` trait for a named-field struct.
139///
140/// Annotate fields with `#[validate(...)]` to declare one or more rules.
141/// Available rules:
142///
143/// - `required` — field must not be `None` (for `Option`) or empty (for `String`)
144/// - `min_length = N` / `max_length = N` — minimum/maximum character count for strings
145/// - `email` — basic email format check
146/// - `min = V` / `max = V` — numeric range for comparable types
147/// - `custom = "path::to::fn"` — call a `fn(&T) -> Result<(), String>` function
148///
149/// Each rule accepts an optional `(message = "...")` override. A field-level
150/// `message = "..."` key acts as a fallback for all rules on that field.
151///
152/// The generated `validate()` method returns `Ok(())` or `Err(modo::Error)`
153/// containing all collected error messages keyed by field name.
154#[proc_macro_derive(Validate, attributes(validate))]
155pub fn derive_validate(input: TokenStream) -> TokenStream {
156    validate::expand(input.into())
157        .unwrap_or_else(|e| e.to_compile_error())
158        .into()
159}
160
161/// Translates a localisation key using the i18n runtime.
162///
163/// # Syntax
164///
165/// ```text
166/// t!(i18n, "key")
167/// t!(i18n, "key", name = expr, count = expr)
168/// ```
169///
170/// The first argument is an expression that resolves to the i18n context
171/// (typically an `I18n` value extracted from a handler parameter). The second
172/// argument is a string literal key. Additional `name = value` pairs are
173/// substituted into the translation string.
174///
175/// When a `count` variable is present the macro calls `.t_plural` instead of
176/// `.t` to select the correct plural form.
177///
178/// Requires the `i18n` feature on `modo`.
179#[proc_macro]
180pub fn t(input: TokenStream) -> TokenStream {
181    t_macro::expand(input.into())
182        .unwrap_or_else(|e| e.to_compile_error())
183        .into()
184}
185
186/// Adds `serde::Serialize`, `axum::response::IntoResponse`, and `ViewRender`
187/// implementations to a struct, linking it to a MiniJinja template.
188///
189/// # Syntax
190///
191/// ```text
192/// #[view("templates/page.html")]
193/// #[view("templates/page.html", htmx = "templates/partial.html")]
194/// ```
195///
196/// The macro derives `serde::Serialize` on the struct and implements
197/// `axum::response::IntoResponse` by serializing the struct as the template
198/// context and rendering the named template. When the optional `htmx` path is
199/// provided, HTMX requests (`HX-Request` header present) render the partial
200/// instead of the full-page template.
201///
202/// Requires the `templates` feature on `modo`.
203#[proc_macro_attribute]
204pub fn view(attr: TokenStream, item: TokenStream) -> TokenStream {
205    view::expand(attr.into(), item.into())
206        .unwrap_or_else(|e| e.to_compile_error())
207        .into()
208}
209
210/// Registers a function as a named MiniJinja global function.
211///
212/// # Syntax
213///
214/// ```text
215/// #[template_function]                    // uses the Rust function name
216/// #[template_function(name = "fn_name")] // explicit template name
217/// ```
218///
219/// The function is submitted via `inventory` and registered into the
220/// MiniJinja environment when the `TemplateEngine` service starts.
221/// The `inventory::submit!` call is guarded by `#[cfg(feature = "templates")]`
222/// in the generated code, so the function definition is always compiled but
223/// the registration only takes effect when that feature is enabled on `modo`.
224///
225/// Requires the `templates` feature on `modo`.
226#[proc_macro_attribute]
227pub fn template_function(attr: TokenStream, item: TokenStream) -> TokenStream {
228    template_function::expand(attr.into(), item.into())
229        .unwrap_or_else(|e| e.to_compile_error())
230        .into()
231}
232
233/// Registers a function as a named MiniJinja template filter.
234///
235/// # Syntax
236///
237/// ```text
238/// #[template_filter]                       // uses the Rust function name
239/// #[template_filter(name = "filter_name")] // explicit filter name
240/// ```
241///
242/// The function is submitted via `inventory` and registered into the
243/// MiniJinja environment when the `TemplateEngine` service starts.
244/// The `inventory::submit!` call is guarded by `#[cfg(feature = "templates")]`
245/// in the generated code, so the function definition is always compiled but
246/// the registration only takes effect when that feature is enabled on `modo`.
247///
248/// Requires the `templates` feature on `modo`.
249#[proc_macro_attribute]
250pub fn template_filter(attr: TokenStream, item: TokenStream) -> TokenStream {
251    template_filter::expand(attr.into(), item.into())
252        .unwrap_or_else(|e| e.to_compile_error())
253        .into()
254}