Crate wayfind

source ·
Expand description

crates.io documentation rust: 1.66+ unsafe: forbidden license: MIT/Apache-2.0

codspeed codecov

§wayfind

A speedy, flexible router for Rust.

§Why another router?

wayfind attempts to bridge the gap between existing Rust router options:

  • fast routers, lacking in flexibility
  • flexible routers, lacking in speed

Real-world projects often need fancy routing capabilities, such as projects ported from frameworks like Ruby on Rails, or those adhering to specifications like the Open Container Initiative (OCI) Distribution Specification.

The goal of wayfind is to remain competitive with the fastest libraries, while offering advanced routing features when needed. Unused features shouldn’t impact performance - you only pay for what you use.

§Features

§Dynamic Routing

Dynamic parameters allow matching for any byte, excluding the path delimiter /.

We support both:

  • whole segment parameters: /{name}/
  • inline parameters: /{year}-{month}-{day}/

Inline dynamic parameters are greedy in nature, similar to a regex .*, and will attempt to match as many bytes as possible.

§Example
use std::error::Error;
use wayfind::{Path, Router};

fn main() -> Result<(), Box<dyn Error>> {
    let mut router = Router::new();
    router.insert("/users/{id}", 1)?;
    router.insert("/users/{id}/files/{filename}.{extension}", 2)?;

    let path = Path::new("/users/123")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/users/{id}".into());
    assert_eq!(search.parameters[0].key, "id");
    assert_eq!(search.parameters[0].value, "123");

    let path = Path::new("/users/123/files/my.document.pdf")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/users/{id}/files/{filename}.{extension}".into());
    assert_eq!(search.parameters[0].key, "id");
    assert_eq!(search.parameters[0].value, "123");
    assert_eq!(search.parameters[1].key, "filename");
    assert_eq!(search.parameters[1].value, "my.document");
    assert_eq!(search.parameters[2].key, "extension");
    assert_eq!(search.parameters[2].value, "pdf");

    Ok(())
}

§Wildcard Routing

Wildcard parameters enable matching of one or more segments within a path.

We support both:

  • mid-route wildcards: /api/{*path}/help
  • end-route catch-all: /{*catch_all}
§Example
use std::error::Error;
use wayfind::{Path, Router};

fn main() -> Result<(), Box<dyn Error>> {
    let mut router = Router::new();
    router.insert("/files/{*slug}/delete", 1)?;
    router.insert("/{*catch_all}", 2)?;

    let path = Path::new("/files/documents/reports/annual.pdf/delete")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/files/{*slug}/delete".into());
    assert_eq!(search.parameters[0].key, "slug");
    assert_eq!(search.parameters[0].value, "documents/reports/annual.pdf");

    let path = Path::new("/any/other/path")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/{*catch_all}".into());
    assert_eq!(search.parameters[0].key, "catch_all");
    assert_eq!(search.parameters[0].value, "any/other/path");

    Ok(())
}

§Optional Parameters

wayfind supports marking a parameter as optional.

We support:

  • optional dynamic segments: /users/{user?}
  • optional dynamic inlines: /release/v{major}.{minor?}.{patch?}
  • optional wildcard segments: /files/{*file?}/info

Optional parameters work as syntactic sugar for equivilant, simplified routes.

/users/{user?}:

  • /users/{user}
  • /users

/release/v{major}.{minor?}.{patch?}:

  • /release/v{major}.{minor}.{patch}
  • /release/v{major}.{minor}
  • /release/v{major}

/files/{*file?}/info:

  • /files/{*file}/info
  • /files/info

There is a small overhead to using optionals, due to Arc usage internally for data storage.

§Example
use std::error::Error;
use wayfind::{Path, Router};

fn main() -> Result<(), Box<dyn Error>> {
    let mut router = Router::new();
    router.insert("/users/{id?}", 1)?;
    router.insert("/files/{*slug}/{file}.{extension?}", 2)?;

    let path = Path::new("/users")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/users/{id?}".into());
    assert_eq!(search.expanded, Some("/users".into()));

    let path = Path::new("/users/123")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/users/{id?}".into());
    assert_eq!(search.expanded, Some("/users/{id}".into()));
    assert_eq!(search.parameters[0].key, "id");
    assert_eq!(search.parameters[0].value, "123");

    let path = Path::new("/files/documents/folder/report.pdf")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/files/{*slug}/{file}.{extension?}".into());
    assert_eq!(search.expanded, Some("/files/{*slug}/{file}.{extension}".into()));
    assert_eq!(search.parameters[0].key, "slug");
    assert_eq!(search.parameters[0].value, "documents/folder");
    assert_eq!(search.parameters[1].key, "file");
    assert_eq!(search.parameters[1].value, "report");
    assert_eq!(search.parameters[2].key, "extension");
    assert_eq!(search.parameters[2].value, "pdf");

    let path = Path::new("/files/documents/folder/readme")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/files/{*slug}/{file}.{extension?}".into());
    assert_eq!(search.expanded, Some("/files/{*slug}/{file}".into()));
    assert_eq!(search.parameters[0].key, "slug");
    assert_eq!(search.parameters[0].value, "documents/folder");
    assert_eq!(search.parameters[1].key, "file");
    assert_eq!(search.parameters[1].value, "readme");

    Ok(())
}

§Constraints

Constraints allow for custom logic to be injected into the routing process.

We support constraints for all types of parameters:

  • Dynamic constraint: /{name:constraint}
  • Wildcard constraint: /{*name:constraint}

The typical use-case for constraints would be to run a regex, or a simple FromStr implementation, against a path segment.

A common mistake would be to use these for validation of parameters. This should be avoided.

If a constraint fails to match, and no other suitable match exists, it results in a Not Found response, rather than any sort of Bad Request.

They act as an escape-hatch for when you need to disambiguate routes.

The current constraint implementation has a number of limitations:

  • constraints cannot take parameters
  • checks cannot make use of any prior state
  • checks cannot store data after a successful check
§Default Constraints

wayfind ships with a number of default constraints.

Curently, these can’t be disabled.

NameMethod
u8u8::from_str
u16u16::from_str
u32u32::from_str
u64u64::from_str
u128u128::from_str
usizeusize::from_str
i8i8::from_str
i16i16::from_str
i32i32::from_str
i64i64::from_str
i128i128::from_str
isizeisize::from_str
f32f32::from_str
f64f64::from_str
boolbool::from_str
ipv4Ipv4Addr::from_str
ipv6Ipv6Addr::from_str
§Example
use std::error::Error;
use wayfind::{Constraint, Path, Router};

struct NamespaceConstraint;
impl Constraint for NamespaceConstraint {
    const NAME: &'static str = "namespace";

    fn check(segment: &str) -> bool {
        segment
            .split('/')
            .all(|part| {
                !part.is_empty() && part.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
            })
    }
}

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

    router.insert("/v2", 1)?;
    router.insert("/v2/{*name:namespace}/blobs/{type}:{digest}", 2)?;

    let path = Path::new("/v2")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/v2".into());

    let path = Path::new("/v2/my-org/my-repo/blobs/sha256:1234567890")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/v2/{*name:namespace}/blobs/{type}:{digest}".into());
    assert_eq!(search.parameters[0].key, "name");
    assert_eq!(search.parameters[0].value, "my-org/my-repo");
    assert_eq!(search.parameters[1].key, "type");
    assert_eq!(search.parameters[1].value, "sha256");
    assert_eq!(search.parameters[2].key, "digest");
    assert_eq!(search.parameters[2].value, "1234567890");

    let path = Path::new("/v2/invalid repo/blobs/uploads")?;
    assert!(router.search(&path)?.is_none());

    Ok(())
}

§Optional Trailing Slashes

wayfind supports optional trailing slashes.

This works via adding {/} to the end of a route.

§Example
use std::error::Error;
use wayfind::{Path, Router};

fn main() -> Result<(), Box<dyn Error>> {
    let mut router = Router::new();
    router.insert("/users{/}", 1)?;
    router.insert("/posts/{id}{/}", 2)?;

    let path = Path::new("/users")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/users{/}".into());
    assert_eq!(search.expanded, Some("/users".into()));

    let path = Path::new("/users/")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 1);
    assert_eq!(search.route, "/users{/}".into());
    assert_eq!(search.expanded, Some("/users/".into()));

    let path = Path::new("/posts/123")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/posts/{id}{/}".into());
    assert_eq!(search.expanded, Some("/posts/{id}".into()));
    assert_eq!(search.parameters[0].key, "id");
    assert_eq!(search.parameters[0].value, "123");

    let path = Path::new("/posts/123/")?;
    let search = router.search(&path)?.unwrap();
    assert_eq!(*search.data, 2);
    assert_eq!(search.route, "/posts/{id}{/}".into());
    assert_eq!(search.expanded, Some("/posts/{id}/".into()));
    assert_eq!(search.parameters[0].key, "id");
    assert_eq!(search.parameters[0].value, "123");

    Ok(())
}

§User-Friendly Error Messages

Where possible, we try to provide user-friendly error messages.

§Example
use std::error::Error;
use wayfind::{Constraint, Router, errors::ConstraintError};

const ERROR_DISPLAY: &str = "
duplicate constraint name

The constraint name 'my_constraint' is already in use:
    - existing constraint type: 'rust_out::ConstraintA'
    - new constraint type: 'rust_out::ConstraintB'

help: each constraint must have a unique name

try:
    - Check if you have accidentally added the same constraint twice
    - Ensure different constraints have different names
";

struct ConstraintA;
impl Constraint for ConstraintA {
    const NAME: &'static str = "my_constraint";

    fn check(segment: &str) -> bool {
        segment == "a"
    }
}

struct ConstraintB;
impl Constraint for ConstraintB {
    const NAME: &'static str = "my_constraint";

    fn check(segment: &str) -> bool {
        segment == "b"
    }
}

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

    let error = router.constraint::<ConstraintB>().unwrap_err();
    assert_eq!(error.to_string(), ERROR_DISPLAY.trim());

    Ok(())
}

§Router Display

Routers can print their routes as an tree diagram.

  • represents the root node.
  • represents nodes within the tree that can be matched against.

Currenty, this doesn’t handle split multi-byte characters well.

§Example
use std::error::Error;
use wayfind::Router;

const ROUTER_DISPLAY: &str = "
▽
├─ /
│  ├─ pet ○
│  │    ╰─ /
│  │       ├─ findBy
│  │       │       ├─ Status ○
│  │       │       ╰─ Tags ○
│  │       ╰─ {petId} ○
│  │                ╰─ /uploadImage ○
│  ├─ store/
│  │       ├─ inventory ○
│  │       ╰─ order ○
│  │              ╰─ /
│  │                 ╰─ {orderId} ○
│  ╰─ user ○
│        ╰─ /
│           ├─ createWithList ○
│           ├─ log
│           │    ├─ in ○
│           │    ╰─ out ○
│           ╰─ {username} ○
╰─ {*catch_all} ○
";

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

    router.insert("/pet", 1)?;
    router.insert("/pet/findByStatus", 2)?;
    router.insert("/pet/findByTags", 3)?;
    router.insert("/pet/{petId}", 4)?;
    router.insert("/pet/{petId}/uploadImage", 5)?;

    router.insert("/store/inventory", 6)?;
    router.insert("/store/order", 7)?;
    router.insert("/store/order/{orderId}", 8)?;

    router.insert("/user", 9)?;
    router.insert("/user/createWithList", 10)?;
    router.insert("/user/login", 11)?;
    router.insert("/user/logout", 12)?;
    router.insert("/user/{username}", 13)?;

    router.insert("{*catch_all}", 14)?;

    assert_eq!(router.to_string(), ROUTER_DISPLAY.trim());
    Ok(())
}

§Performance

wayfind is fast, and appears to be competitive against other top performers in all benchmarks we currently run.

However, as is often the case, your mileage may vary (YMMV). Benchmarks, especially micro-benchmarks, should be taken with a grain of salt.

§Benchmarks

All benchmarks ran on a M1 Pro laptop.

Check out our codspeed results for a more accurate set of timings.

§Context

For all benchmarks, we percent-decode the path before matching. After matching, we convert any extracted parameters to strings.

Some routers perform these operations automatically, while others require them to be done manually.

We do this to try and match behaviour as best as possible. This is as close to an “apples-to-apples” comparison as we can get.

§matchit inspired benches

In a router of 130 routes, benchmark matching 4 paths.

LibraryTimeAlloc CountAlloc SizeDealloc CountDealloc Size
matchit470.40 ns4416 B4448 B
wayfind499.12 ns7649 B7649 B
xitca-router567.11 ns7800 B7832 B
path-tree579.33 ns4416 B4448 B
ntex-router1.7468 µs181.248 KB181.28 KB
route-recognizer4.6578 µs1608.515 KB1608.547 KB
routefinder6.5084 µs675.024 KB675.056 KB
actix-router21.377 µs21413.93 KB21413.96 KB
§path-tree inspired benches

In a router of 320 routes, benchmark matching 80 paths.

LibraryTimeAlloc CountAlloc SizeDealloc CountDealloc Size
wayfind7.2267 µs1179.991 KB1179.991 KB
matchit8.9524 µs14017.81 KB14017.83 KB
path-tree9.4107 µs597.447 KB597.47 KB
xitca-router10.948 µs20925.51 KB20925.53 KB
ntex-router29.514 µs20119.54 KB20119.56 KB
routefinder100.21 µs52548.4 KB52548.43 KB
route-recognizer107.15 µs2872191.8 KB2872205 KB
actix-router185.69 µs2201128.8 KB2201128.8 KB

§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
  • ASP.NET Core: Constraints-based approach to routing

Modules§

Structs§

Traits§

  • A constraint that can be used for custom path routing logic.