Parameterized routing for generic resources in Rust

Overview

Usher

Build Status Crates.io

Usher provides an easy way to construct parameterized routing trees in Rust.

The nodes of these trees is naturally generic, allowing Usher to lend itself to a wide variety of use cases. Matching and parameterization rules are defined by the developer using a simple set of traits, allowing for customization in the routing algorithm itself. This provides easy support for various contexts in which routing may be used.

This project was born of a personal need for something small to sit on top of Hyper, without having to work with a whole framework. Over time it became clear that it provides utility outside of the HTTP realm, and so the API was adapted to become more generic. As such, Usher provides several "extensions" based on certain domains which essentially provide sugar over a typical router. These extensions are all off by default, but can easily be set as enabled via Cargo features.

Prior to v1.0 you can expect the API to receive some changes, although I will do my best to keep this to a minimum to reduce any churn involved. One choice that is perhaps going to change is the API around using non-filesystem based pathing. Other than that expect changes as optimizations (and the likely API refactoring associated with them) still need to be fully investigated.

Getting Started

Usher is available on crates.io. The easiest way to use it is to add an entry to your Cargo.toml defining the dependency:

[dependencies]
usher = "0.1"

If you require any of the Usher extensions, you can opt into them by setting the feature flags in your dependency configuration:

usher = { version = "0.1", features = ["web"] }

You can find the available extensions in the documentation.

Basic Usage

The construction of a tree is quite simple, depending on what your desired outcome is. To construct a very basic/static tree, you can simply insert the routes you care about:

use usher::prelude::*;

fn main() {
    // First we construct our `Router` using a set of parsers. Fortunately for
    // this example, Usher includes the `StaticParser` which uses basic string
    // matching to determine whether a segment in the path matches.
    let mut router: Router<String> = Router::new(vec![
        Box::new(StaticParser),
    ]);

    // Then we insert our routes; in this case we're going to store the numbers
    // 1, 2 and 3, against their equivalent name in typed English (with a "/"
    // as a prefix, as Usher expects filesystem-like paths (for now)).
    router.insert("/one", "1".to_string());
    router.insert("/two", "2".to_string());
    router.insert("/three", "3".to_string());

    // Finally we'll just do a lookup on each path, as well as the a path which
    // doesn't match ("/"), just to demonstrate what the return types look like.
    for path in vec!["/", "/one", "/two", "/three"] {
        println!("{}: {:?}", path, router.lookup(path));
    }
}

This will route exactly as it looks; matching each static segment provided against the tree and retrieving the value associated with the path. The return type of the lookup(path) function is Option<(&T, Vec<(&str, (usize, usize)>)>, with &T referring to the generic value provided ("1", etc), and the Vec including a set of any parameters found during routing. In the case of no parameters, this vector will be empty (as is the case above).

For usage based around extensions (such as HTTP), please see the documentation for the module containing it - or visit the examples directory for actual usage.

Advanced Usage

Of course, for some use cases you need to be able to control more than statically matching against the path segments. In a web framework, you might allow for some path segments which match regardless and simply capture their value (i.e. :id). In order to allow this type of usage, there are two traits available in Usher; the Parser and Matcher traits. These two traits can be implemented to describe how to match against specific segments in an incoming path.

The Matcher trait is used to determine if an incoming path segment matches a configured path segment. It's also responsible for pulling out any capture that is associated with the incoming segment. The Parser trait is used to calculate which Matcher type should be used on a configured path segment. At a glance it might seem that these two traits could be combined but the difference is that the Parser trait operates at router creation time, whereas the Matcher trait exists for execution when matching against a created router.

To demonstrate these traits, we can use the :id example of a typical web framework. The concept of this syntax is that it should match any value provided to the tree. If my router was configured with the path /:id, it would match incoming paths of /123 and /abc (but not /). This would provide a captured value id which holds the value 123 or abc.

Matcher

This pattern is pretty simple to implement using the two traits we defined above. First of all we must construct our Matcher type (technically you might write the Parser first, but it's easier to explain in this order). Fortunately, the rules here are very simple.

/// A `Matcher` type used to match against dynamic segments.
///
/// The internal value here is the name of the path parameter (based on the
/// example talked through above, this would be the _owned_ `String` of `"id"`).
pub struct DynamicMatcher {
    inner: String
}

impl Matcher for DynamicMatcher {
    /// Determines if there is a capture for the incoming segment.
    ///
    /// In the pattern we described above the entire value becomes the capture,
    /// so we return a tuple of `("id", (start, end))` to represent the capture.
    fn capture(&self, segment: &str) -> Option<(&str, (usize, usize))> {
        Some((&self.inner, (0, segment.len())))
    }

    /// Determines if this matcher matches the incoming segment.
    ///
    /// Because the segment is dynamic and matches any value, this is able to
    /// always return `true` without even looking at the incoming segment.
    fn is_match(&self, _segment: &str) -> bool {
        true
    }
}

This implementation is fairly trivial and should be quite self-explanatory; the matcher matches anything so is_match/1 will always return true. We always want to capture the segment, so that's returned from capture/1. A couple of things to mention about captures;

  • An implementation of capture/1 is option, as it will default to None.
  • The capture/1 implementation is only called if is_match/1 resolved to true.
  • The tuple structure used for captures is necessary as we need some way to know the name of the captures at runtime. The names cannot be stored in the router itself as there may be use cases where the capture name is actually a function of the incoming path segment (not in this case specifically, of course).

Parser

Now that we have our Matcher type, we need to construct a Parser type in order to associate the configured segments with the correct Matcher. This is pretty trivial in our case, because pretty much the only rule we have is that the segment must be of the pattern :.+, which we can roughly translate to starts_with(":") for example purposes. As such, a Parser type might look like this:

/// A `Parser` type used to parse out `DynamicMatcher` values.
pub struct DynamicParser;

impl Parser for DynamicParser {
    /// Attempts to parse a segment into a corresponding `Matcher`.
    ///
    /// As a dynamic segment is determined by the pattern `:.+`, we check the first
    /// character of the segment. If the segment is not `:` we are unable to parse
    /// and so return a `None` value.
    ///
    /// If it does start with a `:`, we construct a `DynamicMatcher` and pass the
    /// parameter name through as it's used when capturing values.
    fn parse(&self, segment: &str) -> Option<Box<Matcher>> {
        if &segment[0..1] != ":" {
            return None;
        }

        let field = &segment[1..];
        let matcher = DynamicMatcher {
            inner: field.to_owned()
        };

        Some(Box::new(matcher))
    }
}

One of the nice things about splitting the traits is that you can switch up the syntax easily. Although both DynamicMatcher and DynamicParser are included in Usher, you might want to use a different syntax. One other example of syntax for parameters (I think in the Java realm) is {id}. To accomodate this case, you only have to write a new Parser implementation; the existing Matcher struct already works!

/// A customer `Parser` type used to parse out `DynamicMatcher` values.
pub struct CustomDynamicParser;

impl Parser for CustomDynamicParser {
    /// Attempts to parse a segment into a corresponding `Matcher`.
    ///
    /// This will match segments based on `{id}` syntax, rather than `:id`. We have
    /// to check the end characters, and pass back the something in the middle!
    fn parse(&self, segment: &str) -> Option<Box<Matcher>> {
        // has to start with "{"
        if &segment[0..1] != "{" {
            return None;
        }

        // has to end with "}"
        if &segment[(len - 1)..] != "}" {
            return None;
        }

        // so 1..(len - 1) trim the brackets
        let field = &segment[1..(len - 1)];
        let matcher = DynamicMatcher::new(field);

        // wrap it up!
        Some(Box::new(matcher))
    }
}

Of course, this also makes it trivial to match either of the two forms shown above. You can attach both parsers to your tree at startup, and it will allow for both :id and {id}. This flexibility can be definitely be useful when writing more involved frameworks, using Usher as the underlying routing layer.

Configuration

Now we have our types, we have to actually configure them in a router in order for them to take effect. This is done at router initialization time, and you've already seen an example of this in the basic example where we provide the basic StaticParser type. Much like this example, we pass our parser in directly:

let mut router: Router<String> = Router::new(vec![
    Box::new(DynamicParser),
    Box::new(StaticParser),
]);

Using this definition, our new Parser will be used to determine if we can parse dynamic segments from the path. Below is a demonstration of a simple path which makes use of both matcher types (S dictates a static segment, and D dictates a dynamic segment):

/api/user/:id
  ^   ^    ^
  |   |    |
  S   S    D

Please note that the order the parsers are provided is very important; you should place the most "specific" parsers first as they are tested in order. If you placed StaticParser first in the list above, then nothing would ever continue through to the DynamicParser as every segment satisfies the StaticParser requirements.

You might also like...
A proof of concept implementation of cyclic data structures in stable, safe, Rust.

A proof of concept implementation of cyclic data structures in stable, safe, Rust. This demonstrates the combined power of the static-rc crate and the

Doubly-Linked List Implementation in Rust

Doubly-Linked List Implementation in Rust Purely for educational and recreational purposes. For real world production please use std::collections::Lin

Rust library for string parsing of basic data structures.

afmt Simple rust library for parsing basic data structures from strings. Usage You can specify string formats to any strucute, via the use of the fmt

Hash Table Implementation in Rust
Hash Table Implementation in Rust

Hash Table Implementation in Rust Purely for educational and recreational purposes. For real world production please use std::collections::HashMap. Fo

SegVec data structure for rust. Similar to Vec, but allocates memory in chunks of increasing size.

segvec This crate provides the SegVec data structure. It is similar to Vec, but allocates memory in chunks of increasing size, referred to as "segment

Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

Blazing fast immutable collection datatypes for Rust.

imbl Blazing fast immutable collection datatypes for Rust. This is a fork of the im crate, which appears to be unmaintained. (This fork is not current

Algorithms and Data Structures of all kinds written in Rust.

Classic Algorithms in Rust This repo contains the implementation of various classic algorithms for educational purposes in Rust. Right now, it is in i

VLists in rust

Running WORK IN PROGRESS! (0. curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh) 1. cargo test Motivation In the urbit memory model every

Comments
  • Come up with a way to easily enable

    Come up with a way to easily enable "all" methods

    Initially this could be some basic sugar in a macro that just binds all methods (which sucks). In future we could add support for some sort of compact type to alias this though; I think it's just a tradeoff of CPU vs. RAM.

    enhancement 
    opened by whitfin 0
  • Investigate a builder interface for routers

    Investigate a builder interface for routers

    Originally I felt this was unnecessary, but the currently implementation lends itself well to a builder as there are several advantages of a final build() call.

    • We can drop all Parser structures, because they're no longer needed.
    • We can compress all internal memory, as it's a guaranteed size (so e.g. shrink maps/vectors)`.
    • We can avoid Box<Parser> at creation time and use a generic list structure.

    It also lends itself better to customization in future - the types behind the builder can be out of the public API (and documented as such). This allows us to change various types ourselves, and return a Router<_>.

    enhancement 
    opened by whitfin 0
  • Provide a trait to enable dynamic collection of captures into a custom type

    Provide a trait to enable dynamic collection of captures into a custom type

    Currently all captures are Vec<(&str, (usize, usize))>. This is probably the most efficient way we can go by default, but it's not particularly friendly for those who need to then change this type into something else (e.g. HashMap<&str, String>. It would be better if we could go directly into that type at capture time.

    This would avoid an unnecessary Vec allocation, and save a few cycles for the actual parsing. It is likely also easier for the developer to provide the trait implementation, rather than manually parsing out their values.

    enhancement 
    opened by whitfin 0
  • Investigate support for nested router instances

    Investigate support for nested router instances

    One feature that other routers seem to like is the ability to plug a new router in as a delegate, rather than just routes themselves.

    If I create paths /a, /b and /c on a router, I could attach that router on /numbers on a parent router to create /numbers/a, etc. It's unsure whether this would be re-calculated directly into the tree, or there would be some switch on whether a node is a router or not.

    Technically you could build this on the current public API (I think), but it might be nice to support it directly as it allows more module code. You could have a users.rs which exposes all paths on the /user route - rather than always attach this prefix, this file could export a Router which is mounted on /user in a parent router.

    enhancement 
    opened by whitfin 0
Releases(v0.2.1)
Owner
Isaac Whitfield
Fan of all things automated. OSS when applicable. Author of Cachex for Elixir. Senior Software Engineer at Axway. Intelligence wanes without practice.
Isaac Whitfield
A lightweight Rust library for BitVector Rank&Select operations, coupled with a generic Sparse Array implementation

A lightweight Rust library for BitVector Rank&Select operations, coupled with a generic Sparse Array implementation

Alperen Keleş 5 Jun 20, 2022
Rust-algorithm-club - Learn algorithms and data structures with Rust

Rust Algorithm Club ?? ?? This repo is under construction. Most materials are written in Chinese. Check it out here if you are able to read Chinese. W

Weihang Lo 360 Dec 28, 2022
Ternary search tree collection in rust

tst Ternary search tree collection in rust with similar API to std::collections as it possible. Ternary search tree is a type of trie (sometimes calle

Alexey Pervushin 20 Dec 7, 2022
Array helpers for Rust's Vector and String types

array_tool Array helpers for Rust. Some of the most common methods you would use on Arrays made available on Vectors. Polymorphic implementations for

Daniel P. Clark 69 Dec 9, 2022
A priority queue for Rust with efficient change function.

PriorityQueue This crate implements a Priority Queue with a function to change the priority of an object. Priority and items are stored in an IndexMap

null 139 Dec 30, 2022
K-dimensional tree in Rust for fast geospatial indexing and lookup

kdtree K-dimensional tree in Rust for fast geospatial indexing and nearest neighbors lookup Crate Documentation Usage Benchmark License Usage Add kdtr

Rui Hu 152 Jan 2, 2023
Roaring bitmap implementation for Rust

RoaringBitmap This is not yet production ready. The API should be mostly complete now. This is a Rust port of the Roaring bitmap data structure, initi

Roaring bitmaps: A better compressed bitset 552 Jan 1, 2023
Rust Persistent Data Structures

Rust Persistent Data Structures Rust Persistent Data Structures provides fully persistent data structures with structural sharing. Setup To use rpds a

Diogo Sousa 883 Dec 31, 2022
Rust crate to extend io::Read & io::Write types with progress callbacks

progress-streams Rust crate to provide progress callbacks for types which implement io::Read or io::Write. Examples Reader extern crate progress_strea

Pop!_OS 19 Dec 3, 2022
RiteLinked - LinkedHashMap & LinkedHashSet in Rust

RiteLinked -- HashMap-like containers that hold their key-value pairs in a user controllable order RiteLinked provides more up to date versions of Lin

Rite Database 52 Aug 19, 2022