Использование макросов Axum
Библиотека axum-macros
предоставляет набор полезных макросов, которые упрощают работу с Axum и делают код более лаконичным. В этом разделе мы рассмотрим возможности и примеры использования этих макросов.
Содержание
- Установка axum-macros
- Макрос debug_handler
- Макрос FromRequest
- Макрос FromRequestParts
- Макрос routing
- Примеры использования
- Лучшие практики
Установка axum-macros
Для использования макросов Axum необходимо добавить зависимость в Cargo.toml
:
[dependencies]
axum = "0.7.2"
axum-macros = "0.4.0"
Макрос debug_handler
Макрос debug_handler
помогает отлаживать проблемы с обработчиками, предоставляя понятные сообщения об ошибках компиляции. Он особенно полезен при работе с экстракторами и типами возвращаемых значений.
Синтаксис
use axum_macros::debug_handler;
#[debug_handler]
async fn my_handler(...) -> ... {
// ...
}
Примеры использования
use axum::{
extract::{Path, Query},
http::StatusCode,
Json,
};
use axum_macros::debug_handler;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Params {
limit: Option<usize>,
offset: Option<usize>,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
// Простой обработчик с использованием debug_handler
#[debug_handler]
async fn get_user(Path(user_id): Path<u64>) -> Result<Json<User>, StatusCode> {
if user_id == 42 {
Ok(Json(User {
id: 42,
name: "Alice".to_string(),
}))
} else {
Err(StatusCode::NOT_FOUND)
}
}
// Более сложный обработчик с несколькими экстракторами
#[debug_handler]
async fn list_users(
Query(params): Query<Params>,
header: Option<TypedHeader<headers::Authorization<Bearer>>>,
) -> Result<Json<Vec<User>>, StatusCode> {
// Проверка авторизации
if header.is_none() {
return Err(StatusCode::UNAUTHORIZED);
}
// Имитация получения пользователей
let users = vec![
User { id: 1, name: "Alice".to_string() },
User { id: 2, name: "Bob".to_string() },
];
Ok(Json(users))
}
Преимущества использования debug_handler
- Понятные сообщения об ошибках — вместо запутанных ошибок трейтов вы получите конкретные сообщения о проблеме
- Проверка совместимости типов — макрос проверяет, совместимы ли экстракторы и возвращаемые типы с требованиями Axum
- Улучшение IDE-опыта — некоторые IDE лучше работают с кодом, использующим макросы
Макрос FromRequest
Макрос FromRequest
облегчает создание собственных экстракторов, реализуя трейт FromRequest
для вашего типа.
Синтаксис
use axum_macros::FromRequest;
#[derive(FromRequest)]
#[from_request(via(...))]
struct MyExtractor {
// ...
}
Примеры использования
use axum::{
extract::{FromRequest, Query},
http::StatusCode,
};
use axum_macros::FromRequest;
use serde::Deserialize;
// Определение параметров пагинации
#[derive(Deserialize)]
struct PaginationParams {
page: Option<usize>,
per_page: Option<usize>,
}
// Создание экстрактора для пагинации с валидацией
#[derive(FromRequest)]
#[from_request(via(Query))] // Извлекаем через Query
struct ValidatedPagination {
page: usize,
per_page: usize,
total_pages: usize,
}
// Реализация преобразования из исходных параметров
impl From<PaginationParams> for ValidatedPagination {
fn from(params: PaginationParams) -> Self {
// Установка значений по умолчанию и границ
let page = params.page.unwrap_or(1).max(1);
let per_page = params.per_page.unwrap_or(20).clamp(1, 100);
// Вычисляем общее количество страниц (в реальном приложении
// здесь будет запрос в БД для определения общего количества элементов)
let total_items = 255; // Например, у нас есть 255 элементов всего
let total_pages = (total_items + per_page - 1) / per_page;
Self {
page,
per_page,
total_pages,
}
}
}
// Использование нашего экстрактора в обработчике
async fn list_items(
pagination: ValidatedPagination,
) -> String {
format!(
"Страница {} из {} (по {} элементов на странице)",
pagination.page,
pagination.total_pages,
pagination.per_page
)
}
Макрос FromRequestParts
Макрос FromRequestParts
похож на FromRequest
, но реализует трейт FromRequestParts
, который не требует доступа к телу запроса. Это полезно для экстракторов, которым нужен доступ только к заголовкам, URI или другим частям запроса, но не к телу.
Синтаксис
use axum_macros::FromRequestParts;
#[derive(FromRequestParts)]
#[from_request(...)]
struct MyExtractor {
// ...
}
Примеры использования
use axum::{
extract::{FromRequestParts, TypedHeader},
headers::UserAgent,
};
use axum_macros::FromRequestParts;
// Экстрактор для информации о клиенте
#[derive(FromRequestParts)]
#[from_request(rejection(StatusCode))] // Использование StatusCode для отклонения
struct ClientInfo {
user_agent: String,
ip_address: String,
}
// Реализация FromRequestParts вручную для более сложной логики
#[async_trait]
impl<B> FromRequestParts<B> for ClientInfo
where
B: Send + Sync,
{
type Rejection = StatusCode;
async fn from_request_parts(
parts: &mut RequestParts,
state: &S,
) -> Result<Self, Self::Rejection> {
// Получаем User-Agent из заголовков
let user_agent = TypedHeader::<UserAgent>::from_request_parts(parts, state)
.await
.map(|ua| ua.to_string())
.unwrap_or_else(|_| "Unknown".to_string());
// Получаем IP-адрес из заголовков или соединения
let ip_address = parts
.headers
.get("X-Forwarded-For")
.and_then(|v| v.to_str().ok())
.or_else(|| {
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|ci| ci.0.ip().to_string().as_str())
})
.unwrap_or("unknown")
.to_string();
Ok(ClientInfo {
user_agent,
ip_address,
})
}
}
// Использование нашего экстрактора
async fn client_info_handler(
client: ClientInfo,
) -> String {
format!(
"Клиент: User-Agent={}, IP={}",
client.user_agent,
client.ip_address
)
}
Макрос routing
Макрос routing
предоставляет более компактный синтаксис для определения маршрутов.
Синтаксис
use axum_macros::routing;
#[routing("/path", method = get)]
async fn my_handler() -> ... {
// ...
}
Примеры использования
use axum::{
Router,
http::StatusCode,
response::IntoResponse,
};
use axum_macros::routing;
// Определение GET обработчика
#[routing("/users", method = get)]
async fn list_users() -> &'static str {
"Список пользователей"
}
// Определение POST обработчика
#[routing("/users", method = post)]
async fn create_user() -> impl IntoResponse {
(StatusCode::CREATED, "Пользователь создан")
}
// Обработчик с параметрами пути
#[routing("/users/:id", method = get)]
async fn get_user(Path(id): Path<String>) -> impl IntoResponse {
format!("Пользователь с ID: {}", id)
}
// Создание маршрутизатора с обработчиками
fn create_router() -> Router {
Router::new()
.merge(list_users)
.merge(create_user)
.merge(get_user)
}
Примеры использования
Комбинирование макросов
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum_macros::{debug_handler, FromRequest, routing};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
// Модель данных
#[derive(Serialize, Deserialize)]
struct Task {
id: u64,
title: String,
completed: bool,
}
// Сервис для работы с задачами
#[derive(Clone)]
struct TaskService {
// В реальном приложении здесь был бы доступ к БД
}
impl TaskService {
fn new() -> Self {
Self {}
}
async fn get_task(&self, id: u64) -> Option<Task> {
// Имитация получения задачи из БД
if id == 1 {
Some(Task {
id: 1,
title: "Изучить Axum".to_string(),
completed: false,
})
} else {
None
}
}
}
// Создание кастомного экстрактора для параметров фильтрации
#[derive(FromRequest, Deserialize)]
#[from_request(via(Query))]
struct TaskFilters {
completed: Option<bool>,
search: Option<String>,
}
// Обработчик с использованием макросов
#[routing("/tasks/:id", method = get)]
#[debug_handler]
async fn get_task(
Path(id): Path<u64>,
State(service): State<Arc<TaskService>>,
) -> Result<Json<Task>, StatusCode> {
match service.get_task(id).await {
Some(task) => Ok(Json(task)),
None => Err(StatusCode::NOT_FOUND),
}
}
// Обработчик со сложной валидацией и множеством экстракторов
#[routing("/tasks", method = get)]
#[debug_handler]
async fn list_tasks(
filters: TaskFilters,
pagination: ValidatedPagination,
State(service): State<Arc<TaskService>>,
) -> impl IntoResponse {
// Здесь будет логика получения и фильтрации задач
let tasks = vec![
Task {
id: 1,
title: "Изучить Axum".to_string(),
completed: false,
},
Task {
id: 2,
title: "Изучить макросы Axum".to_string(),
completed: true,
},
];
// Фильтрация по completed, если указан
let tasks = if let Some(completed) = filters.completed {
tasks.into_iter()
.filter(|t| t.completed == completed)
.collect::<Vec<_>>()
} else {
tasks
};
// Фильтрация по поиску
let tasks = if let Some(search) = filters.search {
tasks.into_iter()
.filter(|t| t.title.contains(&search))
.collect::<Vec<_>>()
} else {
tasks
};
Json(tasks)
}
Лучшие практики
1. Используйте debug_handler при разработке
Макрос debug_handler
особенно полезен при разработке, когда вы экспериментируете с различными экстракторами и возвращаемыми типами:
// При разработке
#[debug_handler]
async fn my_handler(...) -> ... {
// ...
}
// В продакшн-коде можно убрать debug_handler,
// если вы уверены, что все работает правильно
async fn my_handler(...) -> ... {
// ...
}
2. Создавайте собственные экстракторы для повторяющейся логики
#[derive(FromRequest)]
#[from_request(via(Path))]
struct UserId(u64);
#[derive(FromRequest)]
#[from_request(via(TypedHeader))]
struct AuthToken(String);
// Использование
async fn get_user_profile(
UserId(id): UserId,
AuthToken(token): AuthToken,
) -> impl IntoResponse {
// ...
}
3. Используйте макросы для уменьшения дублирования кода
// Без макросов
async fn handler1(
Path(id): Path<u64>,
Query(params): Query<Params>,
State(db): State<PgPool>,
) -> Result<Json<Task>, StatusCode> {
// Валидация params
// Проверка доступа
// Получение данных
// ...
}
async fn handler2(
Path(id): Path<u64>,
Query(params): Query<Params>,
State(db): State<PgPool>,
) -> Result<Json<Task>, StatusCode> {
// Валидация params
// Проверка доступа
// Получение данных
// ...
}
// С макросами
#[derive(FromRequest)]
struct ValidatedRequest {
id: u64,
params: ValidatedParams,
db: PgPool,
}
// Реализация FromRequest для извлечения и валидации всех параметров
// ...
// Теперь обработчики проще
async fn handler1(req: ValidatedRequest) -> Result<Json<Task>, StatusCode> {
// Работа с уже валидированными данными
// ...
}
async fn handler2(req: ValidatedRequest) -> Result<Json<Task>, StatusCode> {
// Работа с уже валидированными данными
// ...
}
4. Документируйте кастомные экстракторы
/// Экстрактор, который получает и валидирует параметры пагинации.
///
/// # Пример
///
/// ```
/// async fn handler(pagination: ValidatedPagination) -> impl IntoResponse {
/// format!("Page: {}, PerPage: {}", pagination.page, pagination.per_page)
/// }
/// ```
#[derive(FromRequest)]
#[from_request(via(Query))]
struct ValidatedPagination {
/// Номер страницы (начиная с 1)
pub page: usize,
/// Количество элементов на странице (от 1 до 100)
pub per_page: usize,
}
5. Тестируйте кастомные экстракторы
#[cfg(test)]
mod tests {
use super::*;
use axum::{
body::Body,
http::{Request, StatusCode},
routing::get,
Router,
};
use tower::ServiceExt;
// Обработчик для тестирования
async fn test_handler(pagination: ValidatedPagination) -> String {
format!("page={}, per_page={}", pagination.page, pagination.per_page)
}
#[tokio::test]
async fn test_pagination_extractor_defaults() {
// Настройка маршрутизатора с обработчиком
let app = Router::new().route("/test", get(test_handler));
// Запрос без параметров - должны использоваться значения по умолчанию
let response = app
.oneshot(Request::builder().uri("/test").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"page=1, per_page=20");
}
#[tokio::test]
async fn test_pagination_extractor_with_params() {
let app = Router::new().route("/test", get(test_handler));
// Запрос с параметрами
let response = app
.oneshot(Request::builder().uri("/test?page=2&per_page=50").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"page=2, per_page=50");
}
#[tokio::test]
async fn test_pagination_extractor_validation() {
let app = Router::new().route("/test", get(test_handler));
// Запрос с некорректными параметрами - должен применяться clamp
let response = app
.oneshot(Request::builder().uri("/test?page=0&per_page=200").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"page=1, per_page=100"); // Значения ограничены
}
}
Макросы Axum предоставляют удобные инструменты для упрощения разработки веб-приложений. Используя их правильно, вы можете сделать ваш код более чистым, понятным и легко поддерживаемым.