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:
- https://docs.rs/axum/latest/axum/extract/index.html#applying-multiple-extractors
Specifies how to set an optional query struct, but not how to handle optional values inside the struct
Top Google Results:
- How to use both axum::extract::Query and axum::extract::State with Axum?
Just about using multiple extractors combined in request - https://www.reddit.com/r/learnrust/comments/yk0wib/axum_optional_path_extractor/
About optional path extractors, not query extractor or multiple optional values