Skip to content

DuskSystems/wayfind

Repository files navigation

license: MIT/Apache-2.0 crates.io documentation

rust: 1.88+ unsafe: forbidden wasm: compatible no-std: compatible

codecov codspeed

wayfind

A speedy, flexible router for Rust.

Why another router?

Real-world projects often need advanced routing: inline parameters, mid-route wildcards, or compatibility with frameworks like Ruby on Rails and specifications like the OCI Distribution Specification.

wayfind aims to be competitive with the fastest routers while supporting these features. Unused features don't impact performance.

Showcase

[dependencies]
wayfind = "0.9"
use core::error::Error;

use wayfind::Router;

fn main() -> Result<(), Box<dyn Error>> {
    let mut router = Router::new();

    // Static
    router.insert("/", 1)?;
    router.insert("/health", 2)?;

    {
        let search = router.search("/").unwrap();
        assert_eq!(search.data, &1);
        assert_eq!(search.template, "/");

        let search = router.search("/health").unwrap();
        assert_eq!(search.data, &2);
        assert_eq!(search.template, "/health");

        let search = router.search("/heal");
        assert_eq!(search, None);
    }

    // Dynamic
    router.insert("/users/<id>", 3)?;
    router.insert("/users/<id>/message", 4)?;

    {
        let search = router.search("/users/123").unwrap();
        assert_eq!(search.data, &3);
        assert_eq!(search.template, "/users/<id>");
        assert_eq!(search.parameters[0], ("id", "123"));

        let search = router.search("/users/123/message").unwrap();
        assert_eq!(search.data, &4);
        assert_eq!(search.template, "/users/<id>/message");
        assert_eq!(search.parameters[0], ("id", "123"));

        let search = router.search("/users/");
        assert_eq!(search, None);
    }

    // Dynamic Inline
    router.insert("/images/<name>.png", 5)?;

    {
        let search = router.search("/images/avatar.final.png").unwrap();
        assert_eq!(search.data, &5);
        assert_eq!(search.template, "/images/<name>.png");
        assert_eq!(search.parameters[0], ("name", "avatar.final"));

        let search = router.search("/images/.png");
        assert_eq!(search, None);
    }

    // Wildcard
    router.insert("/files/<*path>", 6)?;
    router.insert("/files/<*path>/delete", 7)?;

    {
        let search = router.search("/files/documents").unwrap();
        assert_eq!(search.data, &6);
        assert_eq!(search.template, "/files/<*path>");
        assert_eq!(search.parameters[0], ("path", "documents"));

        let search = router.search("/files/documents/my-project/delete").unwrap();
        assert_eq!(search.data, &7);
        assert_eq!(search.template, "/files/<*path>/delete");
        assert_eq!(search.parameters[0], ("path", "documents/my-project"));

        let search = router.search("/files");
        assert_eq!(search, None);
    }

    // Wildcard Inline
    router.insert("/backups/<*path>.tar.gz", 8)?;

    {
        let search = router.search("/backups/production/database.tar.gz").unwrap();
        assert_eq!(search.data, &8);
        assert_eq!(search.template, "/backups/<*path>.tar.gz");
        assert_eq!(search.parameters[0], ("path", "production/database"));

        let search = router.search("/backups/.tar.gz");
        assert_eq!(search, None);
    }

    println!("{router}");
    Ok(())
}
/
├─ backups/
│  ╰─ <*path>
│     ╰─ .tar.gz
├─ files/
│  ├─ <*path>
│  │  ╰─ /delete
│  ╰─ <*path>
├─ health
├─ images/
│  ╰─ <name>
│     ╰─ .png
╰─ users/
   ╰─ <id>
      ╰─ /message

Implementation Details

wayfind stores routes in a compressed radix trie.

When searching, each node tries its children in priority order:

  1. static
  2. dynamic
  3. wildcard

All parameters are greedy, consuming as much of the path as possible.

Limitations

There is no backtracking across priority levels. This can result in some matches which may be unexpected.

In the following router:

/api/
├─ <version>
│  ╰─ /
│     ╰─ <*rest>
╰─ <*path>
   ╰─ /help

The path /api/docs/help would match the first route, not the second. Even though the second is arguably more specific.

Performance

wayfind is competitive with the fastest Rust routers across all benchmarks we run.

For all benchmarks, we convert any extracted parameters to strings.

All routers provide a way to return parameters as strings, but some delay the actual UTF-8 decoding until post-search.

Library Percent Decoding String Parameters
wayfind no yes
actix-router partial yes
matchit no delayed
ntex-router partial yes
path-tree no delayed
route-recognizer no yes
xitca-router no yes

As such, we provide 2 sets of results per benchmark:

  • one with the default behaviour of the router.
  • one with the parameters extracted to Vec<(&str, &str)>.

See the results at: https://codspeed.io/DuskSystems/wayfind/benchmarks

License

wayfind is licensed under the terms of both the MIT License and the Apache License (Version 2.0).

Inspirations

  • poem: Initial experimentations started out as a Poem router fork
  • matchit: Performance leader among pre-existing routers
  • path-tree: Extensive testing and router display feature

About

A speedy, flexible router for Rust.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Contributors

Languages