4 releases

0.2.1 Jun 27, 2022
0.2.0 Feb 21, 2020
0.1.1 Apr 28, 2019
0.1.0 Apr 3, 2019

#1669 in Web programming

MIT license

34KB
257 lines

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.

Dependencies

~125KB