Skip to content

Path параметры

Path параметры — это динамические сегменты URL, содержащие данные, которые являются частью пути. Axum предоставляет мощный механизм для извлечения и использования этих параметров в обработчиках запросов.

Основы работы с Path параметрами

В Axum для извлечения параметров из URL-пути используется экстрактор Path<T>:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};
use serde::Deserialize;

// Определяем структуру для Path-параметров
#[derive(Deserialize)]
struct UserParams {
    user_id: u64,
}

// Обработчик с извлечением Path-параметров
async fn get_user(Path(params): Path<UserParams>) -> String {
    format!("Получен запрос пользователя с ID: {}", params.user_id)
}

// Регистрация маршрута
let app = Router::new()
    .route("/users/:user_id", get(get_user));

Здесь :user_id в пути — это динамический сегмент, который будет извлечен и десериализован в поле user_id структуры UserParams.

Извлечение одиночных параметров

Для извлечения одиночного параметра можно использовать примитивные типы:

rust
async fn get_user_by_id(Path(user_id): Path<u64>) -> String {
    format!("Пользователь с ID: {}", user_id)
}

let app = Router::new()
    .route("/users/:user_id", get(get_user_by_id));

Извлечение нескольких параметров

Для извлечения нескольких параметров используйте структуры или кортежи:

rust
// Использование структуры
#[derive(Deserialize)]
struct PostParams {
    user_id: u64,
    post_id: u64,
}

async fn get_user_post(Path(params): Path<PostParams>) -> String {
    format!(
        "Пост {} пользователя {}",
        params.post_id,
        params.user_id
    )
}

// Использование кортежа
async fn get_user_post_tuple(
    Path((user_id, post_id)): Path<(u64, u64)>
) -> String {
    format!("Пост {} пользователя {}", post_id, user_id)
}

let app = Router::new()
    // Структура
    .route("/users/:user_id/posts/:post_id", get(get_user_post))
    // Кортеж
    .route("/api/users/:user_id/posts/:post_id", get(get_user_post_tuple));

Обратите внимание, что при использовании кортежа важен порядок параметров: они десериализуются в том порядке, в котором появляются в URL-пути.

Преобразование типов

Axum автоматически конвертирует параметры пути в нужные типы, если это возможно:

rust
use uuid::Uuid;
use chrono::NaiveDate;

#[derive(Deserialize)]
struct ResourceParams {
    // UUID для идентификаторов
    id: Uuid,
    
    // Дата в формате YYYY-MM-DD
    // Требуется аннотация для deserialization
    #[serde(with = "chrono::serde::ts_seconds_option")]
    date: Option<NaiveDate>,
    
    // Перечисление
    #[serde(default)]
    kind: ResourceKind,
}

#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case")]
enum ResourceKind {
    #[default]
    Basic,
    Premium,
    Enterprise,
}

async fn get_resource(Path(params): Path<ResourceParams>) -> String {
    format!(
        "Ресурс: ID={}, Дата={:?}, Тип={:?}",
        params.id,
        params.date,
        params.kind
    )
}

let app = Router::new()
    .route("/resources/:id/:date/:kind", get(get_resource));

Обработка ошибок при разборе Path-параметров

По умолчанию, если Axum не может десериализовать параметры пути, он возвращает ошибку 400 Bad Request. Можно настроить собственную обработку ошибок:

rust
use axum::{
    extract::{Path, rejection::PathRejection},
    response::{IntoResponse, Response},
    http::StatusCode,
};
use serde_json::json;

async fn handler_with_error_handling(
    result: Result<Path<u64>, PathRejection>,
) -> Response {
    match result {
        Ok(Path(user_id)) => {
            format!("Пользователь с ID: {}", user_id)
                .into_response()
        },
        Err(err) => {
            (
                StatusCode::BAD_REQUEST,
                json!({
                    "error": format!("Неверный формат ID пользователя: {}", err)
                }),
            ).into_response()
        }
    }
}

let app = Router::new()
    .route("/users/:user_id", get(handler_with_error_handling));

Использование Path с другими экстракторами

Path часто используется вместе с другими экстракторами, такими как Query, Json или State:

rust
use axum::{
    extract::{Path, Query, State, Json},
    routing::get,
    Router,
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

// Параметры пути
#[derive(Deserialize)]
struct UserParams {
    user_id: u64,
}

// Query параметры
#[derive(Deserialize)]
struct UserQuery {
    include_posts: Option<bool>,
    post_limit: Option<u32>,
}

// Данные пользователя
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    // другие поля
}

// Состояние приложения
#[derive(Clone)]
struct AppState {
    // например, пул соединений с БД
    db: Arc<Database>,
}

struct Database {
    // имитация БД
}

impl Database {
    fn find_user(&self, id: u64) -> Option<User> {
        // В реальном приложении - запрос к БД
        Some(User {
            id,
            name: format!("Пользователь {}", id),
        })
    }
}

async fn get_user(
    Path(params): Path<UserParams>,
    Query(query): Query<UserQuery>,
    State(state): State<AppState>,
) -> Result<Json<User>, StatusCode> {
    // Извлекаем пользователя из БД
    let user = state.db.find_user(params.user_id)
        .ok_or(StatusCode::NOT_FOUND)?;
    
    // Включать ли посты (из query-параметров)
    let _include_posts = query.include_posts.unwrap_or(false);
    let _post_limit = query.post_limit.unwrap_or(10);
    
    // Возвращаем пользователя как JSON
    Ok(Json(user))
}

// Инициализация состояния
let state = AppState {
    db: Arc::new(Database {}),
};

// Регистрация маршрута
let app = Router::new()
    .route("/users/:user_id", get(get_user))
    .with_state(state);

Сложные шаблоны URL и захват сегментов

Захват всего остатка пути

Для захвата остатка пути используйте параметр с символом *:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};

async fn catch_all(Path(path): Path<String>) -> String {
    format!("Захваченный путь: {}", path)
}

let app = Router::new()
    .route("/files/*path", get(catch_all));

Запрос к /files/images/avatar.png вернет Захваченный путь: images/avatar.png.

Необязательные параметры и альтернативные маршруты

Для обработки необязательных параметров используйте несколько маршрутов:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};

async fn get_resource(Path(id): Path<String>) -> String {
    format!("Ресурс: {}", id)
}

async fn get_all_resources() -> &'static str {
    "Все ресурсы"
}

let app = Router::new()
    .route("/resources/:id", get(get_resource))
    .route("/resources", get(get_all_resources));

Параметры с ограничениями

Чтобы добавить ограничения для параметров пути, используйте валидацию в обработчике:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
    http::StatusCode,
};
use serde::Deserialize;
use regex::Regex;

#[derive(Deserialize)]
struct ResourceId {
    id: String,
}

async fn get_resource(
    Path(params): Path<ResourceId>,
) -> Result<String, StatusCode> {
    // Проверка формата ID (например, только буквы и цифры)
    let re = Regex::new(r"^[a-zA-Z0-9]+$").unwrap();
    
    if !re.is_match(&params.id) {
        return Err(StatusCode::BAD_REQUEST);
    }
    
    Ok(format!("Ресурс: {}", params.id))
}

let app = Router::new()
    .route("/resources/:id", get(get_resource));

Обработка иерархических путей

Для работы с иерархическими путями, такими как категории и подкатегории:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct CategoryPath {
    category: String,
    subcategory: Option<String>,
    product_id: Option<String>,
}

async fn get_category(
    Path(params): Path<CategoryPath>,
) -> String {
    if let Some(product_id) = params.product_id {
        if let Some(subcategory) = params.subcategory {
            format!(
                "Продукт {} в подкатегории {} категории {}",
                product_id, subcategory, params.category
            )
        } else {
            format!(
                "Продукт {} в категории {}",
                product_id, params.category
            )
        }
    } else if let Some(subcategory) = params.subcategory {
        format!(
            "Подкатегория {} в категории {}",
            subcategory, params.category
        )
    } else {
        format!("Категория {}", params.category)
    }
}

let app = Router::new()
    .route("/shop/:category", get(get_category.clone()))
    .route("/shop/:category/:subcategory", get(get_category.clone()))
    .route("/shop/:category/:subcategory/:product_id", get(get_category));

Продвинутые техники

Использование enums для разделения обработки

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
    response::IntoResponse,
};
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(tag = "type", content = "data")]
enum ResourceRequest {
    #[serde(rename = "users")]
    User { id: u64 },
    #[serde(rename = "posts")]
    Post { id: u64, slug: String },
    #[serde(rename = "products")]
    Product { code: String },
}

async fn get_resource(
    Path(req): Path<ResourceRequest>,
) -> impl IntoResponse {
    match req {
        ResourceRequest::User { id } => {
            format!("Пользователь с ID: {}", id)
        },
        ResourceRequest::Post { id, slug } => {
            format!("Пост {}: {}", id, slug)
        },
        ResourceRequest::Product { code } => {
            format!("Продукт с кодом: {}", code)
        },
    }
}

let app = Router::new()
    .route("/api/:type/:data", get(get_resource));

Обработка параметров с двоеточием

Если в URL требуется использовать двоеточие как часть параметра (например, для UUID), используйте соответствующие ограничения и преобразования:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};
use serde::Deserialize;
use uuid::Uuid;

#[derive(Deserialize)]
struct NamespacedId {
    namespace: String,
    id: String,
}

async fn get_resource_by_namespaced_id(
    Path(params): Path<NamespacedId>,
) -> String {
    format!(
        "Ресурс в пространстве имен '{}' с ID '{}'",
        params.namespace,
        params.id
    )
}

let app = Router::new()
    // Маршрут, захватывающий путь вида /resources/users:123
    .route("/resources/:namespace:id", get(get_resource_by_namespaced_id));

Внутренняя работа Path-экстрактора

Path-экстрактор в Axum использует возможности библиотеки serde для десериализации строковых значений в нужные типы. Имена параметров в URL-пути должны соответствовать именам полей в структуре, используемой для десериализации.

Процесс извлечения Path-параметров включает следующие шаги:

  1. Сопоставление URL с шаблоном маршрута
  2. Извлечение значений динамических сегментов
  3. Преобразование строковых значений в требуемые типы
  4. Заполнение структуры данными

Лучшие практики

Строгая типизация

Используйте конкретные типы для параметров пути, а не String, где это возможно:

rust
// Предпочтительно
#[derive(Deserialize)]
struct UserParams {
    user_id: u64,  // используем u64 вместо String
}

// Вместо
#[derive(Deserialize)]
struct UserParamsString {
    user_id: String,  // менее типобезопасно
}

Документирование API

Добавляйте комментарии к структурам и полям для документирования параметров:

rust
/// Параметры для доступа к пользовательским ресурсам
#[derive(Deserialize)]
struct UserResourceParams {
    /// ID пользователя (числовой идентификатор)
    user_id: u64,
    /// Опциональный идентификатор ресурса
    resource_id: Option<String>,
}

Валидация

Выполняйте валидацию параметров в обработчике:

rust
use validator::{Validate, ValidationError};

#[derive(Deserialize, Validate)]
struct ItemParams {
    #[validate(range(min = 1, message = "ID должен быть положительным числом"))]
    id: u64,
    
    #[validate(custom = "validate_category")]
    category: String,
}

fn validate_category(category: &str) -> Result<(), ValidationError> {
    let valid_categories = ["books", "electronics", "clothing"];
    
    if !valid_categories.contains(&category) {
        let mut err = ValidationError::new("invalid_category");
        err.message = Some(format!(
            "Категория должна быть одной из: {}",
            valid_categories.join(", ")
        ).into());
        return Err(err);
    }
    
    Ok(())
}

async fn get_item(
    Path(params): Path<ItemParams>,
) -> Result<String, (StatusCode, String)> {
    // Валидация параметров
    if let Err(errors) = params.validate() {
        return Err((
            StatusCode::BAD_REQUEST,
            format!("Ошибка валидации: {:?}", errors),
        ));
    }
    
    Ok(format!(
        "Товар с ID: {} в категории: {}",
        params.id,
        params.category
    ))
}

Структурирование кода

Группируйте связанные параметры в отдельные структуры:

rust
// Организация кода по моделям
mod users {
    use serde::Deserialize;
    
    #[derive(Deserialize)]
    pub struct Params {
        pub user_id: u64,
    }
    
    #[derive(Deserialize)]
    pub struct PostParams {
        pub user_id: u64,
        pub post_id: u64,
    }
}

mod products {
    use serde::Deserialize;
    
    #[derive(Deserialize)]
    pub struct Params {
        pub product_id: String,
    }
}

// Использование
async fn get_user(Path(params): Path<users::Params>) -> String {
    format!("Пользователь {}", params.user_id)
}

async fn get_product(Path(params): Path<products::Params>) -> String {
    format!("Товар {}", params.product_id)
}

Заключение

Path-параметры в Axum обеспечивают мощный и типобезопасный способ извлечения данных из URL-путей. Основные преимущества:

  1. Легкая интеграция с Rust-типами через Serde
  2. Автоматическое преобразование строковых значений в нужные типы
  3. Поддержка сложных структур и вложенных типов
  4. Хорошая обработка ошибок

Правильное использование Path-параметров позволяет создавать чистые, хорошо структурированные и типобезопасные API.

Path-параметры особенно полезны для работы с ресурсно-ориентированными API в стиле REST, где идентификаторы ресурсов являются частью URL-пути.