Query параметры
Query параметры — важная часть URL, которая позволяет передавать данные в HTTP-запросах методом GET. Axum предоставляет удобные инструменты для работы с query-параметрами, автоматически извлекая и преобразуя их в Rust-структуры.
Основы работы с Query параметрами
В Axum для извлечения query-параметров используется экстрактор Query<T>
, где T
- тип, в который будут десериализованы параметры:
use axum::{
extract::Query,
routing::get,
Router,
};
use serde::Deserialize;
// Определяем структуру для query-параметров
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
// Обработчик с извлечением query-параметров
async fn list_items(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
format!("Страница: {}, Элементов на странице: {}", page, per_page)
}
// Регистрация маршрута
let app = Router::new()
.route("/items", get(list_items));
Этот код обрабатывает запросы вида /items?page=2&per_page=20
.
Обработка простых типов
Для обработки одиночных параметров можно использовать простые типы:
// Извлечение одиночного параметра
#[derive(Deserialize)]
struct IdParam {
id: String,
}
async fn get_by_id(Query(params): Query<IdParam>) -> String {
format!("ID: {}", params.id)
}
// Или с использованием HashMap
use std::collections::HashMap;
async fn generic_handler(Query(params): Query<HashMap<String, String>>) -> String {
let mut response = String::new();
for (key, value) in params {
response.push_str(&format!("{}={}, ", key, value));
}
response
}
Вложенные структуры и массивы
Axum поддерживает сложные структуры в query-параметрах:
#[derive(Deserialize)]
struct FilterParams {
// Фильтр по диапазону дат
from_date: Option<String>,
to_date: Option<String>,
// Фильтр по множеству категорий
// Позволяет обрабатывать ?category=1&category=2&category=3
category: Option<Vec<u32>>,
// Фильтр по статусу
status: Option<String>,
}
async fn filter_items(
Query(params): Query<FilterParams>,
) -> String {
let mut response = String::new();
if let Some(from) = params.from_date {
response.push_str(&format!("От: {}, ", from));
}
if let Some(to) = params.to_date {
response.push_str(&format!("До: {}, ", to));
}
if let Some(categories) = params.category {
response.push_str(&format!("Категории: {:?}, ", categories));
}
if let Some(status) = params.status {
response.push_str(&format!("Статус: {}", status));
}
response
}
Преобразование и валидация
Serde позволяет выполнять преобразование и базовую валидацию параметров:
use chrono::{DateTime, Utc};
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchParams {
// Поисковый запрос
q: String,
// Преобразование строки в дату
#[serde(default)]
#[serde(with = "chrono::serde::ts_seconds_option")]
after: Option<DateTime<Utc>>,
// Ограничение значений с помощью валидации
#[serde(default = "default_limit")]
#[serde(deserialize_with = "validate_limit")]
limit: u32,
}
fn default_limit() -> u32 {
10
}
fn validate_limit<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::Deserializer<'de>,
{
let limit: u32 = u32::deserialize(deserializer)?;
// Ограничиваем максимальное значение
if limit > 100 {
return Ok(100);
}
// Минимальное значение
if limit == 0 {
return Ok(1);
}
Ok(limit)
}
async fn search(
Query(params): Query<SearchParams>,
) -> String {
format!(
"Поиск: {}, После: {:?}, Лимит: {}",
params.q,
params.after,
params.limit
)
}
Обработка ошибок при разборе query-параметров
По умолчанию, если Axum не может десериализовать query-параметры, он возвращает ошибку 400 Bad Request. Можно настроить собственную обработку ошибок:
use axum::{
extract::{Query, rejection::QueryRejection},
response::{IntoResponse, Response},
http::StatusCode,
};
use serde_json::json;
async fn handler_with_error_handling(
result: Result<Query<Pagination>, QueryRejection>,
) -> Response {
match result {
Ok(Query(params)) => {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
format!("Страница: {}, Элементов на странице: {}", page, per_page)
.into_response()
},
Err(err) => {
(
StatusCode::BAD_REQUEST,
json!({
"error": format!("Неверные параметры запроса: {}", err)
}),
).into_response()
}
}
}
Опциональные параметры
Для необязательных параметров используйте Option
:
#[derive(Deserialize)]
struct QueryParams {
// Обязательный параметр
id: String,
// Необязательные параметры
name: Option<String>,
age: Option<u32>,
// Параметр со значением по умолчанию
#[serde(default = "default_sort")]
sort: String,
}
fn default_sort() -> String {
"name".to_string()
}
async fn get_item(
Query(params): Query<QueryParams>,
) -> String {
let name_display = match params.name {
Some(name) => format!("с именем '{}'", name),
None => "без указания имени".to_string(),
};
let age_display = match params.age {
Some(age) => format!("возраст: {}", age),
None => "возраст не указан".to_string(),
};
format!(
"Запрос элемента с ID: {} {} ({}), сортировка: {}",
params.id,
name_display,
age_display,
params.sort
)
}
Использование Query с Path и другими экстракторами
Query часто используется в сочетании с другими экстракторами:
use axum::{
extract::{Path, Query, State},
routing::get,
Router,
};
use serde::Deserialize;
// Параметры пути
#[derive(Deserialize)]
struct UserParams {
user_id: u64,
}
// Query-параметры
#[derive(Deserialize)]
struct UserQuery {
include_details: Option<bool>,
}
// Состояние приложения
#[derive(Clone)]
struct AppState {
// Например, соединение с БД
}
async fn get_user(
Path(path): Path<UserParams>,
Query(query): Query<UserQuery>,
State(state): State<AppState>,
) -> String {
let show_details = query.include_details.unwrap_or(false);
format!(
"Получение пользователя с ID: {}, показывать детали: {}",
path.user_id,
show_details
)
}
// Регистрация маршрута
let app = Router::new()
.route("/users/:user_id", get(get_user))
.with_state(AppState { /* ... */ });
Сложные случаи использования
Фильтрация и сортировка
Query-параметры часто используются для фильтрации и сортировки данных:
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize)]
struct ListOptions {
// Пагинация
page: Option<u32>,
per_page: Option<u32>,
// Сортировка
sort_by: Option<String>,
order: Option<String>, // asc или desc
// Фильтры - произвольные поля
#[serde(flatten)]
filters: HashMap<String, String>,
}
async fn list_resources(
Query(options): Query<ListOptions>,
) -> String {
let page = options.page.unwrap_or(1);
let per_page = options.per_page.unwrap_or(10);
let sort_by = options.sort_by.unwrap_or_else(|| "id".to_string());
let order = options.order.unwrap_or_else(|| "asc".to_string());
let mut filters_str = String::new();
for (key, value) in options.filters {
// Пропускаем служебные параметры
if !["page", "per_page", "sort_by", "order"].contains(&key.as_str()) {
filters_str.push_str(&format!("{}={}, ", key, value));
}
}
format!(
"Страница: {}, Элементов: {}, Сортировка: {} {}, Фильтры: {}",
page, per_page, sort_by, order, filters_str
)
}
Поиск с множеством параметров
Для сложного поиска с множеством опциональных параметров:
#[derive(Deserialize)]
struct SearchOptions {
// Поисковый запрос
q: Option<String>,
// Временной диапазон
from: Option<String>,
to: Option<String>,
// Фильтры по категориям (множественный выбор)
category: Option<Vec<String>>,
// Фильтр по статусу
status: Option<String>,
// Геопозиция (для поиска ближайших)
lat: Option<f64>,
lng: Option<f64>,
radius: Option<u32>,
}
async fn search(
Query(options): Query<SearchOptions>,
) -> String {
let mut search_description = String::new();
if let Some(q) = options.q {
search_description.push_str(&format!("Запрос: '{}', ", q));
}
// Дополнительная логика для обработки других параметров
// ...
if let Some(status) = options.status {
search_description.push_str(&format!("Статус: {}, ", status));
}
// Проверка наличия геопозиции
if options.lat.is_some() && options.lng.is_some() {
let radius = options.radius.unwrap_or(10);
search_description.push_str(
&format!(
"Поиск в радиусе {} км от координат ({}, {})",
radius,
options.lat.unwrap(),
options.lng.unwrap()
)
);
}
search_description
}
Лучшие практики
Структурирование параметров
Для большей удобочитаемости и поддерживаемости группируйте связанные параметры в структуры:
#[derive(Deserialize)]
struct ListParams {
// Параметры пагинации
#[serde(flatten)]
pagination: Pagination,
// Параметры сортировки
#[serde(flatten)]
sorting: Sorting,
// Другие параметры
// ...
}
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
#[derive(Deserialize)]
struct Sorting {
sort_by: Option<String>,
order: Option<String>,
}
async fn list_handler(
Query(params): Query<ListParams>,
) -> String {
let page = params.pagination.page.unwrap_or(1);
let per_page = params.pagination.per_page.unwrap_or(10);
let sort_by = params.sorting.sort_by.unwrap_or_else(|| "id".to_string());
let order = params.sorting.order.unwrap_or_else(|| "asc".to_string());
// ...
format!(
"Страница: {}, Элементов: {}, Сортировка: {} {}",
page, per_page, sort_by, order
)
}
Валидация значений
Используйте библиотеку validator
для валидации параметров:
use validator::{Validate, ValidationError};
#[derive(Deserialize, Validate)]
struct SearchParams {
#[validate(length(min = 2, message = "Поисковый запрос должен содержать не менее 2 символов"))]
q: Option<String>,
#[validate(range(min = 1, max = 100, message = "Лимит должен быть от 1 до 100"))]
limit: Option<u32>,
#[validate(custom = "validate_sort_field")]
sort_by: Option<String>,
}
fn validate_sort_field(sort: &str) -> Result<(), ValidationError> {
// Список разрешенных полей для сортировки
let allowed_fields = ["id", "name", "created_at", "price"];
if !allowed_fields.contains(&sort) {
let mut err = ValidationError::new("invalid_sort_field");
err.message = Some(format!(
"Недопустимое поле для сортировки. Разрешены: {}",
allowed_fields.join(", ")
).into());
return Err(err);
}
Ok(())
}
async fn search(
Query(params): Query<SearchParams>,
) -> Result<String, (StatusCode, String)> {
// Валидация параметров
if let Err(errors) = params.validate() {
return Err((
StatusCode::BAD_REQUEST,
format!("Ошибка валидации: {:?}", errors),
));
}
// Обработка запроса
let q = params.q.unwrap_or_default();
let limit = params.limit.unwrap_or(10);
let sort_by = params.sort_by.unwrap_or_else(|| "id".to_string());
Ok(format!(
"Поиск: '{}', Лимит: {}, Сортировка по: {}",
q, limit, sort_by
))
}
Заключение
Query-параметры в Axum обеспечивают удобный и типобезопасный способ передачи данных в GET-запросах. Используя экстрактор Query<T>
и возможности Serde, вы можете легко создавать API с гибкими параметрами фильтрации, сортировки и пагинации.
Ключевые преимущества подхода Axum:
- Автоматическая десериализация параметров в Rust-структуры
- Поддержка вложенных структур и массивов
- Возможность валидации и преобразования значений
- Типобезопасность и хорошая обработка ошибок
Правильная организация и валидация query-параметров делает ваш API удобным, предсказуемым и защищенным от ошибок.