Axumのハンドラで任意で渡されるクエリパラメータを受け取る
たとえばAxumのハンドラでクエリパラメータ page
を受け付けるときに、
page=1
が付与されていれば値として1
を使うpage=a
のように値が無効ならデフォルト値を使うpage
が付与されていないならデフォルト値を使うpage=
のように値が空ならデフォルト値を使う
… のすべてをうまくハンドリングしたい。
方法
1つの方法として、たとえばAxumのextractorのドキュメントでも取り上げられているPagination
を任意のクエリパラメータに対応させると次のように書ける。crateとしてserde、serde_withも必要です。
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Pagination {
#[serde_as(as = "NoneAsEmptyString")]
pub page: Option<usize>,
#[serde_as(as = "NoneAsEmptyString")]
pub per_page: Option<usize>,
}
impl Pagination {
const DEFAULT_PAGE: usize = 1;
const DEFAULT_PER_PAGE: usize = 10;
pub fn params(&self) -> (usize, usize) {
(
self.page.unwrap_or(Self::DEFAULT_PAGE),
self.per_page.unwrap_or(Self::DEFAULT_PER_PAGE),
)
}
}
impl Default for Pagination {
fn default() -> Self {
Self {
page: Some(Self::DEFAULT_PAGE),
per_page: Some(Self::DEFAULT_PER_PAGE),
}
}
}
pub async fn get_products(query: Option<Query<Pagination>>) {
let (page, per_page) = query.unwrap_or_default().params();
// ...
}
これは
- GET /products
- GET /products?page=1
- GET /products?per_page=10
- GET /products?page
- GET /products?page=
- GET /products?page=a
のいずれも対応できる。有効なパラメータでなければ、デフォルト値を使う。
解説
AxumのextractorとSerdeを併用すると、クエリパラメータを引っこ抜いて構造体に格納できる。
#[derive(Deserialize)]
pub struct Pagination {
pub page: usize,
pub per_page: usize,
}
pub async fn get_products(query: Query<Pagination>) {
// let Query(pagination) = query;
// let page = query.page;
// ...
}
しかし、このコードだと GET products?page=
や GET /products?
のようにパラメータが送られてこなかったときエラーになる。
Failed to deserialize query string: missing field `page`
まず、GET /products?
のようにそもそもパラメータが見つからないならNone
として扱いたい。これはフィールドをOption
で包めばよい。
#[derive(Debug, Deserialize)]
pub struct Pagination {
pub page: Option<usize>,
pub per_page: Option<usize>,
}
さらに、パラメータは送られてきても値が空のときはNone
扱いにしたい。これは#[serde(default)]
とserde_withのNoneAsEmptyString
を併用することで実現できる。NoneAsEmptyString
を使うとパラメータの値が空(?page=
のような形式)をNone
にデシリアライズできるが、その代わりパラメータのキーの存在が必須になる。ここで#[serde(default)]
を付与することで、キーが存在しないときはデフォルト値をフィールドにセットするようにしておく。デフォルト値を与えるためにDefault
を実装しておくのがよい。
#[serde_as] // 先頭に付与する必要がある
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Pagination {
#[serde_as(as = "NoneAsEmptyString")]
pub page: Option<usize>,
#[serde_as(as = "NoneAsEmptyString")]
pub per_page: Option<usize>,
}
impl Default for Pagination {
fn default() -> Self {
Self {
page: Some(1),
per_page: Some(10),
}
}
}
さらに、パラメータの値が構造体のフィールドの型と合わずデシリアライズでエラーになるとき(?page=abc
のような形式)はデフォルトの値を使うとすると、extractor自体をOption
で包んでエラー時にはNone
になるようにしておき、先ほど実装したDefault
を利用することができる。
pub async fn get_products(query: Option<Query<Pagination>>) {
// paginationはデシリアライズできたものかDefault::default()で得られるもののどちらかになる
let Query(pagination) = query.unwrap_or_default();
}
これらすべてを取り込むと、最初に書いたようなコードになる。
他の方法
Query
extractorは内部(extract::Query::try_from_uri
)で文字列をSerdeのデシリアライザに渡しているので、カスタムのデシリアライザを書けばNoneAsEmptyString
は使わなくてよくなりそう。また、たとえばpage
とper_page
の片方がデシリアライズできなければ、全体をデフォルト値にフォールバックしていたが、それもカスタムのデシリアライザでデフォルト値にするなどの方法も考えられる。