Skip to content

Использование макросов Axum

Библиотека axum-macros предоставляет набор полезных макросов, которые упрощают работу с Axum и делают код более лаконичным. В этом разделе мы рассмотрим возможности и примеры использования этих макросов.

Содержание

Установка axum-macros

Для использования макросов Axum необходимо добавить зависимость в Cargo.toml:

toml
[dependencies]
axum = "0.7.2"
axum-macros = "0.4.0"

Макрос debug_handler

Макрос debug_handler помогает отлаживать проблемы с обработчиками, предоставляя понятные сообщения об ошибках компиляции. Он особенно полезен при работе с экстракторами и типами возвращаемых значений.

Синтаксис

rust
use axum_macros::debug_handler;

#[debug_handler]
async fn my_handler(...) -> ... {
    // ...
}

Примеры использования

rust
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

  1. Понятные сообщения об ошибках — вместо запутанных ошибок трейтов вы получите конкретные сообщения о проблеме
  2. Проверка совместимости типов — макрос проверяет, совместимы ли экстракторы и возвращаемые типы с требованиями Axum
  3. Улучшение IDE-опыта — некоторые IDE лучше работают с кодом, использующим макросы

Макрос FromRequest

Макрос FromRequest облегчает создание собственных экстракторов, реализуя трейт FromRequest для вашего типа.

Синтаксис

rust
use axum_macros::FromRequest;

#[derive(FromRequest)]
#[from_request(via(...))]
struct MyExtractor {
    // ...
}

Примеры использования

rust
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 или другим частям запроса, но не к телу.

Синтаксис

rust
use axum_macros::FromRequestParts;

#[derive(FromRequestParts)]
#[from_request(...)]
struct MyExtractor {
    // ...
}

Примеры использования

rust
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 предоставляет более компактный синтаксис для определения маршрутов.

Синтаксис

rust
use axum_macros::routing;

#[routing("/path", method = get)]
async fn my_handler() -> ... {
    // ...
}

Примеры использования

rust
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)
}

Примеры использования

Комбинирование макросов

rust
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 особенно полезен при разработке, когда вы экспериментируете с различными экстракторами и возвращаемыми типами:

rust
// При разработке
#[debug_handler]
async fn my_handler(...) -> ... {
    // ...
}

// В продакшн-коде можно убрать debug_handler, 
// если вы уверены, что все работает правильно
async fn my_handler(...) -> ... {
    // ...
}

2. Создавайте собственные экстракторы для повторяющейся логики

rust
#[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. Используйте макросы для уменьшения дублирования кода

rust
// Без макросов
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. Документируйте кастомные экстракторы

rust
/// Экстрактор, который получает и валидирует параметры пагинации.
///
/// # Пример
///
/// ```
/// 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. Тестируйте кастомные экстракторы

rust
#[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 предоставляют удобные инструменты для упрощения разработки веб-приложений. Используя их правильно, вы можете сделать ваш код более чистым, понятным и легко поддерживаемым.