wayfind

Crates.iowayfind
lib.rswayfind
version0.9.0
created_at2024-07-31 13:50:27.370374+00
updated_at2025-08-27 17:38:48.223661+00
descriptionA speedy, flexible router.
homepage
repositoryhttps://github.com/DuskSystems/wayfind
max_upload_size
id1320945
size147,060
Cathal (CathalMullan)

documentation

README

license: MIT/Apache-2.0 crates.io documentation

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

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.

Showcase

[dependencies]
wayfind = "0.9"
use std::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)?;
    router.insert("/images/<name>.<ext>", 6)?;

    {
        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/photo.jpg").unwrap();
        assert_eq!(search.data, &6);
        assert_eq!(search.template, "/images/<name>.<ext>");
        assert_eq!(search.parameters[0], ("name", "photo"));
        assert_eq!(search.parameters[1], ("ext", "jpg"));

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

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

    {
        let search = router.search("/files/documents").unwrap();
        assert_eq!(search.data, &7);
        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, &8);
        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", 9)?;
    router.insert("/backups/<*path>.<ext>", 10)?;

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

        let search = router.search("/backups/dev/application.log.bak").unwrap();
        assert_eq!(search.data, &10);
        assert_eq!(search.template, "/backups/<*path>.<ext>");
        assert_eq!(search.parameters[0], ("path", "dev/application.log"));
        assert_eq!(search.parameters[1], ("ext", "bak"));

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

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

Implementation details

wayfind uses a compressed radix trie for its data storage. This is the common backbone of almost all routers implemented in Rust.

What sets wayfind apart is its search strategy. Most routers either use "first match wins" or "best match wins" (via backtracking), wayfind uses a hybrid approach:

  • per segment: first match wins
  • within segment: best match wins

You only pay the cost of backtracking if you make use of inline parameters, and only for that given segment.

This can result in some matches which may be unexpected, but in practice it works well for real-world usage.

Performance

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

See BENCHMARKING.md for the results.

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
Commit count: 455

cargo fmt