├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── basic.rs ├── web-basic.rs ├── web-hyper-async.rs └── web-hyper-sync.rs ├── src ├── capture.rs ├── extensions │ ├── http.rs │ └── mod.rs ├── lib.rs ├── matcher.rs ├── node.rs ├── parser.rs └── router.rs └── tests ├── capture_test.rs ├── matcher_test.rs ├── parser_test.rs └── router_test.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Rust (${{ matrix.rust }}) (${{ matrix.os }}) 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - macos-latest 18 | - ubuntu-latest 19 | - windows-latest 20 | rust: 21 | - stable 22 | - beta 23 | - nightly 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: ${{ matrix.rust }} 32 | override: true 33 | components: rustfmt, clippy 34 | 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: build 38 | 39 | - uses: actions-rs/cargo@v1 40 | with: 41 | command: test 42 | 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: fmt 46 | args: --all -- --check 47 | 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: clippy 51 | args: --all --all-features --profile test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /tarpaulin-report.html 4 | /**/*.rs.bk 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "usher" 3 | version = "0.2.1" # remember to update html_root_url 4 | authors = ["Isaac Whitfield "] 5 | description = "Parameterized routing for generic resources in Rust" 6 | repository = "https://github.com/whitfin/usher" 7 | keywords = ["data-structures", "http", "io", "tree", "web-services"] 8 | categories = ["algorithms", "data-structures", "web-programming"] 9 | readme = "README.md" 10 | edition = "2018" 11 | license = "MIT" 12 | 13 | [badges] 14 | travis-ci = { repository = "whitfin/usher" } 15 | 16 | [features] 17 | default = [] 18 | web = ["http"] 19 | 20 | [dependencies] 21 | http = { version = "0.2", optional = true } 22 | 23 | [dev-dependencies] 24 | futures = "0.3" 25 | hyper = { version = "0.14", features = ["full"] } 26 | tokio = { version = "1.19", features = ["full"] } 27 | 28 | [[example]] 29 | name = "web-basic" 30 | required-features = ["web"] 31 | 32 | [[example]] 33 | name = "web-hyper-async" 34 | required-features = ["web"] 35 | 36 | [[example]] 37 | name = "web-hyper-sync" 38 | required-features = ["web"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Isaac Whitfield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Usher 2 | [![Build Status](https://img.shields.io/github/actions/workflow/status/whitfin/usher/rust.yml?branch=main)](https://github.com/whitfin/usher/actions) 3 | [![Crates.io](https://img.shields.io/crates/v/usher.svg)](https://crates.io/crates/usher) 4 | 5 | Usher provides an easy way to construct parameterized routing trees in Rust. 6 | 7 | The nodes of these trees is naturally generic, allowing Usher to lend itself 8 | to a wide variety of use cases. Matching and parameterization rules are defined 9 | by the developer using a simple set of traits, allowing for customization in 10 | the routing algorithm itself. This provides easy support for various contexts 11 | in which routing may be used. 12 | 13 | This project was born of a personal need for something small to sit on top of 14 | [Hyper](https://hyper.rs/), without having to work with a whole framework. Over 15 | time it became clear that it provides utility outside of the HTTP realm, and so 16 | the API was adapted to become more generic. As such, Usher provides several 17 | "extensions" based on certain domains which essentially provide sugar over a 18 | typical router. These extensions are all off by default, but can easily be set 19 | as enabled via Cargo features. 20 | 21 | Prior to v1.0 you can expect the API to receive some changes, although I will 22 | do my best to keep this to a minimum to reduce any churn involved. One choice 23 | that is perhaps going to change is the API around using non-filesystem based 24 | pathing. Other than that expect changes as optimizations (and the likely API 25 | refactoring associated with them) still need to be fully investigated. 26 | 27 | ### Getting Started 28 | 29 | Usher is available on [crates.io](https://crates.io/crates/usher). The easiest 30 | way to use it is to add an entry to your `Cargo.toml` defining the dependency: 31 | 32 | ```toml 33 | [dependencies] 34 | usher = "0.1" 35 | ``` 36 | 37 | If you require any of the Usher extensions, you can opt into them by setting the 38 | feature flags in your dependency configuration: 39 | 40 | ```toml 41 | usher = { version = "0.1", features = ["web"] } 42 | ``` 43 | 44 | You can find the available extensions in the documentation. 45 | 46 | ### Basic Usage 47 | 48 | The construction of a tree is quite simple, depending on what your desired outcome 49 | is. To construct a very basic/static tree, you can simply insert the routes you 50 | care about: 51 | 52 | ```rust 53 | use usher::prelude::*; 54 | 55 | fn main() { 56 | // First we construct our `Router` using a set of parsers. Fortunately for 57 | // this example, Usher includes the `StaticParser` which uses basic string 58 | // matching to determine whether a segment in the path matches. 59 | let mut router: Router = Router::new(vec![ 60 | Box::new(StaticParser), 61 | ]); 62 | 63 | // Then we insert our routes; in this case we're going to store the numbers 64 | // 1, 2 and 3, against their equivalent name in typed English (with a "/" 65 | // as a prefix, as Usher expects filesystem-like paths (for now)). 66 | router.insert("/one", "1".to_string()); 67 | router.insert("/two", "2".to_string()); 68 | router.insert("/three", "3".to_string()); 69 | 70 | // Finally we'll just do a lookup on each path, as well as the a path which 71 | // doesn't match ("/"), just to demonstrate what the return types look like. 72 | for path in vec!["/", "/one", "/two", "/three"] { 73 | println!("{}: {:?}", path, router.lookup(path)); 74 | } 75 | } 76 | ``` 77 | 78 | This will route exactly as it looks; matching each static segment provided against 79 | the tree and retrieving the value associated with the path. The return type of the 80 | `lookup(path)` function is `Option<(&T, Vec<(&str, (usize, usize)>)>`, with `&T` 81 | referring to the generic value provided (`"1"`, etc), and the `Vec` including a set 82 | of any parameters found during routing. In the case of no parameters, this vector 83 | will be empty (as is the case above). 84 | 85 | For usage based around extensions (such as HTTP), please see the documentation for 86 | the module containing it - or visit the examples directory for actual usage. 87 | 88 | ### Advanced Usage 89 | 90 | Of course, for some use cases you need to be able to control more than statically 91 | matching against the path segments. In a web framework, you might allow for some 92 | path segments which match regardless and simply capture their value (i.e. `:id`). 93 | In order to allow this type of usage, there are two traits available in Usher; the 94 | `Parser` and `Matcher` traits. These two traits can be implemented to describe how 95 | to match against specific segments in an incoming path. 96 | 97 | The `Matcher` trait is used to determine if an incoming path segment matches a 98 | configured path segment. It's also responsible for pulling out any capture that 99 | is associated with the incoming segment. The `Parser` trait is used to calculate 100 | which `Matcher` type should be used on a configured path segment. At a glance it 101 | might seem that these two traits could be combined but the difference is that the 102 | `Parser` trait operates at router creation time, whereas the `Matcher` trait exists 103 | for execution when matching against a created router. 104 | 105 | To demonstrate these traits, we can use the `:id` example of a typical web framework. 106 | The concept of this syntax is that it should match any value provided to the tree. 107 | If my router was configured with the path `/:id`, it would match incoming paths of 108 | `/123` and `/abc` (but not `/`). This would provide a captured value `id` which holds 109 | the value `123` or `abc`. 110 | 111 | #### Matcher 112 | 113 | This pattern is pretty simple to implement using the two traits we defined above. 114 | First of all we must construct our `Matcher` type (technically you might write the 115 | `Parser` first, but it's easier to explain in this order). Fortunately, the rules 116 | here are very simple. 117 | 118 | ```rust 119 | /// A `Matcher` type used to match against dynamic segments. 120 | /// 121 | /// The internal value here is the name of the path parameter (based on the 122 | /// example talked through above, this would be the _owned_ `String` of `"id"`). 123 | pub struct DynamicMatcher { 124 | inner: String 125 | } 126 | 127 | impl Matcher for DynamicMatcher { 128 | /// Determines if there is a capture for the incoming segment. 129 | /// 130 | /// In the pattern we described above the entire value becomes the capture, 131 | /// so we return a tuple of `("id", (start, end))` to represent the capture. 132 | fn capture(&self, segment: &str) -> Option<(&str, (usize, usize))> { 133 | Some((&self.inner, (0, segment.len()))) 134 | } 135 | 136 | /// Determines if this matcher matches the incoming segment. 137 | /// 138 | /// Because the segment is dynamic and matches any value, this is able to 139 | /// always return `true` without even looking at the incoming segment. 140 | fn is_match(&self, _segment: &str) -> bool { 141 | true 142 | } 143 | } 144 | ``` 145 | 146 | This implementation is fairly trivial and should be quite self-explanatory; the 147 | matcher matches anything so `is_match/1` will always return true. We always want 148 | to capture the segment, so that's returned from `capture/1`. A couple of things 149 | to mention about captures; 150 | 151 | - An implementation of `capture/1` is option, as it will default to `None`. 152 | - The `capture/1` implementation is only called if `is_match/1` resolved to `true`. 153 | - The tuple structure used for captures is necessary as we need some way to know 154 | the name of the captures at runtime. The names cannot be stored in the router 155 | itself as there may be use cases where the capture name is actually a function 156 | of the incoming path segment (not in this case specifically, of course). 157 | 158 | #### Parser 159 | 160 | Now that we have our `Matcher` type, we need to construct a `Parser` type in 161 | order to associate the configured segments with the correct `Matcher`. This is 162 | pretty trivial in our case, because pretty much the only rule we have is that 163 | the segment must be of the pattern `:.+`, which we can roughly translate to 164 | `starts_with(":")` for example purposes. As such, a `Parser` type might look 165 | like this: 166 | 167 | ```rust 168 | /// A `Parser` type used to parse out `DynamicMatcher` values. 169 | pub struct DynamicParser; 170 | 171 | impl Parser for DynamicParser { 172 | /// Attempts to parse a segment into a corresponding `Matcher`. 173 | /// 174 | /// As a dynamic segment is determined by the pattern `:.+`, we check the first 175 | /// character of the segment. If the segment is not `:` we are unable to parse 176 | /// and so return a `None` value. 177 | /// 178 | /// If it does start with a `:`, we construct a `DynamicMatcher` and pass the 179 | /// parameter name through as it's used when capturing values. 180 | fn parse(&self, segment: &str) -> Option> { 181 | if &segment[0..1] != ":" { 182 | return None; 183 | } 184 | 185 | let field = &segment[1..]; 186 | let matcher = DynamicMatcher { 187 | inner: field.to_owned() 188 | }; 189 | 190 | Some(Box::new(matcher)) 191 | } 192 | } 193 | ``` 194 | 195 | One of the nice things about splitting the traits is that you can switch up the 196 | syntax easily. Although both `DynamicMatcher` and `DynamicParser` are included 197 | in Usher, you might want to use a different syntax. One other example of syntax 198 | for parameters (I think in the Java realm) is `{id}`. To accomodate this case, 199 | you only have to write a new `Parser` implementation; the existing `Matcher` 200 | struct already works! 201 | 202 | ```rust 203 | /// A customer `Parser` type used to parse out `DynamicMatcher` values. 204 | pub struct CustomDynamicParser; 205 | 206 | impl Parser for CustomDynamicParser { 207 | /// Attempts to parse a segment into a corresponding `Matcher`. 208 | /// 209 | /// This will match segments based on `{id}` syntax, rather than `:id`. We have 210 | /// to check the end characters, and pass back the something in the middle! 211 | fn parse(&self, segment: &str) -> Option> { 212 | // has to start with "{" 213 | if &segment[0..1] != "{" { 214 | return None; 215 | } 216 | 217 | // has to end with "}" 218 | if &segment[(len - 1)..] != "}" { 219 | return None; 220 | } 221 | 222 | // so 1..(len - 1) trim the brackets 223 | let field = &segment[1..(len - 1)]; 224 | let matcher = DynamicMatcher::new(field); 225 | 226 | // wrap it up! 227 | Some(Box::new(matcher)) 228 | } 229 | } 230 | ``` 231 | 232 | Of course, this also makes it trivial to match _either_ of the two forms shown 233 | above. You can attach both parsers to your tree at startup, and it will allow 234 | for both `:id` and `{id}`. This flexibility can be definitely be useful when 235 | writing more involved frameworks, using Usher as the underlying routing layer. 236 | 237 | #### Configuration 238 | 239 | Now we have our types, we have to actually configure them in a router in order 240 | for them to take effect. This is done at router initialization time, and you've 241 | already seen an example of this in the basic example where we provide the basic 242 | `StaticParser` type. Much like this example, we pass our parser in directly: 243 | 244 | ```rust 245 | let mut router: Router = Router::new(vec![ 246 | Box::new(DynamicParser), 247 | Box::new(StaticParser), 248 | ]); 249 | ``` 250 | 251 | Using this definition, our new `Parser` will be used to determine if we can parse 252 | dynamic segments from the path. Below is a demonstration of a simple path which 253 | makes use of both matcher types (`S` dictates a static segment, and `D` dictates 254 | a dynamic segment): 255 | 256 | ``` 257 | /api/user/:id 258 | ^ ^ ^ 259 | | | | 260 | S S D 261 | ``` 262 | 263 | Please note that the order the parsers are provided is very important; you should 264 | place the most "specific" parsers first as they are tested in order. If you placed 265 | `StaticParser` first in the list above, then nothing would ever continue through to 266 | the `DynamicParser` as every segment satisfies the `StaticParser` requirements. 267 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use usher::prelude::*; 2 | 3 | fn main() { 4 | // First we construct our `Router` using a set of parsers. Fortunately for 5 | // this example, Usher includes the `StaticParser` which uses basic string 6 | // matching to determine whether a segment in the path matches. 7 | let mut router: Router = Router::new(vec![Box::new(StaticParser)]); 8 | 9 | // Then we insert our routes; in this case we're going to store the numbers 10 | // 1, 2 and 3, against their equivalent name in typed English (with a "/" 11 | // as a prefix, as Usher expects filesystem-like paths (for now)). 12 | router.insert("/one", "1".to_string()); 13 | router.insert("/two", "2".to_string()); 14 | router.insert("/three", "3".to_string()); 15 | 16 | // Finally we'll just do a lookup on each path, as well as the a path which 17 | // doesn't match ("/"), just to demonstrate what the return types look like. 18 | for path in &["/", "/one", "/two", "/three"] { 19 | println!("{}: {:?}", path, router.lookup(path)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/web-basic.rs: -------------------------------------------------------------------------------- 1 | use http::Method; 2 | use usher::http::HttpRouter; 3 | use usher::prelude::*; 4 | 5 | fn main() { 6 | // Just like in a normal Router, we provide our parsers at startup. 7 | let mut router: HttpRouter<()> = 8 | HttpRouter::new(vec![Box::new(DynamicParser), Box::new(StaticParser)]); 9 | 10 | // Then we insert some HTTP routes (note the HTTP method being used as 11 | // the method name for insertion). 12 | router.get("/", ()); 13 | router.get("/status", ()); 14 | router.get("/api/v1/user", ()); 15 | router.post("/api/v1/user", ()); 16 | router.put("/api/v1/user/:id", ()); 17 | 18 | // Fetch some HTTP handlers based on the method/path combinations. If 19 | // the path matches, but the method does not, no handler will be found. 20 | println!("GET /: {:?}", router.handler(&Method::GET, "/")); 21 | println!("GET /status: {:?}", router.handler(&Method::GET, "/status")); 22 | println!("GET /api: {:?}", router.handler(&Method::GET, "/api")); 23 | println!("GET /api/v1: {:?}", router.handler(&Method::GET, "/api/v1")); 24 | println!( 25 | "GET /api/v1/user: {:?}", 26 | router.handler(&Method::GET, "/api/v1/user") 27 | ); 28 | println!( 29 | "PUT /api/v1/user: {:?}", 30 | router.handler(&Method::PUT, "/api/v1/user") 31 | ); 32 | println!( 33 | "POST /api/v1/user: {:?}", 34 | router.handler(&Method::POST, "/api/v1/user") 35 | ); 36 | println!( 37 | "GET /api/v1/user/steve: {:?}", 38 | router.handler(&Method::GET, "/api/v1/user/steve") 39 | ); 40 | println!( 41 | "PUT /api/v1/user/steve: {:?}", 42 | router.handler(&Method::PUT, "/api/v1/user/steve") 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/web-hyper-async.rs: -------------------------------------------------------------------------------- 1 | use hyper::service::{make_service_fn, service_fn}; 2 | use hyper::{Body, Request, Response, Server, StatusCode}; 3 | use usher::capture::find_capture; 4 | use usher::http::HttpRouter; 5 | use usher::prelude::*; 6 | 7 | use std::future::Future; 8 | use std::pin::Pin; 9 | use std::sync::Arc; 10 | 11 | /// This signature is borrowed from the Hyper "echo" example codebase. 12 | type BoxFut = Pin, hyper::Error>> + Send>>; 13 | 14 | /// Represents a boxed function which receives a request/params and returns a response future. 15 | type Callee = Box, Vec<(&str, (usize, usize))>) -> BoxFut + Send + Sync>; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | // Create our address to bind to, localhost:3000 20 | let addr = ([127, 0, 0, 1], 3000).into(); 21 | 22 | // Just like in a normal Router, we provide our parsers at startup. 23 | let mut router: HttpRouter = 24 | HttpRouter::new(vec![Box::new(DynamicParser), Box::new(StaticParser)]); 25 | 26 | // This will echo the provided name back to the caller. 27 | router.get( 28 | "/:name", 29 | Box::new(|req, params| { 30 | let path = req.uri().path(); 31 | let name = find_capture(path, ¶ms, "name").unwrap(); 32 | 33 | let body = format!("Hello, {}!\n", name).into(); 34 | let resp = Response::new(body); 35 | 36 | Box::pin(async move { Ok(resp) }) 37 | }), 38 | ); 39 | 40 | // Wrap inside an Arc to avoid large clones. 41 | let router = Arc::new(router); 42 | 43 | // Construct our Hyper server. 44 | let server = make_service_fn(move |_conn| { 45 | // We need a "clone" of the router. 46 | let router = router.clone(); 47 | 48 | async { 49 | // Construct a Hyper service from a function which turns a request 50 | // into an asynchronous response (which comes from echo()). 51 | let server = service_fn(move |req: Request| { 52 | // We need a "clone" of the router. 53 | let router = router.clone(); 54 | 55 | async move { 56 | // First we need to extract the method and path to use for the 57 | // actual handler lookup as it uses a combination of both values. 58 | let method = req.method(); 59 | let path = req.uri().path(); 60 | 61 | // Then we delegate to a hander when possible. 62 | match router.handler(method, path) { 63 | // In this case, invoke the handler and pass through the 64 | // request instance and the captures associated with it. 65 | // 66 | // In this case we pass through the captures as they're 67 | // generated by default, but this might be where you wish 68 | // to turn them into something like a `HashMap` for access. 69 | Some((handler, captures)) => handler(req, captures).await, 70 | 71 | // If no handler matches, we generate a 404 response to 72 | // state so back to the caller. This happens when either 73 | // the HTTP method or the path is wrong during matching. 74 | None => { 75 | let mut response = Response::new(Body::empty()); 76 | *response.status_mut() = StatusCode::NOT_FOUND; 77 | Ok(response) 78 | } 79 | } 80 | } 81 | }); 82 | 83 | // pass back the server value 84 | Ok::<_, hyper::Error>(server) 85 | } 86 | }); 87 | 88 | // Log the port we're listening on so we don't forget! 89 | println!("Listening on http://{}", addr); 90 | 91 | // Initialze the actual service. 92 | Server::bind(&addr).serve(server).await?; 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /examples/web-hyper-sync.rs: -------------------------------------------------------------------------------- 1 | use hyper::service::{make_service_fn, service_fn}; 2 | use hyper::{Body, Request, Response, Server, StatusCode}; 3 | use usher::capture::find_capture; 4 | use usher::http::HttpRouter; 5 | use usher::prelude::*; 6 | 7 | use std::sync::Arc; 8 | 9 | /// Represents a boxed function which receives a request/params and returns a response. 10 | type Callee = 11 | Box, Vec<(&str, (usize, usize))>) -> Response + Send + Sync>; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<(), Box> { 15 | // Create our address to bind to, localhost:3000 16 | let addr = ([127, 0, 0, 1], 3000).into(); 17 | 18 | // Just like in a normal Router, we provide our parsers at startup. 19 | let mut router: HttpRouter = 20 | HttpRouter::new(vec![Box::new(DynamicParser), Box::new(StaticParser)]); 21 | 22 | // This will echo the provided name back to the caller. 23 | router.get( 24 | "/:name", 25 | Box::new(|req, params| { 26 | let path = req.uri().path(); 27 | let name = find_capture(path, ¶ms, "name").unwrap(); 28 | 29 | Response::new(format!("Hello, {}!\n", name).into()) 30 | }), 31 | ); 32 | 33 | // Wrap inside an Arc to avoid large clones. 34 | let router = Arc::new(router); 35 | 36 | // Construct our Hyper server. 37 | let server = make_service_fn(move |_conn| { 38 | // We need a "clone" of the router. 39 | let router = router.clone(); 40 | 41 | async { 42 | // Construct a Hyper service from a function which turns a request 43 | // into an asynchronous response (which comes from echo()). 44 | let server = service_fn(move |req: Request| { 45 | // We need a "clone" of the router. 46 | let router = router.clone(); 47 | 48 | async move { 49 | // First we need to extract the method and path to use for the 50 | // actual handler lookup as it uses a combination of both values. 51 | let method = req.method(); 52 | let path = req.uri().path(); 53 | 54 | // Then we delegate to a hander when possible. 55 | let response = match router.handler(method, path) { 56 | // In this case, invoke the handler and pass through the 57 | // request instance and the captures associated with it. 58 | // 59 | // In this case we pass through the captures as they're 60 | // generated by default, but this might be where you wish 61 | // to turn them into something like a `HashMap` for access. 62 | Some((handler, captures)) => handler(req, captures), 63 | 64 | // If no handler matches, we generate a 404 response to 65 | // state so back to the caller. This happens when either 66 | // the HTTP method or the path is wrong during matching. 67 | None => { 68 | let mut response = Response::new(Body::empty()); 69 | *response.status_mut() = StatusCode::NOT_FOUND; 70 | response 71 | } 72 | }; 73 | 74 | // pass it back to the service_fn 75 | Ok::<_, hyper::Error>(response) 76 | } 77 | }); 78 | 79 | // pass back the server value 80 | Ok::<_, hyper::Error>(server) 81 | } 82 | }); 83 | 84 | // Log the port we're listening on so we don't forget! 85 | println!("Listening on http://{}", addr); 86 | 87 | // Initialze the actual service. 88 | Server::bind(&addr).serve(server).await?; 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /src/capture.rs: -------------------------------------------------------------------------------- 1 | //! Types and traits related to parameter capturing. 2 | //! 3 | //! Currently this module is a placeholder for new traits coming later; the 4 | //! only things stored in this module at this point are simple type aliases 5 | //! (which are extremely likely to change in future, so don't rely on them). 6 | 7 | /// Basic type alias for a captured value pair. 8 | pub type Capture<'a> = (&'a str, (usize, usize)); 9 | 10 | /// Alias type for a set of multiple `Capture` values. 11 | pub type Captures<'a> = Vec>; 12 | pub type CapturesRef<'a> = &'a [Capture<'a>]; 13 | 14 | /// Retrieves a potential captured value from a parameter set by name. 15 | /// 16 | /// This function uses the provided path and captures to locate a value set against 17 | /// the provided name. If multiple values exist, only the first value will be found. 18 | /// 19 | /// This function will panic if the bounds provided are invalid for the provided path, 20 | /// although this should never happen in reality unless you're mocking captures. 21 | #[inline] 22 | pub fn find_capture<'a, 'p>(path: &'p str, capt: CapturesRef<'a>, name: &str) -> Option<&'p str> { 23 | capt.iter() 24 | .find(|(n, _)| *n == name) 25 | .map(|capt| lookup_capture(path, *capt)) 26 | } 27 | 28 | /// Retrieves a captured value from a path. 29 | /// 30 | /// This function will panic if the bounds provided are invalid for the provided path, 31 | /// although this should never happen in reality unless you're mocking captures. 32 | #[inline] 33 | pub fn lookup_capture<'a, 'p>(path: &'p str, capt: Capture<'a>) -> &'p str { 34 | &path[((capt.1).0)..((capt.1).1)] 35 | } 36 | -------------------------------------------------------------------------------- /src/extensions/http.rs: -------------------------------------------------------------------------------- 1 | //! HTTP routing components based on an Usher `Router`. 2 | //! 3 | //! This module exposes the `HttpRouter` structure, which represents a 4 | //! generic `Router` in a more HTTP appropriate way. Rather than the 5 | //! typical insertion functions, this router exposes HTTP verbs as the 6 | //! names of functions to provide a simple API for mapping HTTP requests. 7 | //! 8 | //! To activate this extension, use the `"web"` Cargo feature. 9 | use http::Method; 10 | 11 | use std::collections::HashMap; 12 | 13 | use crate::capture::Captures; 14 | use crate::parser::Parser; 15 | use crate::router::Router; 16 | 17 | /// A basic HTTP routing structure for generic handlers. 18 | /// 19 | /// Almost all internals of this router are controlled by the usual `Router`, 20 | /// with this structure simply providing a more HTTP friendly API for ergonomics. 21 | /// 22 | /// To construct a router this way, HTTP verbs must be used as there must be a 23 | /// verb associated with each request. There is currently no way to match any 24 | /// verb, although this will potentially be improved at some point in future. 25 | pub struct HttpRouter { 26 | router: Router>, 27 | } 28 | 29 | /// Delegates a HTTP method to the `route` method in a router. 30 | macro_rules! http_delegate { 31 | ($name:ident, $method:expr, $smethod:expr) => { 32 | #[inline(always)] 33 | #[doc = "Registers a handler for the `"] 34 | #[doc = $smethod] 35 | #[doc = "` HTTP method."] 36 | pub fn $name(&mut self, path: &str, t: T) { 37 | self.insert($method, path, t) 38 | } 39 | }; 40 | } 41 | 42 | impl HttpRouter { 43 | /// Creates a new `Router` with provided matchers. 44 | pub fn new(parsers: Vec>) -> Self { 45 | Self { 46 | router: Router::new(parsers), 47 | } 48 | } 49 | 50 | // Automatic HTTP method delegates. 51 | http_delegate!(connect, Method::CONNECT, "CONNECT"); 52 | http_delegate!(delete, Method::DELETE, "DELETE"); 53 | http_delegate!(get, Method::GET, "GET"); 54 | http_delegate!(head, Method::HEAD, "HEAD"); 55 | http_delegate!(options, Method::OPTIONS, "OPTIONS"); 56 | http_delegate!(patch, Method::PATCH, "PATCH"); 57 | http_delegate!(post, Method::POST, "POST"); 58 | http_delegate!(put, Method::PUT, "PUT"); 59 | http_delegate!(trace, Method::TRACE, "TRACE"); 60 | 61 | /// Inserts a route/handler pair for the provided method and path. 62 | fn insert(&mut self, method: Method, path: &str, t: T) { 63 | self.router.update(path, |node| { 64 | let mut map = node.unwrap_or_default(); 65 | if !map.contains_key(&method) { 66 | map.reserve(1); 67 | } 68 | map.insert(method, t); 69 | map 70 | }); 71 | } 72 | 73 | /// Attempts to route a method/path combination to a handler. 74 | /// 75 | /// If a handler exists for the provided method/path combination, it will 76 | /// be returned - along with any captures found during matching. If the 77 | /// path does not exist, or the method is not available on the path, a 78 | /// `None` value will be returned and a handler will not be found. 79 | pub fn handler<'a>(&'a self, method: &Method, path: &str) -> Option<(&T, Captures<'a>)> { 80 | // look for the node in the router based on the path 81 | self.router.lookup(path).and_then(|(node, captures)| { 82 | // unpack the method and map the handler back directly 83 | node.get(method).map(|handler| (handler, captures)) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extension implementations based on top of a `Router`. 2 | //! 3 | //! These structures are applicable in different circumstances, depending on 4 | //! what the developer is trying to do. Each extension is disabled by default 5 | //! and opt-in via build features. Whether these extensions live in this crate 6 | //! in future is yet to be determined; so be prepared for the possibility that 7 | //! this module disappear at some point in future (prior to v1.0). 8 | //! 9 | //! See the documentation for each extension to find the feature necessary to 10 | //! include the module. In general the name of the extension will match the 11 | //! feature, but this isn't always possible due to some Cargo behaviour. 12 | #[cfg(feature = "web")] 13 | pub mod http; 14 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Parameterized routing for generic resources in Rust. 2 | #![doc(html_root_url = "https://docs.rs/usher/0.2.1")] 3 | 4 | // exposed modules 5 | pub mod capture; 6 | pub mod matcher; 7 | pub mod node; 8 | pub mod parser; 9 | pub mod router; 10 | 11 | // lift extensions 12 | mod extensions; 13 | pub use extensions::*; 14 | 15 | // prelude module 16 | pub mod prelude { 17 | //! A "prelude" for crates using the `usher` crate. 18 | //! 19 | //! This prelude contains the required imports for almost all use cases, to 20 | //! avoid having to include modules and structures directly: 21 | //! 22 | //! ```rust 23 | //! use usher::prelude::*; 24 | //! ``` 25 | //! 26 | //! The prelude may grow over time, but it is unlikely to shrink. 27 | pub use super::matcher::Matcher; 28 | pub use super::parser::{DynamicParser, Parser, StaticParser}; 29 | pub use super::router::Router; 30 | } 31 | -------------------------------------------------------------------------------- /src/matcher.rs: -------------------------------------------------------------------------------- 1 | //! Matchers used to compare against incoming segments. 2 | //! 3 | //! Values of type `Matcher` are stored inside a tree and used to match 4 | //! against incoming segments in order to walk through the tree correctly. 5 | use crate::capture::Capture; 6 | 7 | /// Matching trait to enable generic route matching algorithms. 8 | /// 9 | /// This trait backs the main tree, enabling custom segment matching based 10 | /// on the needs of the end developer. In many cases it's wasteful to check 11 | /// for things like RegEx, especially when all routes will only be static 12 | /// (as an example). 13 | pub trait Matcher: Send + Sync { 14 | /// Retrieves a potential capture from a segment. 15 | fn capture<'a>(&'a self, _segment: &str) -> Option> { 16 | None 17 | } 18 | 19 | /// Determines whether an incoming segment is a match for a base segment. 20 | fn is_match(&self, segment: &str) -> bool; 21 | } 22 | 23 | /// Blanket implementation of `Matcher` for pure functions. 24 | /// 25 | /// Pure functions are assumed to not have a capture group, as there's no 26 | /// way to directly name them at this point (unless derived from the input). 27 | impl Matcher for F 28 | where 29 | F: Fn(&str) -> bool + Send + Sync, 30 | { 31 | /// Determines whether an incoming segment is a match for a base segment. 32 | fn is_match(&self, segment: &str) -> bool { 33 | self(segment) 34 | } 35 | } 36 | 37 | /// Static path segment matcher. 38 | /// 39 | /// This struct is constructed via the `StaticParser` and compares incoming 40 | /// segments directly against the internal static `String` segment. 41 | pub struct StaticMatcher { 42 | inner: String, 43 | } 44 | 45 | impl StaticMatcher { 46 | /// Constructs a new `StaticMatcher` from a segment. 47 | pub fn new>(s: S) -> Self { 48 | Self { inner: s.into() } 49 | } 50 | } 51 | 52 | impl Matcher for StaticMatcher { 53 | /// Compares an incoming segment against a literal base segment. 54 | fn is_match(&self, segment: &str) -> bool { 55 | self.inner == segment 56 | } 57 | } 58 | 59 | /// Dynamic path segment matcher. 60 | /// 61 | /// This struct is constructed via the `DynamicParser` and assumes that any 62 | /// incoming path segment is a candidate for matching. 63 | pub struct DynamicMatcher { 64 | inner: String, 65 | } 66 | 67 | impl DynamicMatcher { 68 | /// Constructs a new `DynamicMatcher` from a segment. 69 | pub fn new>(s: S) -> Self { 70 | Self { inner: s.into() } 71 | } 72 | } 73 | 74 | impl Matcher for DynamicMatcher { 75 | /// Determines if there is a capture for the incoming segment. 76 | fn capture<'a>(&'a self, segment: &str) -> Option> { 77 | Some((&self.inner, (0, segment.len()))) 78 | } 79 | 80 | /// Determines if this matcher matches the incoming segment. 81 | fn is_match(&self, _segment: &str) -> bool { 82 | true 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | //! Nodes to represent the internal structure of a router. 2 | use super::matcher::Matcher; 3 | 4 | /// Node structure to represent the internal structure of a router. 5 | /// 6 | /// A router is simply a `Node` which doesn't have any parent nodes, 7 | /// which allows for the recursive structure of the tree. Each 8 | /// `Node` can have a value of the generic type, which is the value 9 | /// returned when routing occurs. 10 | /// 11 | /// Every `Node` also has an associated `Matcher` which is used 12 | /// to test for compatibility when routing (because not every node 13 | /// is applicable on a given segment order). This `Matcher` is 14 | /// automatically provided to the `Node` at creation time and is 15 | /// calculated by the routing system. 16 | /// 17 | /// Lastly, a `Node` can have child instances to represent the 18 | /// recursive structure of a router. These children are stored in 19 | /// a `Vec` as there's currently no logical way to index them into 20 | /// a more suitable structure. If a `Node` has no children, the 21 | /// containing vector does not require any memory allocation. Any 22 | /// memory will be allocated lazily, and should remain minimal in 23 | /// most standard cases (as it depends on the allocator in use). 24 | pub struct Node { 25 | value: Option, 26 | matcher: Box, 27 | children: Vec>, 28 | } 29 | 30 | impl Node { 31 | /// Constructs a new `Node` from a literal. 32 | pub(crate) fn new(matcher: Box) -> Self { 33 | Self { 34 | matcher, 35 | value: None, 36 | children: Vec::new(), 37 | } 38 | } 39 | 40 | /// Registers a child node inside this node. 41 | pub(crate) fn add_child(&mut self, child: Node) { 42 | self.children.reserve_exact(1); 43 | self.children.push(child); 44 | } 45 | 46 | /// Retrieves a reference to the children of this node. 47 | pub(crate) fn children(&self) -> &[Node] { 48 | &self.children 49 | } 50 | 51 | /// Retrieves a mutable reference to the children of this node. 52 | pub(crate) fn children_mut(&mut self) -> &mut [Node] { 53 | &mut self.children 54 | } 55 | 56 | /// Retrieves the matching struct for this node. 57 | pub(crate) fn matcher(&self) -> &dyn Matcher { 58 | &*self.matcher 59 | } 60 | 61 | /// Updates the inner value of this routing 62 | pub(crate) fn update(&mut self, f: F) 63 | where 64 | F: FnOnce(Option) -> T, 65 | { 66 | let t = f(self.value.take()); 67 | self.value.replace(t); 68 | } 69 | 70 | /// Retrieves a potential handler associated with the provided method. 71 | pub(crate) fn value(&self) -> Option<&T> { 72 | self.value.as_ref() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | //! Parsers used to create `Matcher` values from segments. 2 | //! 3 | //! Values of type `Parser` are required to construct `Matcher` values 4 | //! at tree creation time, to specify priority order when routing an 5 | //! incoming set of segments. A parser can also be a pure function which 6 | //! can derive a potential `Matcher` from an input segment directly. 7 | use crate::matcher::{DynamicMatcher, Matcher, StaticMatcher}; 8 | 9 | /// Parsing trait to enable conversion from literals into matchers. 10 | /// 11 | /// This is used to run through a cascading parsing flow to enable custom 12 | /// matchers being implemented. This trait enables all segment matching to 13 | /// be determined at creation time to avoid any costs at routing time. 14 | pub trait Parser: Send + Sync { 15 | /// Attempts to parse a `Matcher` out of a segment. 16 | fn parse(&self, segment: &str) -> Option>; 17 | } 18 | 19 | /// Blanket implementation of `Parser` for pure functions. 20 | impl Parser for F 21 | where 22 | F: Fn(&str) -> Option> + Send + Sync, 23 | { 24 | /// Attempts to parse a `Matcher` out of a segment. 25 | fn parse(&self, segment: &str) -> Option> { 26 | self(segment) 27 | } 28 | } 29 | 30 | /// Segment parser to generate static route matchers. 31 | pub struct StaticParser; 32 | 33 | /// `Parser` implementation for the static matcher. 34 | impl Parser for StaticParser { 35 | /// Parses out a static matcher from a segment literal. 36 | /// 37 | /// Note that although this returns a result, it will never fail 38 | /// as every string literal can be treated as a static matcher. 39 | fn parse(&self, segment: &str) -> Option> { 40 | Some(Box::new(StaticMatcher::new(segment))) 41 | } 42 | } 43 | 44 | /// Segment parser to generate dynamic router matchers. 45 | pub struct DynamicParser; 46 | 47 | impl Parser for DynamicParser { 48 | /// Parses out a dynamic segment based on the `:.+` syntax. 49 | /// 50 | /// If you wish to use a custom syntax, you can construct a custom `Parser` 51 | /// implementation which constructs a `DynamicMatcher` instance. 52 | fn parse(&self, segment: &str) -> Option> { 53 | if &segment[0..1] != ":" || segment.len() == 1 { 54 | return None; 55 | } 56 | 57 | let field = &segment[1..]; 58 | let matcher = DynamicMatcher::new(field); 59 | 60 | Some(Box::new(matcher)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/router.rs: -------------------------------------------------------------------------------- 1 | //! Routing abstractions based on custom rulesets. 2 | //! 3 | //! Although the behaviour implemented in this module is quite minimal, it 4 | //! should be applicable in many use cases due to the generic typing and 5 | //! tries to avoid tying itself to any particular mental model (except for 6 | //! a generic tree structure). The structures in this module can be used 7 | //! directly, but would typically provide more value as the underlying 8 | //! routing for more domain oriented structures. 9 | use crate::capture::Captures; 10 | use crate::matcher::Matcher; 11 | use crate::node::Node; 12 | use crate::parser::Parser; 13 | 14 | /// Routing structure providing routing for generic types. 15 | /// 16 | /// A `Router` is constructed from a set of `Parser` values, which are used to 17 | /// construct the shape of the structure internally. During construction of a 18 | /// `Router`, these parsers are used to turn path segments into `Matcher` types 19 | /// which are used in routing to calculate the traversal of the tree. 20 | /// 21 | /// The order of the provided `Parser` values is important as it defines the order 22 | /// they're checked against a path segment. If a parser matching any segment is 23 | /// placed first, it will always match and short circuit before checking any other 24 | /// provided parsers. Always put the "strictest" parsers first in the vector. 25 | pub struct Router { 26 | root: Node, 27 | parsers: Vec>, 28 | } 29 | 30 | impl Router { 31 | /// Creates a new `Router`, using the provided matchers. 32 | pub fn new(mut parsers: Vec>) -> Self { 33 | parsers.shrink_to_fit(); 34 | 35 | let parsed = parse_segment(&parsers, "/"); 36 | let parsed = parsed.expect("unparsed segment"); 37 | 38 | Self { 39 | parsers, 40 | root: Node::new(parsed), 41 | } 42 | } 43 | 44 | /// Inserts a route/handler pair for the provided path and method. 45 | /// 46 | /// Internally this is pretty similar to `update`, except that it guarantees 47 | /// that the provided value `t` is stored as the leaf value. If the leaf already 48 | /// contains a value, it will be overwritten. If this is not desired, you can 49 | /// likely implement the insertion easily via `update` instead. 50 | #[inline(always)] 51 | pub fn insert(&mut self, path: &str, t: T) { 52 | self.update(path, |_| t) 53 | } 54 | 55 | /// Attempts to route a path to a leaf value. 56 | /// 57 | /// This function will also capture any parameters involved in routing, into a 58 | /// `Vec` which is returned inside the containing `Option`. Each capture consists 59 | /// of a name and bounds of a value, to help identify the matched parameter. Whilst 60 | /// this is easily determined as the vector is ordered, it's helpful for those who 61 | /// wish to turn captures into a map-like structure afterward. 62 | /// 63 | /// Index bounds are used over path references to avoid lifetime requirements on 64 | /// the path itself, which can cause problems when working in certain contexts. At 65 | /// some point in future, Usher will provide APIs to turn these index captures into 66 | /// friendlier containers - but as this is the lowest cost for a default, it makes 67 | /// sense for now. 68 | /// 69 | /// If a route does not require any parameters, this vector is still returned but 70 | /// is empty. This isn't a big deal; a `Vec` will only allocate memory when you 71 | /// first push something into it in most cases, so the performance hit is minimal. 72 | pub fn lookup<'a>(&'a self, path: &str) -> Option<(&T, Captures<'a>)> { 73 | let offset = path.as_ptr() as usize; 74 | let mut current = &self.root; 75 | let mut captures = Vec::new(); 76 | 77 | for segment in path.split('/').filter(|s| !s.is_empty()) { 78 | current = current 79 | .children() 80 | .iter() 81 | .find(|child| child.matcher().is_match(segment))?; 82 | 83 | let matcher = current.matcher(); 84 | let capture = matcher.capture(segment); 85 | 86 | if let Some((name, (start, end))) = capture { 87 | let ptr = segment.as_ptr() as usize - offset; 88 | let val = (ptr + start, ptr + end); 89 | 90 | captures.push((name, val)); 91 | } 92 | } 93 | 94 | current.value().map(|handler| (handler, captures)) 95 | } 96 | 97 | /// Updates a leaf node inside a `Router`. 98 | /// 99 | /// If the node does not currently exist, it will be built out and populated 100 | /// with the result of the update function (which can be used to generate a 101 | /// value for first insertion). 102 | pub fn update(&mut self, path: &str, f: F) 103 | where 104 | F: FnOnce(Option) -> T, 105 | { 106 | let mut current = &mut self.root; 107 | 108 | for segment in path.split('/').filter(|s| !s.is_empty()) { 109 | let child = current 110 | .children() 111 | .iter() 112 | .find(|child| child.matcher().is_match(segment)); 113 | 114 | if child.is_none() { 115 | let parsed = parse_segment(&self.parsers, segment); 116 | let parsed = parsed.expect("unparsed segment"); 117 | let router = Node::new(parsed); 118 | 119 | current.add_child(router); 120 | } 121 | 122 | current = current 123 | .children_mut() 124 | .iter_mut() 125 | .find(|child| child.matcher().is_match(segment)) 126 | .unwrap(); 127 | } 128 | 129 | current.update(f); 130 | } 131 | } 132 | 133 | /// Attempts to parse a `Matcher` based on the provided segment literal. 134 | /// 135 | /// All provided parsers will be tested (in order) against the input segment to enable 136 | /// passing the most "specific" parsers earlier in the chain. In the case a `Matcher` 137 | /// is found, this function will short circuit and pass the first matcher back to the caller. 138 | fn parse_segment(parsers: &[Box], segment: &str) -> Option> { 139 | parsers.iter().find_map(|parser| parser.parse(segment)) 140 | } 141 | -------------------------------------------------------------------------------- /tests/capture_test.rs: -------------------------------------------------------------------------------- 1 | pub mod capture { 2 | use usher::capture::*; 3 | 4 | #[test] 5 | fn finding_captures() { 6 | let path = "/api/v1/user/123"; 7 | let captures = vec![("vsn", (5, 7)), ("type", (8, 12)), ("id", (13, 16))]; 8 | 9 | let id = find_capture(path, &captures, "id"); 10 | assert_eq!(id, Some("123")); 11 | 12 | let object = find_capture(path, &captures, "type"); 13 | assert_eq!(object, Some("user")); 14 | 15 | let version = find_capture(path, &captures, "vsn"); 16 | assert_eq!(version, Some("v1")); 17 | 18 | let missing = find_capture(path, &captures, "missing"); 19 | assert_eq!(missing, None); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/matcher_test.rs: -------------------------------------------------------------------------------- 1 | pub mod matcher { 2 | use usher::matcher::*; 3 | 4 | #[test] 5 | fn static_matching() { 6 | let matcher = StaticMatcher::new("value"); 7 | 8 | assert!(matcher.is_match("value")); 9 | assert!(!matcher.is_match("not-value")); 10 | 11 | assert_eq!(matcher.capture("value"), None); 12 | assert_eq!(matcher.capture("not-value"), None); 13 | } 14 | 15 | #[test] 16 | fn dynamic_matching() { 17 | let matcher = DynamicMatcher::new("field"); 18 | 19 | assert!(matcher.is_match("value")); 20 | assert!(matcher.is_match("not-value")); 21 | 22 | assert_eq!(matcher.capture("value"), Some(("field", (0, 5)))); 23 | assert_eq!(matcher.capture("not-value"), Some(("field", (0, 9)))); 24 | } 25 | 26 | #[test] 27 | fn closure_matching() { 28 | let matcher = |input: &str| input == "value"; 29 | 30 | assert!(matcher.is_match("value")); 31 | assert!(!matcher.is_match("not-value")); 32 | 33 | assert_eq!(matcher.capture("value"), None); 34 | assert_eq!(matcher.capture("not-value"), None); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/parser_test.rs: -------------------------------------------------------------------------------- 1 | pub mod parser { 2 | use usher::matcher::*; 3 | use usher::parser::*; 4 | 5 | #[test] 6 | fn static_parsing() { 7 | assert!(StaticParser.parse("anything").is_some()); 8 | } 9 | 10 | #[test] 11 | fn dynamic_parsing() { 12 | assert!(DynamicParser.parse("nah").is_none()); 13 | assert!(DynamicParser.parse(":id").is_some()); 14 | } 15 | 16 | #[test] 17 | fn closure_parsing() { 18 | assert!(create_static_matcher.parse("anything").is_some()); 19 | } 20 | 21 | fn create_static_matcher(input: &str) -> Option> { 22 | Some(Box::new(StaticMatcher::new(input))) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/router_test.rs: -------------------------------------------------------------------------------- 1 | pub mod router { 2 | use usher::prelude::*; 3 | 4 | #[test] 5 | fn basic_routing() { 6 | let mut router: Router = Router::new(vec![Box::new(StaticParser)]); 7 | 8 | router.insert("/", 0); 9 | router.insert("/1", 1); 10 | router.insert("/2", 2); 11 | router.insert("/3", 3); 12 | 13 | let n0 = router.lookup("/"); 14 | let n1 = router.lookup("/1"); 15 | let n2 = router.lookup("/2"); 16 | let n3 = router.lookup("/3"); 17 | let n4 = router.lookup("/4"); 18 | 19 | assert_eq!(n0, Some((&0, vec![]))); 20 | assert_eq!(n1, Some((&1, vec![]))); 21 | assert_eq!(n2, Some((&2, vec![]))); 22 | assert_eq!(n3, Some((&3, vec![]))); 23 | assert_eq!(n4, None); 24 | } 25 | 26 | #[test] 27 | fn nested_routing() { 28 | let mut router: Router = Router::new(vec![Box::new(StaticParser)]); 29 | 30 | router.insert("/number/1", 1); 31 | router.insert("/number/2", 2); 32 | router.insert("/number/3", 3); 33 | 34 | let n1 = router.lookup("/number/1"); 35 | let n2 = router.lookup("/number/2"); 36 | let n3 = router.lookup("/number/3"); 37 | let n4 = router.lookup("/number/4"); 38 | 39 | assert_eq!(n1, Some((&1, vec![]))); 40 | assert_eq!(n2, Some((&2, vec![]))); 41 | assert_eq!(n3, Some((&3, vec![]))); 42 | assert_eq!(n4, None); 43 | } 44 | 45 | #[test] 46 | fn captured_routing() { 47 | let mut router: Router<()> = 48 | Router::new(vec![Box::new(DynamicParser), Box::new(StaticParser)]); 49 | 50 | router.insert("/:id", ()); 51 | 52 | let n1 = router.lookup("/1"); 53 | let n2 = router.lookup("/1/1"); 54 | let n3 = router.lookup("/"); 55 | 56 | assert_eq!(n1, Some((&(), vec![("id", (1, 2))]))); 57 | assert_eq!(n2, None); 58 | assert_eq!(n3, None); 59 | } 60 | } 61 | --------------------------------------------------------------------------------