Customizing Axum Query extractor

Hey there,

I'm trying to modify or if necessary create a new extractor for deserializing a query string into a Vec<Enum> but I'm a bit out of my depth, I can see that it uses the serde_urlencoded crate which does have a Serialize Enum function, I'm thinking that maybe I could extend it to create a Vec with an enum for each query param, but I'm lost on how to go about it.

I could use some pointers, or maybe I'm going about this wrong to begin with ?

Thanks.

x-www-form-urlencoded doesn't support much data types besides key-value pairs unless you define some custom syntax on top of it. I don't think I ever coaxed serde_urlencoded to deserialize something into a Vec<T>, but I might be wrong. Do you have an example of your query parameters and your desired output?

The idea would be to use the Variant name as the key, something like this:

// ?user_id=12?department=foo would turn into Vec<Filter>:
// [Filter::UserId(12), Filter::Department("foo")]
enum Filter {
    UserId(u32),
    Department(String),
}

Can there be multiple user id or department filters in the same query?

That's not really needed in my use case, but it wouldn't be a problem if it had duplicate keys.

When I need to find out how I can deserialize something into my types I usually start the wrong way round by first finding out how (in the case of x-www-form-urlencoded I find out if my types serialize rather than how most of the time) my types serialize. Doing this for your Vec<Filter> validates my suspicion that serde_urlencoded can't handle it, panicking during serialization:

use serde::Serialize;
use serde_urlencoded::to_string;

#[derive(Serialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
enum Filter {
    UserId(u32),
    Department(String),
}

fn main() {
    let query = "user_id=12&department=foo";

    let v = vec![Filter::UserId(12), Filter::Department("foo".to_owned())];

    let s = to_string(&v).unwrap();

    assert_eq!(s, query);
}

Playground.

stdout:

thread 'main' panicked at src/main.rs:16:27:
called `Result::unwrap()` on an `Err` value: Custom("unsupported pair")

So we need some other way to represent your query on the Rust side.

That means there is no meaning to providing two user_ids, for example. I.e. you wouldn't return results for both user_ids. In that case I'd just rewrite your Filter enum to a struct with optional fields, as serde_urlencoded can parse this:

use serde::Deserialize;
use serde_urlencoded::from_str;

#[derive(Deserialize, Debug, PartialEq)]
struct Filter {
    user_id: Option<u32>,
    department: Option<String>,
}

fn main() {
    let query = "user_id=12&department=foo";

    let v: Filter = from_str(query).unwrap();

    assert_eq!(
        v,
        Filter {
            user_id: Some(12),
            department: Some("foo".to_owned())
        }
    );
}

Playground.

Thanks, the main reason I don't like the struct approach is that while its nice that I can have multiple keys with different types, Its not really suited for iterating over the fields, which means I would need to write a macro (or implement indexing for my struct ?), which honestly might be easier than implementing Deserialize for my type (I'm not sure, never done it before : D), but I wanted to see if I could make it.

I also think it would be a nicer API for doing something like:

fn apply(query_builder: &mut QB, filters: Vec<Filter>) {
    filters.into_iter().for_each(|filter| match filter {
         Filter::UserId(id) => query_builder.where("user_id", id),
         Filter::Department(name) => query_builder.where("department", name),
    });
}

If I can't keep you from falling down the rabbit hole that is x-www-form-urlencoded, here a starting point for your adventure:

use serde::de::{Deserializer, MapAccess, Visitor};
use serde::Deserialize;

use serde_urlencoded::from_str;

#[derive(Debug, PartialEq)]
enum Filter {
    UserId(u32),
    Department(String),
}

#[derive(PartialEq, Debug)]
pub struct FilterVec(Vec<Filter>);

struct FilterVisitor;

impl<'de> Visitor<'de> for FilterVisitor {
    type Value = Vec<Filter>;

    fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str("filter in a format `filter_name=filter_value` with `filter_name` and `filter_value` being urlencoded")
    }

    fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
    where
        M: MapAccess<'de>,
    {
        let mut res: Vec<Filter> = Vec::with_capacity(map.size_hint().unwrap_or(0));

        while let Some((key, value)) = map.next_entry::<&str, String>()? {
            match key {
                "user_id" => {
                    res.push(Filter::UserId(value.parse().expect("TODO")));
                }
                "department" => {
                    res.push(Filter::Department(value));
                }
                _ => panic!("TODO"),
            }
        }

        Ok(res)
    }
}

impl<'de> Deserialize<'de> for FilterVec {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(FilterVec(deserializer.deserialize_map(FilterVisitor)?))
    }
}

fn main() {
    let query = "user_id=12&department=foo";

    let res: FilterVec = from_str(query).unwrap();

    let expect = FilterVec(vec![
        Filter::UserId(12),
        Filter::Department("foo".to_owned()),
    ]);

    assert_eq!(res, expect);
}

Playground.

In the snippet above I didn't handle the actual encoding, i.e. strings with encoded characters will still have the encoded characters (a space " " becomes "%20" or even worse a "+" sometimes, for example). I once fell down that rabbit hole too, so here my implementation of the decoding part.

Never mind, serde_urlencoded actually handles decoding, which is pretty cool.

1 Like

Thank you !

1 Like