How to parse "rich" path components in Axum?

437 views Asked by At

For URLs paths like:

/api/2023-11-01T16-52-00Z/FOO_BAR_BAZ/123_456

I would like to use:

    let app = Router::new()
        .route("/api/:start/:tokens/:coords", get(stats))
...
async fn stats(
    Path((start, tokens, coords)): Path<(DateTime<Utc>, Vec<String>, (u32, u32))>,
) {
    todo!()
}

but I get:

error[E0277]: the trait bound `fn(axum::extract::Path<(DateTime<Utc>, Vec<String>, (u32, u32))>) -> impl Future<Output = ()> {stats}: Handler<_, _>` is not satisfied
   --> src/main.rs:22:51
    |
22  |         .route("/api/:start/:tokens/:coords", get(stats))
    |                                               --- ^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(axum::extract::Path<(DateTime<Utc>, Vec<String>, (u32, u32))>) -> impl Future<Output = ()> {stats}`
    |                                               |
    |                                               required by a bound introduced by this call
    |
    = help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`

The documentation reads:

Struct axum::extract::Path source ยท

pub struct Path(pub T);

Extractor that will get captures from the URL and parse them using serde.

What serde serialisation does it use? (Serde supports many serialisations.)

How can I get it to parse (URL-safe) ISO timestamps and underscore-delimited lists?

I expect to write parsing functions for these types, but how do I hook them up?

(I could parse all the parameters as Strings and process them inside the handler function, but I'm hoping that there's a better way.)

1

There are 1 answers

4
Yoric On BEST ANSWER

You have several questions, so let's address them one at a time.

Getting the code to build

The following builds for me

use axum::{Router, routing::get, extract::Path};
use chrono::{DateTime, Utc};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/api/:start/:tokens/:coords", get(stats));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn stats(
    Path((start, tokens, coords)): Path<(DateTime<Utc>, Vec<String>, (u32, u32))>,
    )
{
    todo!()

}

I have only added the axum::Server::bind(...). I suspect that Rust had difficulties inferring your code. In the future, I suggest using #[axum::debug_handler], which improves such error messages.

Deserialization format

What serde serialisation does it use? (Serde supports many serialisations.)

Well, data structures compatible with Serde implement Deserialize, so let's look at the definition of Deserialize for DateTime:

The value to be deserialized must be an rfc3339 string.

also, if you do not like this format, additional formats are provided here. Ping me if you need help, I'll try and give you a hand.

Now, it's a bit different for our Vec. I actually have no clue what the default format is for Vec, but I'm willing to be that it's not _-separated. So let's implement _-separation.


/// A custom separator to parse `_`-separated lists.
struct UnderscoreSeparator;

impl Separator for UnderscoreSeparator {
    fn separator() -> &'static str {
        "_"
    }
}

/// A wrapper for `Vec`, meant to be deserialized for `_`-separated lists.
#[serde_as]
#[derive(Deserialize)]
struct UnderscoreVec(
    #[serde_as(as = "StringWithSeparator::<UnderscoreSeparator, String>")]
    Vec<String>
);

async fn stats(
    Path((start, UnderscoreVec(tokens), coords)): Path<(DateTime<Utc>, UnderscoreVec, (u32, u32))>,
    )
{
    todo!()

}

Does this solve your issues?