Path параметры
Path параметры — это динамические сегменты URL, содержащие данные, которые являются частью пути. Axum предоставляет мощный механизм для извлечения и использования этих параметров в обработчиках запросов.
Основы работы с Path параметрами
В Axum для извлечения параметров из URL-пути используется экстрактор Path<T>
:
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
.
Извлечение одиночных параметров
Для извлечения одиночного параметра можно использовать примитивные типы:
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));
Извлечение нескольких параметров
Для извлечения нескольких параметров используйте структуры или кортежи:
// Использование структуры
#[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 автоматически конвертирует параметры пути в нужные типы, если это возможно:
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. Можно настроить собственную обработку ошибок:
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:
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 и захват сегментов
Захват всего остатка пути
Для захвата остатка пути используйте параметр с символом *
:
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
.
Необязательные параметры и альтернативные маршруты
Для обработки необязательных параметров используйте несколько маршрутов:
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));
Параметры с ограничениями
Чтобы добавить ограничения для параметров пути, используйте валидацию в обработчике:
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(¶ms.id) {
return Err(StatusCode::BAD_REQUEST);
}
Ok(format!("Ресурс: {}", params.id))
}
let app = Router::new()
.route("/resources/:id", get(get_resource));
Обработка иерархических путей
Для работы с иерархическими путями, такими как категории и подкатегории:
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 для разделения обработки
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), используйте соответствующие ограничения и преобразования:
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-параметров включает следующие шаги:
- Сопоставление URL с шаблоном маршрута
- Извлечение значений динамических сегментов
- Преобразование строковых значений в требуемые типы
- Заполнение структуры данными
Лучшие практики
Строгая типизация
Используйте конкретные типы для параметров пути, а не String, где это возможно:
// Предпочтительно
#[derive(Deserialize)]
struct UserParams {
user_id: u64, // используем u64 вместо String
}
// Вместо
#[derive(Deserialize)]
struct UserParamsString {
user_id: String, // менее типобезопасно
}
Документирование API
Добавляйте комментарии к структурам и полям для документирования параметров:
/// Параметры для доступа к пользовательским ресурсам
#[derive(Deserialize)]
struct UserResourceParams {
/// ID пользователя (числовой идентификатор)
user_id: u64,
/// Опциональный идентификатор ресурса
resource_id: Option<String>,
}
Валидация
Выполняйте валидацию параметров в обработчике:
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
))
}
Структурирование кода
Группируйте связанные параметры в отдельные структуры:
// Организация кода по моделям
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-путей. Основные преимущества:
- Легкая интеграция с Rust-типами через Serde
- Автоматическое преобразование строковых значений в нужные типы
- Поддержка сложных структур и вложенных типов
- Хорошая обработка ошибок
Правильное использование Path-параметров позволяет создавать чистые, хорошо структурированные и типобезопасные API.
Path-параметры особенно полезны для работы с ресурсно-ориентированными API в стиле REST, где идентификаторы ресурсов являются частью URL-пути.