{"id":13439143,"url":"https://github.com/whitfin/usher","last_synced_at":"2025-04-19T14:09:27.597Z","repository":{"id":57671339,"uuid":"164033958","full_name":"whitfin/usher","owner":"whitfin","description":"Parameterized routing for generic resources in Rust","archived":false,"fork":false,"pushed_at":"2024-01-03T23:07:18.000Z","size":60,"stargazers_count":36,"open_issues_count":4,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T08:25:09.262Z","etag":null,"topics":["algorithms","data-structures","routing-tables","web-services"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/whitfin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-01-03T23:35:10.000Z","updated_at":"2024-08-28T22:54:24.000Z","dependencies_parsed_at":"2024-01-07T05:55:35.731Z","dependency_job_id":"b3d35e7e-ea95-4ebe-bd9d-f3b419b64e8a","html_url":"https://github.com/whitfin/usher","commit_stats":{"total_commits":57,"total_committers":2,"mean_commits":28.5,"dds":0.01754385964912286,"last_synced_commit":"889884a0589e200140245577a57dafeb7fe1f667"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whitfin%2Fusher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whitfin%2Fusher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whitfin%2Fusher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whitfin%2Fusher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/whitfin","download_url":"https://codeload.github.com/whitfin/usher/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249709410,"owners_count":21313948,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithms","data-structures","routing-tables","web-services"],"created_at":"2024-07-31T03:01:11.505Z","updated_at":"2025-04-19T14:09:27.547Z","avatar_url":"https://github.com/whitfin.png","language":"Rust","funding_links":[],"categories":["Libraries","库 Libraries","库"],"sub_categories":["Data structures","数据结构 Data structures","数据结构"],"readme":"# Usher\n[![Build Status](https://img.shields.io/github/actions/workflow/status/whitfin/usher/rust.yml?branch=main)](https://github.com/whitfin/usher/actions) \n[![Crates.io](https://img.shields.io/crates/v/usher.svg)](https://crates.io/crates/usher)\n\nUsher provides an easy way to construct parameterized routing trees in Rust.\n\nThe nodes of these trees is naturally generic, allowing Usher to lend itself\nto a wide variety of use cases. Matching and parameterization rules are defined\nby the developer using a simple set of traits, allowing for customization in\nthe routing algorithm itself. This provides easy support for various contexts\nin which routing may be used.\n\nThis project was born of a personal need for something small to sit on top of\n[Hyper](https://hyper.rs/), without having to work with a whole framework. Over\ntime it became clear that it provides utility outside of the HTTP realm, and so\nthe API was adapted to become more generic. As such, Usher provides several\n\"extensions\" based on certain domains which essentially provide sugar over a\ntypical router. These extensions are all off by default, but can easily be set\nas enabled via Cargo features.\n\nPrior to v1.0 you can expect the API to receive some changes, although I will\ndo my best to keep this to a minimum to reduce any churn involved. One choice\nthat is perhaps going to change is the API around using non-filesystem based\npathing. Other than that expect changes as optimizations (and the likely API\nrefactoring associated with them) still need to be fully investigated.\n\n### Getting Started\n\nUsher is available on [crates.io](https://crates.io/crates/usher). The easiest\nway to use it is to add an entry to your `Cargo.toml` defining the dependency:\n\n```toml\n[dependencies]\nusher = \"0.1\"\n```\n\nIf you require any of the Usher extensions, you can opt into them by setting the\nfeature flags in your dependency configuration:\n\n```toml\nusher = { version = \"0.1\", features = [\"web\"] }\n```\n\nYou can find the available extensions in the documentation.\n\n### Basic Usage\n\nThe construction of a tree is quite simple, depending on what your desired outcome\nis. To construct a very basic/static tree, you can simply insert the routes you\ncare about:\n\n```rust\nuse usher::prelude::*;\n\nfn main() {\n    // First we construct our `Router` using a set of parsers. Fortunately for\n    // this example, Usher includes the `StaticParser` which uses basic string\n    // matching to determine whether a segment in the path matches.\n    let mut router: Router\u003cString\u003e = Router::new(vec![\n        Box::new(StaticParser),\n    ]);\n\n    // Then we insert our routes; in this case we're going to store the numbers\n    // 1, 2 and 3, against their equivalent name in typed English (with a \"/\"\n    // as a prefix, as Usher expects filesystem-like paths (for now)).\n    router.insert(\"/one\", \"1\".to_string());\n    router.insert(\"/two\", \"2\".to_string());\n    router.insert(\"/three\", \"3\".to_string());\n\n    // Finally we'll just do a lookup on each path, as well as the a path which\n    // doesn't match (\"/\"), just to demonstrate what the return types look like.\n    for path in vec![\"/\", \"/one\", \"/two\", \"/three\"] {\n        println!(\"{}: {:?}\", path, router.lookup(path));\n    }\n}\n```\n\nThis will route exactly as it looks; matching each static segment provided against\nthe tree and retrieving the value associated with the path. The return type of the\n`lookup(path)` function is `Option\u003c(\u0026T, Vec\u003c(\u0026str, (usize, usize)\u003e)\u003e`, with `\u0026T`\nreferring to the generic value provided (`\"1\"`, etc), and the `Vec` including a set\nof any parameters found during routing. In the case of no parameters, this vector\nwill be empty (as is the case above).\n\nFor usage based around extensions (such as HTTP), please see the documentation for\nthe module containing it - or visit the examples directory for actual usage.\n\n### Advanced Usage\n\nOf course, for some use cases you need to be able to control more than statically\nmatching against the path segments. In a web framework, you might allow for some\npath segments which match regardless and simply capture their value (i.e. `:id`).\nIn order to allow this type of usage, there are two traits available in Usher; the\n`Parser` and `Matcher` traits. These two traits can be implemented to describe how\nto match against specific segments in an incoming path.\n\nThe `Matcher` trait is used to determine if an incoming path segment matches a\nconfigured path segment. It's also responsible for pulling out any capture that\nis associated with the incoming segment. The `Parser` trait is used to calculate\nwhich `Matcher` type should be used on a configured path segment. At a glance it\nmight seem that these two traits could be combined but the difference is that the\n`Parser` trait operates at router creation time, whereas the `Matcher` trait exists\nfor execution when matching against a created router.\n\nTo demonstrate these traits, we can use the `:id` example of a typical web framework.\nThe concept of this syntax is that it should match any value provided to the tree.\nIf my router was configured with the path `/:id`, it would match incoming paths of\n`/123` and `/abc` (but not `/`). This would provide a captured value `id` which holds\nthe value `123` or `abc`.\n\n#### Matcher\n\nThis pattern is pretty simple to implement using the two traits we defined above.\nFirst of all we must construct our `Matcher` type (technically you might write the\n`Parser` first, but it's easier to explain in this order). Fortunately, the rules\nhere are very simple.\n\n```rust\n/// A `Matcher` type used to match against dynamic segments.\n///\n/// The internal value here is the name of the path parameter (based on the\n/// example talked through above, this would be the _owned_ `String` of `\"id\"`).\npub struct DynamicMatcher {\n    inner: String\n}\n\nimpl Matcher for DynamicMatcher {\n    /// Determines if there is a capture for the incoming segment.\n    ///\n    /// In the pattern we described above the entire value becomes the capture,\n    /// so we return a tuple of `(\"id\", (start, end))` to represent the capture.\n    fn capture(\u0026self, segment: \u0026str) -\u003e Option\u003c(\u0026str, (usize, usize))\u003e {\n        Some((\u0026self.inner, (0, segment.len())))\n    }\n\n    /// Determines if this matcher matches the incoming segment.\n    ///\n    /// Because the segment is dynamic and matches any value, this is able to\n    /// always return `true` without even looking at the incoming segment.\n    fn is_match(\u0026self, _segment: \u0026str) -\u003e bool {\n        true\n    }\n}\n```\n\nThis implementation is fairly trivial and should be quite self-explanatory; the\nmatcher matches anything so `is_match/1` will always return true. We always want\nto capture the segment, so that's returned from `capture/1`. A couple of things\nto mention about captures;\n\n- An implementation of `capture/1` is option, as it will default to `None`.\n- The `capture/1` implementation is only called if `is_match/1` resolved to `true`.\n- The tuple structure used for captures is necessary as we need some way to know\n  the name of the captures at runtime. The names cannot be stored in the router\n  itself as there may be use cases where the capture name is actually a function\n  of the incoming path segment (not in this case specifically, of course).\n\n#### Parser\n\nNow that we have our `Matcher` type, we need to construct a `Parser` type in\norder to associate the configured segments with the correct `Matcher`. This is\npretty trivial in our case, because pretty much the only rule we have is that\nthe segment must be of the pattern `:.+`, which we can roughly translate to\n`starts_with(\":\")` for example purposes. As such, a `Parser` type might look\nlike this:\n\n```rust\n/// A `Parser` type used to parse out `DynamicMatcher` values.\npub struct DynamicParser;\n\nimpl Parser for DynamicParser {\n    /// Attempts to parse a segment into a corresponding `Matcher`.\n    ///\n    /// As a dynamic segment is determined by the pattern `:.+`, we check the first\n    /// character of the segment. If the segment is not `:` we are unable to parse\n    /// and so return a `None` value.\n    ///\n    /// If it does start with a `:`, we construct a `DynamicMatcher` and pass the\n    /// parameter name through as it's used when capturing values.\n    fn parse(\u0026self, segment: \u0026str) -\u003e Option\u003cBox\u003cMatcher\u003e\u003e {\n        if \u0026segment[0..1] != \":\" {\n            return None;\n        }\n\n        let field = \u0026segment[1..];\n        let matcher = DynamicMatcher {\n            inner: field.to_owned()\n        };\n\n        Some(Box::new(matcher))\n    }\n}\n```\n\nOne of the nice things about splitting the traits is that you can switch up the\nsyntax easily. Although both `DynamicMatcher` and `DynamicParser` are included\nin Usher, you might want to use a different syntax. One other example of syntax\nfor parameters (I think in the Java realm) is `{id}`. To accomodate this case,\nyou only have to write a new `Parser` implementation; the existing `Matcher`\nstruct already works!\n\n```rust\n/// A customer `Parser` type used to parse out `DynamicMatcher` values.\npub struct CustomDynamicParser;\n\nimpl Parser for CustomDynamicParser {\n    /// Attempts to parse a segment into a corresponding `Matcher`.\n    ///\n    /// This will match segments based on `{id}` syntax, rather than `:id`. We have\n    /// to check the end characters, and pass back the something in the middle!\n    fn parse(\u0026self, segment: \u0026str) -\u003e Option\u003cBox\u003cMatcher\u003e\u003e {\n        // has to start with \"{\"\n        if \u0026segment[0..1] != \"{\" {\n            return None;\n        }\n\n        // has to end with \"}\"\n        if \u0026segment[(len - 1)..] != \"}\" {\n            return None;\n        }\n\n        // so 1..(len - 1) trim the brackets\n        let field = \u0026segment[1..(len - 1)];\n        let matcher = DynamicMatcher::new(field);\n\n        // wrap it up!\n        Some(Box::new(matcher))\n    }\n}\n```\n\nOf course, this also makes it trivial to match _either_ of the two forms shown\nabove. You can attach both parsers to your tree at startup, and it will allow\nfor both `:id` and `{id}`. This flexibility can be definitely be useful when\nwriting more involved frameworks, using Usher as the underlying routing layer.\n\n#### Configuration\n\nNow we have our types, we have to actually configure them in a router in order\nfor them to take effect. This is done at router initialization time, and you've\nalready seen an example of this in the basic example where we provide the basic\n`StaticParser` type. Much like this example, we pass our parser in directly:\n\n```rust\nlet mut router: Router\u003cString\u003e = Router::new(vec![\n    Box::new(DynamicParser),\n    Box::new(StaticParser),\n]);\n```\n\nUsing this definition, our new `Parser` will be used to determine if we can parse\ndynamic segments from the path. Below is a demonstration of a simple path which\nmakes use of both matcher types (`S` dictates a static segment, and `D` dictates\na dynamic segment):\n\n```\n/api/user/:id\n  ^   ^    ^\n  |   |    |\n  S   S    D\n```\n\nPlease note that the order the parsers are provided is very important; you should\nplace the most \"specific\" parsers first as they are tested in order. If you placed\n`StaticParser` first in the list above, then nothing would ever continue through to\nthe `DynamicParser` as every segment satisfies the `StaticParser` requirements.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhitfin%2Fusher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwhitfin%2Fusher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhitfin%2Fusher/lists"}