4 releases

new 0.2.3 Nov 18, 2024
0.2.2 Nov 18, 2024
0.2.1 Nov 18, 2024
0.2.0 Aug 23, 2024

#1301 in Network programming

Apache-2.0

10KB
65 lines

Route Match

A utility for simple and intuitve routing of http requests using macros.

Quick Example

With route_match, you can route a request like this:

use route_match::route;

fn handle_request(reqest: Request) -> Response {
  route! {
    match (&reqest.method().as_str(), &reqest.uri().path()) {
      GET /foo/bar => handle_foo_bar(),
      POST /user/:id => handle_post_user(request, id),
      _ => handle_404_error(request),
    }
  }
}

Goals

HTTP routing should be easy. The HTTP protocol hit version 1.0 in 1996 - it's an incredibly well-trodden technology, and it should be a well-solved problem by now to route a request based on a URL and HTTP method, and extract variables from a URL path.

In my experience, existing routing soutions often encounter several pitfalls which make this harder than it has to be:

  • They're often overly coupled to a single solution. One library might have a great routing solution, but you have to adopt other components of their chosen networking stack if you want to use it. It's not always easy if you want to move your API to a different context, like a cloud function, where you're constrained to use a different Request and Response type.

  • They're often magical, and due to heavy use of traits, can be hard to understand, can give confusing error messages, and can take time to learn the "magic words" to make them work.

  • They often come with limitations. When you need to squeeze your solution through a routing interface interface, you might encounter challenges in dealing with lifetimes, or async which were not anticipated or designed around by the library author.

  • They often come with a runtime cost. The router object will often be represented as an object on the heap which has to be iterated over. Admittedly this is a very very minor one, but it doesn't have to be that way.

So route_match aims to solve these problems, and provide a simple-to-use, intuitive, context-agnostic solution for routing HTTP requests in Rust.

The priorities of this project are:

  1. To provide an easy to use, intuitive DSL for routing HTTP requests, through a procedural macro

  2. To let you think about your endpoints in terms of the HTTP protocol rather than Rust syntax

  3. To work in any context where you have an http method and a URL path, regardless of the other tools or the stack you are working with

  4. To focus on one use-case, and to do it well. This libary does not attempt to solve every possible routing scenario under the sun, it aims to handle the most common scenarios while remaining lean, minimal, fast to download and fast to build.

Usage

Here's an example of a simple match statement:

fn match_route(method: &str, path: &str) -> boolean {
    route! {
      match (method, path) {
        GET /foo => true,
        _ => false,
      }
    }
}

match_route("GET", "/foo") // returns true
match_route("GET", "foo") // also true - the leading '/' is optional
match_route("POST", "/foo") // returns false - the method must match
match_route("GET", "/bar") // returns false - the path must match
match_route("GET", "/foo/bar") // returns false - the uri path must match completely

The provided method and path will be checked in order, and the first matching branch will be executed. So for instance for a match expression like this:

fn match_route(method: &str, path: &str) {
    route! {
      match (method, path) {
        _ /foo -> println!("ANY /foo"),
        GET /foo => println!("GET /foo"),
        _ => println!("default"),
      }
    }
}

match_route("GET", "/foo") // prints "ANY /foo"

the GET /foo condition will never be executed, because _ /foo matches the GET condition as well.

Also note, the default _ branch must always be provided.

URL Path Parameters

We can also extract url parameters by inserting the pattern :var_name in the path pattern:

fn match_route(method: &str, path: &str) -> boolean {
    route! {
      match (method, path) {
        GET /user/:id => println!("user id: {}", id),
        _ => println!("default"),
      }
    }
}

match_route("GET /user/456") // prints: "GET /user/456"

Here the id parameter is passed to the branch expression, as an &str. The lifetime of the parameter is the same as the lifetime of the path argument which is passed to the match expression.

Wildcard matches

Sometimes we want to ignore part of a pattern and match inclusively

fn match_route(method: &str, path: &str) -> boolean {
    route! {
      match (method, path) {
        // Match any method, so long as the path matches "/foo"
        _ /foo => println!("Any /foo"),
        // Match any request with the method "OPTIONS"
        OPTIONS _ => println!("OPTIONS"),
        // Mathc any path starting with "/foo/bar"
        // Here "rest" will be bound as an &str containing everything
        // in the path following "/foo/bar"
        GET /foo/bar/*:rest => println("rest: {}", rest),
        // Match any method/path combination at all
        _ => println!("default"),
      }
    }
}

Grammar

The route macro provides a match expression, which lets you match against HTTP methods and uri patterns.

The match statement takes the form:

match_stmnt : match ( , ) { } method: Expression path: Expression

Here the method and path arguments can be any expression which has the type &str.

branches expands to the following:

branches : , ? | ? branch : => Expression

pattern : | _ method : GET | HEAD | POST | PUT | DELETE | CONNECT | OPTIONS | TRACE | PATCH | _ uri : <uri_components> | " <uri_components> " | _

uri_components : / <uri_component> <uri_components>? | / <uri_component>? uri_component : IDENTIFIER | : IDENTIFIER

Runtime Specification

At runtime, the match statement executes the first branch expression, such that the method and path provided match the branch pattern.

Dependencies

~235–680KB
~16K SLoC