# modo-macros
Procedural macros for the modo web framework. Provides attribute macros for
route registration, application bootstrap, and derive macros for input
validation and sanitization.
All macros are re-exported from `modo` — import them as `modo::handler`,
`modo::main`, etc. Do not depend on `modo-macros` directly in application code.
## Features
| `static-embed` | `#[main(static_assets = "...")]` static file embedding via `rust-embed` |
Template and i18n macros (`#[view]`, `#[template_function]`, `#[template_filter]`, `t!`)
are re-exported from `modo` only when the corresponding `templates` or `i18n`
feature is enabled on the `modo` crate.
## Usage
### Application entry point
```rust
#[modo::main]
async fn main(
app: modo::app::AppBuilder,
config: modo::config::AppConfig,
) -> Result<(), Box<dyn std::error::Error>> {
app.config(config).run().await
}
```
The function must be named `main`, be `async`, and accept exactly two
parameters: an `AppBuilder` and a config type. The macro bootstraps a
multi-threaded Tokio runtime, configures `tracing_subscriber`, loads config
via environment variables, and exits with code 1 on error.
### HTTP handlers
```rust
#[modo::handler(GET, "/todos")]
async fn list_todos(/* axum extractors */) -> modo::JsonResult<Vec<Todo>> {
// ...
Ok(modo::Json(vec![]))
}
#[modo::handler(DELETE, "/todos/{id}")]
async fn delete_todo(id: String) -> modo::HandlerResult<()> {
// `id` is extracted from the path automatically
Ok(())
}
```
Supported methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
Path parameters written as `{name}` are extracted automatically. Declare a
function parameter with the matching name and the macro injects
`axum::extract::Path` extraction. Undeclared path params are captured but
ignored (partial extraction).
### Handler-level middleware
```rust
#[modo::handler(GET, "/admin")]
#[middleware(require_auth)]
async fn admin_page() -> &'static str {
"secret"
}
// Factory middleware (called with arguments)
#[modo::handler(GET, "/dashboard")]
#[middleware(require_role("admin"))]
async fn dashboard() -> &'static str {
"dashboard"
}
```
Bare middleware paths are wrapped with `axum::middleware::from_fn`. Paths
followed by `(args)` are called as layer factories.
### Route modules
```rust
#[modo::module(prefix = "/api/v1")]
mod api {
#[modo::handler(GET, "/users")]
async fn list_users() -> &'static str { "users" }
}
// With module-level middleware
#[modo::module(prefix = "/admin", middleware = [require_auth])]
mod admin {
#[modo::handler(GET, "/dashboard")]
async fn dashboard() -> &'static str { "admin" }
}
```
All `#[handler]` attributes inside the module are automatically associated
with the module's prefix and middleware at compile time.
### Custom error handler
```rust
#[modo::error_handler]
fn my_error_handler(
err: modo::Error,
_ctx: &modo::ErrorContext,
) -> axum::response::Response {
err.default_response()
}
```
The function must be sync and accept exactly `(modo::Error, &modo::ErrorContext)`.
It is registered via `inventory` and invoked for every unhandled `modo::Error`.
### Input sanitization
```rust
#[derive(serde::Deserialize, modo::Sanitize)]
struct SignupForm {
#[clean(trim, normalize_email)]
email: String,
#[clean(trim, strip_html_tags, truncate = 500)]
bio: String,
}
```
Available `#[clean(...)]` rules: `trim`, `lowercase`, `uppercase`,
`strip_html_tags`, `collapse_whitespace`, `truncate = N`, `normalize_email`,
`custom = "path::to::fn"`.
### Input validation
```rust
#[derive(serde::Deserialize, modo::Validate)]
struct CreateTodo {
#[validate(
required(message = "title is required"),
min_length = 3,
max_length = 500
)]
title: String,
#[validate(min = 0, max = 100)]
priority: u8,
}
// In a handler:
async fn create(input: modo::validate::Json<CreateTodo>) -> modo::JsonResult<()> {
input.validate()?;
Ok(modo::Json(()))
}
```
Available `#[validate(...)]` rules: `required`, `min_length = N`,
`max_length = N`, `email`, `min = V`, `max = V`, `custom = "path::to::fn"`.
Each rule accepts an optional `(message = "...")` override.
### Templates (requires `templates` feature)
```rust
#[modo::view("pages/home.html")]
struct HomePage {
title: String,
}
// With a separate HTMX partial
#[modo::view("pages/home.html", htmx = "partials/home.html")]
struct HomePageHtmx {
title: String,
}
#[modo::template_function]
fn greeting(hour: u32) -> String {
if hour < 12 { "Good morning".into() } else { "Hello".into() }
}
#[modo::template_filter(name = "shout")]
fn shout_filter(s: String) -> String {
s.to_uppercase()
}
```
### Localisation (requires `i18n` feature)
```rust
// In a handler with an I18n extractor:
let msg = modo::t!(i18n, "welcome.message", name = username);
let items = modo::t!(i18n, "cart.items", count = cart_count);
```
`t!` calls `i18n.t_plural` when a `count` variable is present.
## Key Macros
| `#[handler]` | attribute | Register an async fn as an HTTP route |
| `#[main]` | attribute | Application entry point and runtime bootstrap |
| `#[module]` | attribute | Group routes under a shared prefix |
| `#[error_handler]` | attribute | Register a custom error handler |
| `#[Sanitize]` | derive | Generate `Sanitize::sanitize` from `#[clean]` fields |
| `#[Validate]` | derive | Generate `Validate::validate` from `#[validate]` fields |
| `t!` | function-like | Localisation key lookup with variable substitution |
| `#[view]` | attribute | Link a struct to a MiniJinja template |
| `#[template_function]` | attribute | Register a MiniJinja global function |
| `#[template_filter]` | attribute | Register a MiniJinja filter |