Reputation: 83
Rust newbie here (and first time SO poster)! I am attempting to add pagination to my axum API, and am trying to use an optional Query extractor on a Pagination struct in my request, to allow for users to skip specifying Pagination and allow the default values if they are sufficient for what the user needs. I was able to accomplish this pretty easily by wrapping my query param in an Option and using unwrap_or_default when grabbing it in the code. The problem I have now, is I think it would be useful to allow the user to specify just one value of pagination, i.e. limit or offset, instead of them both in the case that a user only needs to change one value and the default for the other works fine.
I have an impl Default set up to specify a default value for pagination if it is not supplied by the user, but the issue I'm running into now is that the user must specify both the limit and offset to have pagination work - if only one is specified, the axum Query extractor fails to parse the struct and uses the default Pagination instead, disregarding what the user specified for the query params completely.
I thought I could try making the inner fields of the structs Options, but then my default trait would only cover a None case, and I'd have to set up some handling function like Pagination::fill(pagination: Pagination) -> Pagination
or something similar that would parse through all fields and set the None values to their default values, which I would have to specify somewhere and would ultimately defeat the whole point of the default function and does not feel like best practice.
Is there some better way to handle the simple Default for a struct with optional fields, or perhaps some axum extractor specific way to allow for a query struct that can have any permutations of it's fields set (i.e. I am considering implementing a custom extractor for pagination specifically, but feel like there has to be some easier way to handle this behavior).
Example
use axum::{
routing::get,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Pagination {
pub offset: i64,
pub limit: i64,
}
impl Default for Pagination {
fn default() -> Self {
offset: 0,
limit: 10,
}
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/get_many_items", get(get_many_items));
axum::Server::bind(&"0.0.0.0:7878".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// route handler that will return a lot of items
// =============================================
// if the user calls without any query param set or ?offset=<offset>&limit=<limit>
// expected behavior - pagination is set to default or user specified resp.
//
// if the user calls ?offset=<offset> or ?limit=<limit>
// unexpected behavior, query extractor fails and default pagination is used instead
async fn get_many_items(pagination: Option<Pagination>) -> String {
let Query(pagination) = pagination.unwrap_or_default();
let message = format!("offset={} & limit={}", pagination.offset, pagination.limit);
println!("{}", &message);
message
}
Actions attempted
I attempted making both fields Options, but if the query param is not specified the default query extractor fails to recognize the partial fields as the struct still. I am also considering setting explicitly declared constant default values in my pagination struct and using those instead of the Default trait, but adding const values to structs does not seem to be a very "rust" solution to the problem. Additionally, I could change the fields into their own structs, like
pub struct PaginationLimit {
limit: i64,
}
impl Default for PaginationLimit {
fn default() -> Self {
Self {
limit: 10,
}
}
}
pub struct PaginationOffset {
offset: i64,
}
impl Default for PaginationOffset {
fn default() -> Self {
Self {
offset: 0,
}
}
}
or I could just parse the values themselves without using the struct encapsulaiton e.g.
fn get_many(Query(limit): Option<i64>, Query(offset): Option<i64>) { ...
but that feels wrong, and like it would open up paths to a bunch of issues later as you would have to manually specify all fields of pagination in the params of any handler you want pagination in (not to mention the refactoring required if pagination's fields were to change ever in the future).
Finally, I am considering adding in a custom extractor for this use case, but as I would expect this to be a somewhat common use case I would at least like to confirm with the community there is not an easier/better way to handle this behavior before going forwards with that approach.
Relevant links found while researching the topic:
Axum extract documentation:
Top Google Results:
Upvotes: 7
Views: 6447
Reputation: 350
As you wrote yourself, you can set individual fields to options and the deserialization process will fill in the blanks with None; but that's just the serde deserialization default which can be overridden through the default attribute
See https://serde.rs/field-attrs.html
For example this worked for me:
fn default_str() -> String {
"test".to_string()
}
#[derive(Deserialize, Debug)]
struct FormStruct {
#[serde(default = "default_str")]
field: String,
}
Upvotes: 3
Reputation: 387
I'm also newbie here but I don't think you can.
It has nothing to do with Axum
but more like how unwrap_or_default
work. (Query extractor use unwrap_or_default)
When you implement Default
, you are implementing for the Pagination
struct itself, not their fields (or nested). Default only operates on the struct implementing it.
So, when Pagination
is None
, you can fallback to its default.
However, when Pagination
itself is not None
(eventhough there are some fields inside it that is None
) the unwrap_or_default
just will not traverse to all underlying fields (and nested fields -- if there is) and fill-in their value. From what it sees, the struct is already fullfiled -- no need to use default values.
Even it does traverse, I would assume it will fill-in the default value of the field type (0 for i64), not what we defined on the upper struct level.
Anyway, here's another work around that might work beside breaking offset
and limit
into struct itself.
use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PaginationParams {
pub offset: Option<i64>,
pub limit: Option<i64>,
}
pub struct Pagination {
pub offset: i64,
pub limit: i64,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/get_many_items", get(get_many_items));
axum::Server::bind(&"0.0.0.0:7878".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// route handler that will return a lot of items
// =============================================
// if the user calls without any query param set or ?offset=<offset>&limit=<limit>
// expected behavior - pagination is set to default or user specified resp.
//
// if the user calls ?offset=<offset> or ?limit=<limit>
// unexpected behavior, query extractor fails and default pagination is used instead
async fn get_many_items(Query(pagination_params): Query<PaginationParams>) -> String {
let pagination = Pagination {
offset: pagination_params.offset.map_or(0, |v| v),
limit: pagination_params.limit.map_or(10, |v| v),
};
let message = format!("offset={} & limit={}", pagination.offset, pagination.limit);
println!("{}", &message);
message
}
or you can do
async fn get_many_items(Query(pagination_params): Query<PaginationParams>) -> String {
let pagination = Pagination::from(pagination_params);
let message = format!("offset={} & limit={}", pagination.offset, pagination.limit);
println!("{}", &message);
message
}
impl From<PaginationParams> for Pagination {
fn from(params: PaginationParams) -> Self {
Self {
offset: params.offset.map_or(0, |v| v),
limit: params.limit.map_or(10, |v| v),
}
}
}
or you can do it in your custom extract
Upvotes: 0