├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── bench.rs ├── examples └── hello.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── insert_and_find.rs ├── src ├── lib.rs ├── node.rs └── parser.rs └── tests ├── fixtures └── github.rs ├── node.rs ├── parser.rs └── tree.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: fundon 2 | # open_collective: fundon 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | CARGO_INCREMENTAL: 0 11 | CARGO_NET_RETRY: 10 12 | CARGO_TERM_COLOR: always 13 | RUST_BACKTRACE: 1 14 | RUSTFLAGS: -D warnings 15 | RUSTUP_MAX_RETRIES: 10 16 | 17 | jobs: 18 | test: 19 | name: Test (${{ matrix.os }}) 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: 24 | - ubuntu-latest 25 | - macos-latest 26 | - windows-latest 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@nightly 31 | - run: cargo test --all --verbose 32 | - run: cargo bench 33 | 34 | clippy: 35 | name: Clippy 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: dtolnay/rust-toolchain@clippy 40 | - run: cargo clippy --tests -- -Dclippy::all -Dclippy::pedantic 41 | 42 | fmt: 43 | name: Fmt 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | - run: cargo fmt --all -- --check 49 | 50 | docs: 51 | name: Doc 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: dtolnay/rust-toolchain@nightly 56 | - run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --workspace --all-features --no-deps 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.7.4] - 2024-01-02 11 | 12 | ### Features 13 | 14 | - normal match repeated pattern by @TroyKomodo, #36, #37 15 | 16 | ## [0.6.0] - 2022-09-28 17 | 18 | ### Features 19 | 20 | - `no_std` support 21 | 22 | ## [0.5.2] - 2022-09-26 23 | 24 | ### Features 25 | 26 | - Supports Parameters Iterator 27 | - Supports update node value 28 | 29 | ## [0.5.1] - 2022-09-23 30 | 31 | ### Refactor 32 | 33 | - Remove lifetime 34 | 35 | - Inserts then returning node id 36 | 37 | ## [0.5.0] 38 | 39 | ### Changed 40 | 41 | - Refactor it, supports more cases and improve performance. 42 | 43 | ## [0.4.0] 44 | 45 | ### Changed 46 | 47 | - [#20] - Support wildcards that are not preceded by a "/" (`"/foo_*rest"` matches `"foo_bar/baz"`). 48 | 49 | 50 | [#20]: https://github.com/viz-rs/path-tree/pull/20 51 | 52 | ## [0.3.1] - 2022-07-15 53 | 54 | - [#19] - PathTree initializes with an empty path. Thanks to @adriangb. 55 | 56 | ### Changed 57 | 58 | [#19]: https://github.com/viz-rs/path-tree/pull/19 59 | 60 | ## [0.3.0] - 2022-05-28 61 | 62 | ### Changed 63 | 64 | - [#17] - Add basic fuzzing. Thanks to @Txuritan. 65 | - [#16] - Replace the Node.indices String with a Vec, this avoids the expensive code point decoding in position. Thanks to @Txuritan. 66 | 67 | [#17]: https://github.com/viz-rs/path-tree/pull/17 68 | [#16]: https://github.com/viz-rs/path-tree/pull/16 69 | 70 | ## [0.2.2] - 2021-10-24 71 | 72 | ### Fixed 73 | 74 | - [#8] - Thanks to @josalmi 75 | 76 | [#8]: https://github.com/viz-rs/path-tree/pull/8 77 | 78 | ## [0.2.1] - 2021-09-11 79 | 80 | ### Changed 81 | 82 | - Clippy Fixed 83 | 84 | ## [0.2.0] - 2021-09-03 85 | 86 | ### Changed 87 | 88 | - Route finding optimizations. Thanks to @Txuritan. 89 | 90 | ## [0.1.12] - 2020-09-29 91 | 92 | ### Fixed 93 | 94 | - Find byte index, use `str#find` instead of `position`. Thanks to @asaaki. 95 | 96 | ## [0.1.11] - 2020-06-21 97 | 98 | ### Changed 99 | 100 | - Readme 101 | 102 | ## [0.1.10] - 2020-06-20 103 | 104 | ### Changed 105 | 106 | - `const fn` 107 | 108 | ## [0.1.9] - 2019-11-05 109 | 110 | ### Added 111 | 112 | - Benchmark data. 113 | - A lifetime for result. 114 | 115 | ## Changed 116 | 117 | - Dont use unsafe code. 118 | - Dont need mut. 119 | 120 | ## [0.1.4] - 2019-03-18 121 | 122 | ### Changed 123 | 124 | - Tuple struct for PathTree. 125 | 126 | [Unreleased]: https://github.com/viz-rs/path-tree/compare/v0.6.0...HEAD 127 | [0.6.0]: https://github.com/viz-rs/path-tree/compare/v0.5.2...v0.6.0 128 | [0.5.2]: https://github.com/viz-rs/path-tree/compare/v0.5.1...v0.5.2 129 | [0.5.1]: https://github.com/viz-rs/path-tree/compare/v0.5.0...v0.5.1 130 | [0.5.0]: https://github.com/viz-rs/path-tree/compare/v0.4.0...v0.5.0 131 | [0.4.0]: https://github.com/viz-rs/path-tree/compare/v0.3.1...v0.4.0 132 | [0.3.1]: https://github.com/viz-rs/path-tree/compare/v0.3.0...v0.3.1 133 | [0.3.0]: https://github.com/viz-rs/path-tree/compare/v0.2.2...v0.3.0 134 | [0.2.2]: https://github.com/viz-rs/path-tree/compare/v0.2.1...v0.2.2 135 | [0.2.1]: https://github.com/viz-rs/path-tree/compare/v0.2.0...v0.2.1 136 | [0.2.0]: https://github.com/viz-rs/path-tree/compare/v0.1.12...v0.2.0 137 | [0.1.12]: https://github.com/viz-rs/path-tree/compare/v0.1.11...v0.1.12 138 | [0.1.11]: https://github.com/viz-rs/path-tree/compare/v0.1.10...v0.1.11 139 | [0.1.10]: https://github.com/viz-rs/path-tree/compare/v0.1.9...v0.1.10 140 | [0.1.9]: https://github.com/viz-rs/path-tree/compare/v0.1.4...v0.1.9 141 | [0.1.4]: https://github.com/viz-rs/path-tree/releases/tag/v0.1.4 142 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "path-tree" 3 | version = "0.8.3" 4 | authors = ["Fangdun Tsai "] 5 | description = "path-tree is a lightweight high performance HTTP request router for Rust" 6 | repository = "https://github.com/viz-rs/path-tree" 7 | documentation = "https://docs.rs/path-tree" 8 | keywords = ["radix", "tree", "path", "router"] 9 | categories = ["asynchronous", "web-programming", "web-programming::http-server"] 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | edition = "2021" 13 | 14 | include = [ 15 | "CHANGES.md", 16 | "Cargo.toml", 17 | "LICENSE-APACHE", 18 | "LICENSE-MIT", 19 | "README.md", 20 | "src/*.rs" 21 | ] 22 | 23 | [dependencies] 24 | smallvec = { version = "1.15.0", features = ["const_new"] } 25 | 26 | [dev-dependencies] 27 | bytes = "1" 28 | actix-router = "0.5" 29 | ntex-router = "0.5" 30 | path-table = "1.0" 31 | route-recognizer = "0.3" 32 | matchit = "0.7" 33 | gonzales = "0.0.3-beta" 34 | futures = "0.3" 35 | rand = "0.9" 36 | criterion = { version = "0.5", features = ["html_reports"] } 37 | hyper = { version = "1", features = ["full"] } 38 | hyper-util = { version = "0.1", features = ["tokio"] } 39 | http-body-util = "0.1" 40 | tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread", "net"] } 41 | 42 | [[bench]] 43 | name = "bench" 44 | harness = false 45 | path = "benches/bench.rs" 46 | 47 | [[example]] 48 | name = "hello" 49 | path = "examples/hello.rs" 50 | 51 | [profile.bench] 52 | lto = true 53 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019-present Fangdun Tsai 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Fangdun Tsai 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 |

path-tree

2 | 3 |
4 |

A lightweight high performance HTTP request router for Rust

5 |
6 | 7 |
8 | 9 | 10 | Safety! 11 | 12 | 13 | Docs.rs docs 15 | 16 | 17 | Crates.io version 19 | 20 | 21 | Download 23 |
24 | 25 | ## Parameters Syntax 26 | 27 | | Pattern | Kind | Description | 28 | | ------------------------------- | ------------------- | ------------------------------------------------------------------------------ | 29 | | `:name` | `Normal` | Matches a path piece, excludes `/` | 30 | | `:name?` | `Optional` | Matches an optional path piece, excludes `/` | 31 | | `/:name?/` `/:name?` | `OptionalSegment` | Matches an optional path segment, excludes `/`, prefix or suffix should be `/` | 32 | | `+` `:name+` | `OneOrMore` | Matches a path piece, includes `/` | 33 | | `*` `:name*` | `ZeroOrMore` | Matches an optional path piece, includes `/` | 34 | | `/*/` `/*` `/:name*/` `/:name*` | `ZeroOrMoreSegment` | Matches zero or more path segments, prefix or suffix should be `/` | 35 | 36 | ## Supports 37 | 38 | | Case | Parameters | 39 | | ----------------------- | ----------- | 40 | | `:a:b` | `a` `b` | 41 | | `:a:b?` | `a` `b` | 42 | | `:a-:b` `:a.:b` `:a~:b` | `a` `b` | 43 | | `:a_a-:b_b` | `a_a` `b_b` | 44 | | `:a\\:` `:a\\_` | `a` | 45 | | `:a\\::b` `:a\\_:b` | `a` `b` | 46 | | `:a*` | `a` | 47 | | `*` | `*1` | 48 | | `*.*` | `*1` `*2` | 49 | | `:a+` | `a` | 50 | | `+` | `+1` | 51 | | `+.+` | `+1` `+2` | 52 | | `/*/abc/+/def/g` | `*1` `+2` | 53 | 54 | ## Examples 55 | 56 | ```rust 57 | use path_tree::PathTree; 58 | 59 | /* 60 | / •0 61 | ├── api/ 62 | │ └── + •13 63 | ├── login •1 64 | ├── public/ 65 | │ └── ** •7 66 | ├── s 67 | │ ├── ettings •3 68 | │ │ └── / 69 | │ │ └── : •4 70 | │ └── ignup •2 71 | └── : •5 72 | └── / 73 | └── : •6 74 | └── / 75 | ├── actions/ 76 | │ └── : 77 | │ └── \: 78 | │ └── : •10 79 | ├── releases/download/ 80 | │ └── : 81 | │ └── / 82 | │ └── : 83 | │ └── . 84 | │ └── : •8 85 | ├── tags/ 86 | │ └── : 87 | │ └── - 88 | │ └── : 89 | │ └── - 90 | │ └── : •9 91 | ├── : •11 92 | └── ** •12 93 | */ 94 | let mut tree = PathTree::new(); 95 | 96 | tree.insert("/", 0); 97 | tree.insert("/login", 1); 98 | tree.insert("/signup", 2); 99 | tree.insert("/settings", 3); 100 | tree.insert("/settings/:page", 4); 101 | tree.insert("/:user", 5); 102 | tree.insert("/:user/:repo", 6); 103 | tree.insert("/public/:any*", 7); 104 | tree.insert("/:org/:repo/releases/download/:tag/:filename.:ext", 8); 105 | tree.insert("/:org/:repo/tags/:day-:month-:year", 9); 106 | tree.insert("/:org/:repo/actions/:name\\::verb", 10); 107 | tree.insert("/:org/:repo/:page", 11); 108 | tree.insert("/:org/:repo/*", 12); 109 | tree.insert("/api/+", 13); 110 | 111 | let (h, p) = tree.find("/").unwrap(); 112 | assert_eq!(h, &0); 113 | assert_eq!(p.params(), vec![]); 114 | 115 | let (h, p) = tree.find("/login").unwrap(); 116 | assert_eq!(h, &1); 117 | assert_eq!(p.params(), vec![]); 118 | 119 | let (h, p) = tree.find("/settings/admin").unwrap(); 120 | assert_eq!(h, &4); 121 | assert_eq!(p.params(), vec![("page", "admin")]); 122 | 123 | let (h, p) = tree.find("/viz-rs").unwrap(); 124 | assert_eq!(h, &5); 125 | assert_eq!(p.params(), vec![("user", "viz-rs")]); 126 | 127 | let (h, p) = tree.find("/viz-rs/path-tree").unwrap(); 128 | assert_eq!(h, &6); 129 | assert_eq!(p.params(), vec![("user", "viz-rs"), ("repo", "path-tree")]); 130 | 131 | let (h, p) = tree.find("/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-aarch64-apple-darwin.gz").unwrap(); 132 | assert_eq!(h, &8); 133 | assert_eq!( 134 | p.params(), 135 | vec![ 136 | ("org", "rust-lang"), 137 | ("repo", "rust-analyzer"), 138 | ("tag", "2022-09-12"), 139 | ("filename", "rust-analyzer-aarch64-apple-darwin"), 140 | ("ext", "gz") 141 | ] 142 | ); 143 | 144 | let (h, p) = tree.find("/rust-lang/rust-analyzer/tags/2022-09-12").unwrap(); 145 | assert_eq!(h, &9); 146 | assert_eq!( 147 | p.params(), 148 | vec![ 149 | ("org", "rust-lang"), 150 | ("repo", "rust-analyzer"), 151 | ("day", "2022"), 152 | ("month", "09"), 153 | ("year", "12") 154 | ] 155 | ); 156 | 157 | let (h, p) = tree.find("/rust-lang/rust-analyzer/actions/ci:bench").unwrap(); 158 | assert_eq!(h, &10); 159 | assert_eq!( 160 | p.params(), 161 | vec![ 162 | ("org", "rust-lang"), 163 | ("repo", "rust-analyzer"), 164 | ("name", "ci"), 165 | ("verb", "bench"), 166 | ] 167 | ); 168 | 169 | let (h, p) = tree.find("/rust-lang/rust-analyzer/stargazers").unwrap(); 170 | assert_eq!(h, &11); 171 | assert_eq!(p.params(), vec![("org", "rust-lang"), ("repo", "rust-analyzer"), ("page", "stargazers")]); 172 | 173 | let (h, p) = tree.find("/rust-lang/rust-analyzer/stargazers/404").unwrap(); 174 | assert_eq!(h, &12); 175 | assert_eq!(p.params(), vec![("org", "rust-lang"), ("repo", "rust-analyzer"), ("*1", "stargazers/404")]); 176 | 177 | let (h, p) = tree.find("/public/js/main.js").unwrap(); 178 | assert_eq!(h, &7); 179 | assert_eq!(p.params(), vec![("any", "js/main.js")]); 180 | 181 | let (h, p) = tree.find("/api/v1").unwrap(); 182 | assert_eq!(h, &13); 183 | assert_eq!(p.params(), vec![("+1", "v1")]); 184 | ``` 185 | 186 | Hyper hello example can be found [here](examples/hello.rs). 187 | 188 | ## Benchmark 189 | 190 | ```shell 191 | $ cargo bench 192 | ``` 193 | 194 | ## Acknowledgements 195 | 196 | It is inspired by the: 197 | 198 | - [rax] 199 | - [httprouter] 200 | - [echo] router 201 | - [path-to-regexp] 202 | - [gofiber] router 203 | - [trekjs] router 204 | 205 | ## Other languages 206 | 207 | Wrappers for path-tree in other languages: 208 | 209 | - Python: https://github.com/adriangb/routrie 210 | 211 | ## License 212 | 213 | 214 | Licensed under either of Apache License, Version 215 | 2.0 or MIT license at your option. 216 | 217 | 218 |
219 | 220 | 221 | Unless you explicitly state otherwise, any contribution intentionally submitted 222 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 223 | be dual licensed as above, without any additional terms or conditions. 224 | 225 | 226 | [radix tree]: https://github.com/viz-rs/radix-tree 227 | [rax]: https://github.com/antirez/rax 228 | [httprouter]: https://github.com/julienschmidt/httprouter 229 | [path-to-regexp]: https://github.com/pillarjs/path-to-regexp 230 | [echo]: https://github.com/labstack/echo 231 | [gofiber]: https://github.com/gofiber/fiber 232 | [trekjs]: https://github.com/trekjs/router 233 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_must_use)] 2 | 3 | #[path = "../tests/fixtures/github.rs"] 4 | mod github; 5 | use github::*; 6 | 7 | use criterion::*; 8 | 9 | use actix_router::{Path as ActixPath, Router as ActixRouter}; 10 | use ntex_router::{Path as NtexPath, Router as NtexRouter}; 11 | use path_table::PathTable; 12 | use path_tree::PathTree; 13 | use route_recognizer::Router as RRRouter; 14 | // use gonzales::RouterBuilder; 15 | use matchit::Router as MatchitRouter; 16 | 17 | fn bench_path_insert(c: &mut Criterion) { 18 | let mut group = c.benchmark_group("path_insert"); 19 | 20 | group 21 | .bench_function("actix_router_path", |b| { 22 | let mut router = ActixRouter::::build(); 23 | b.iter(|| { 24 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 25 | router.path(*r, i); 26 | } 27 | }) 28 | }) 29 | .bench_function("ntex_router_path", |b| { 30 | let mut router = NtexRouter::::build(); 31 | b.iter(|| { 32 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 33 | router.path(*r, i); 34 | } 35 | }) 36 | }) 37 | .bench_function("path_table_setup", |b| { 38 | let mut table: PathTable = PathTable::new(); 39 | b.iter(|| { 40 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 41 | *table.setup(r) = i; 42 | } 43 | }) 44 | }) 45 | .bench_function("path_tree_insert", |b| { 46 | let mut tree: PathTree = PathTree::new(); 47 | b.iter(|| { 48 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 49 | tree.insert(r, i); 50 | } 51 | }) 52 | }) 53 | .bench_function("matchit_insert", |b| { 54 | let mut matcher = MatchitRouter::new(); 55 | b.iter(|| { 56 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 57 | let _ = matcher.insert(*r, i); 58 | } 59 | }) 60 | }) 61 | .bench_function("route_recognizer_add", |b| { 62 | let mut router = RRRouter::::new(); 63 | b.iter(|| { 64 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 65 | router.add(r, i); 66 | } 67 | }) 68 | }) 69 | /* 70 | .bench_function("gonzales_route", |b| { 71 | let mut router = RouterBuilder::new(); 72 | b.iter(|| { 73 | for (_i, r) in ROUTES_WITH_COLON.iter().enumerate() { 74 | router.build([r]); 75 | } 76 | }) 77 | }) 78 | */ 79 | .sample_size(20); 80 | 81 | group.finish() 82 | } 83 | 84 | fn bench_path_find(c: &mut Criterion) { 85 | let mut group = c.benchmark_group("path_find"); 86 | 87 | group 88 | .bench_function("actix_router_recognize", |b| { 89 | let mut router = ActixRouter::::build(); 90 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 91 | router.path(*r, i); 92 | } 93 | let router = router.finish(); 94 | b.iter(|| { 95 | for (i, r) in ROUTES_URLS.iter().enumerate() { 96 | let mut path = ActixPath::new(*r); 97 | let n = router.recognize(&mut path).unwrap(); 98 | assert_eq!(*n.0, i); 99 | } 100 | }) 101 | }) 102 | .bench_function("ntex_router_recognize", |b| { 103 | let mut router = NtexRouter::::build(); 104 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 105 | router.path(*r, i); 106 | } 107 | let router = router.finish(); 108 | b.iter(|| { 109 | for (i, r) in ROUTES_URLS.iter().enumerate() { 110 | let mut path = NtexPath::new(*r); 111 | let n = router.recognize(&mut path).unwrap(); 112 | assert_eq!(*n.0, i); 113 | } 114 | }) 115 | }) 116 | .bench_function("path_table_route", |b| { 117 | let mut table: PathTable = PathTable::new(); 118 | for (i, r) in ROUTES_WITH_BRACES.iter().enumerate() { 119 | *table.setup(r) = i; 120 | } 121 | b.iter(|| { 122 | for (i, r) in ROUTES_URLS.iter().enumerate() { 123 | let n = table.route(r).unwrap(); 124 | assert_eq!(*n.0, i); 125 | } 126 | }) 127 | }) 128 | .bench_function("path_tree_find", |b| { 129 | let mut tree: PathTree = PathTree::new(); 130 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 131 | tree.insert(r, i); 132 | } 133 | b.iter(|| { 134 | for (i, r) in ROUTES_URLS.iter().enumerate() { 135 | let n = tree.find(r).unwrap(); 136 | assert_eq!(*n.0, i); 137 | } 138 | }) 139 | }) 140 | .bench_function("matchit_at", |b| { 141 | let mut matcher = MatchitRouter::new(); 142 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 143 | let _ = matcher.insert(*r, i); 144 | } 145 | b.iter(|| { 146 | for (i, r) in ROUTES_URLS.iter().enumerate() { 147 | let n = matcher.at(r).unwrap(); 148 | assert_eq!(*n.value, i); 149 | } 150 | }) 151 | }) 152 | .bench_function("route_recognizer_recognize", |b| { 153 | let mut router = RRRouter::::new(); 154 | for (i, r) in ROUTES_WITH_COLON.iter().enumerate() { 155 | router.add(r, i); 156 | } 157 | b.iter(|| { 158 | for (i, r) in ROUTES_URLS.iter().enumerate() { 159 | let n = router.recognize(r).unwrap(); 160 | assert_eq!(**n.handler(), i); 161 | } 162 | }) 163 | }) 164 | /* 165 | .bench_function("gonzales_route", |b| { 166 | let router = RouterBuilder::new() 167 | .ascii_case_insensitive(false) 168 | .build(ROUTES_WITH_BRACES); 169 | b.iter(|| { 170 | for (_i, r) in ROUTES_URLS.iter().enumerate() { 171 | // let n = router.route(r).unwrap(); 172 | // assert_eq!(n.get_index(), i); 173 | black_box(router.route(r)); 174 | } 175 | }) 176 | }) 177 | */ 178 | .sample_size(12); 179 | 180 | group.finish(); 181 | } 182 | 183 | criterion_group!(benches, bench_path_insert, bench_path_find); 184 | criterion_main!(benches); 185 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_must_use)] 2 | 3 | use std::{convert::Infallible, future::Future, net::SocketAddr, pin::Pin, sync::Arc}; 4 | 5 | use bytes::Bytes; 6 | use http_body_util::Full; 7 | use hyper::{ 8 | body::Incoming, server::conn::http1, service::service_fn, Request, Response, StatusCode, 9 | }; 10 | use hyper_util::rt::TokioIo; 11 | use path_tree::PathTree; 12 | use tokio::net::TcpListener; 13 | 14 | static NOT_FOUND: &[u8] = b"Not Found"; 15 | 16 | type Params = Vec<(String, String)>; 17 | type Body = Full; 18 | 19 | trait Handler: Send + Sync + 'static { 20 | fn call<'a>( 21 | &'a self, 22 | req: Request, 23 | ) -> Pin> + Send + 'a>>; 24 | } 25 | 26 | impl Handler for F 27 | where 28 | F: Send + Sync + 'static + Fn(Request) -> R, 29 | R: Future>> + Send + 'static, 30 | { 31 | fn call<'a>( 32 | &'a self, 33 | req: Request, 34 | ) -> Pin> + Send + 'a>> { 35 | let fut = (self)(req); 36 | Box::pin(fut) 37 | } 38 | } 39 | 40 | async fn index(_: Request) -> Response { 41 | Response::new(Body::from("Hello, Web!")) 42 | } 43 | 44 | async fn hello_world(req: Request) -> Response { 45 | let params = req.extensions().get::().unwrap(); 46 | let mut s = String::new(); 47 | s.push_str("Hello, World!\n"); 48 | for (_, v) in params { 49 | s.push_str(&format!("param = {v}")); 50 | } 51 | Response::new(Body::from(s)) 52 | } 53 | 54 | async fn hello_user(req: Request) -> Response { 55 | let params = req.extensions().get::().unwrap(); 56 | let mut s = String::new(); 57 | s.push_str("Hello, "); 58 | for (k, v) in params { 59 | s.push_str(&format!("{k} = {v}")); 60 | } 61 | s.push('!'); 62 | Response::new(Body::from(s)) 63 | } 64 | 65 | async fn hello_rust(_: Request) -> Response { 66 | Response::new(Body::from("Hello, Rust!")) 67 | } 68 | 69 | async fn login(_req: Request) -> Response { 70 | Response::new(Body::from("I'm logined!")) 71 | } 72 | 73 | #[tokio::main] 74 | async fn main() -> Result<(), Box> { 75 | let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); 76 | 77 | let listener = TcpListener::bind(addr).await?; 78 | 79 | // / 80 | // ├── GET/ •0 81 | // │ ├── hello/ 82 | // │ │ └── : •2 83 | // │ ├── rust •3 84 | // │ └── ** •1 85 | // └── POST/login •4 86 | let mut tree = PathTree::>::new(); 87 | tree.insert("/GET/", Box::new(index)); 88 | tree.insert("/GET/*", Box::new(hello_world)); 89 | tree.insert("/GET/hello/:name", Box::new(hello_user)); 90 | tree.insert("/GET/rust", Box::new(hello_rust)); 91 | tree.insert("/POST/login", Box::new(login)); 92 | 93 | let tree = Arc::new(tree); 94 | 95 | loop { 96 | let (tcp, _) = listener.accept().await?; 97 | let io = TokioIo::new(tcp); 98 | let router = Arc::clone(&tree); 99 | 100 | tokio::task::spawn(async move { 101 | if let Err(err) = http1::Builder::new() 102 | .serve_connection( 103 | io, 104 | service_fn(move |mut req| { 105 | let router = router.clone(); 106 | let path = "/".to_owned() + req.method().as_str() + req.uri().path(); 107 | 108 | async move { 109 | Ok::<_, Infallible>(match router.find(&path) { 110 | Some((handler, route)) => { 111 | let p = route 112 | .params() 113 | .iter() 114 | .map(|p| (p.0.to_string(), p.1.to_string())) 115 | .collect::(); 116 | req.extensions_mut().insert(p); 117 | handler.call(req).await 118 | } 119 | None => Response::builder() 120 | .status(StatusCode::NOT_FOUND) 121 | .body(NOT_FOUND.into()) 122 | .unwrap(), 123 | }) 124 | } 125 | }), 126 | ) 127 | .await 128 | { 129 | println!("Error serving connection: {:?}", err); 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "path-tree-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.path-tree] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "insert_and_find" 24 | path = "fuzz_targets/insert_and_find.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/insert_and_find.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: (Vec<(String, i32)>, String, Option)| { 5 | let mut tree = path_tree::PathTree::new(); 6 | 7 | for (path, num) in &data.0 { 8 | tree.insert(path, num); 9 | } 10 | 11 | match data.2 { 12 | None => { 13 | let _ = tree.find(&data.1); 14 | } 15 | Some(b) => { 16 | if !b { 17 | let _ = tree.find(&data.1); 18 | } 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! path-tree is a lightweight high performance HTTP request router for Rust. 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! use path_tree::PathTree; 7 | //! 8 | //! /* 9 | //! / •0 10 | //! ├── api/ 11 | //! │ └── + •13 12 | //! ├── login •1 13 | //! ├── public/ 14 | //! │ └── ** •7 15 | //! ├── s 16 | //! │ ├── ettings •3 17 | //! │ │ └── / 18 | //! │ │ └── : •4 19 | //! │ └── ignup •2 20 | //! └── : •5 21 | //! └── / 22 | //! └── : •6 23 | //! └── / 24 | //! ├── actions/ 25 | //! │ └── : 26 | //! │ └── \: 27 | //! │ └── : •10 28 | //! ├── releases/download/ 29 | //! │ └── : 30 | //! │ └── / 31 | //! │ └── : 32 | //! │ └── . 33 | //! │ └── : •8 34 | //! ├── tags/ 35 | //! │ └── : 36 | //! │ └── - 37 | //! │ └── : 38 | //! │ └── - 39 | //! │ └── : •9 40 | //! ├── : •11 41 | //! └── ** •12 42 | //! */ 43 | //! let mut tree = PathTree::new(); 44 | //! 45 | //! tree.insert("/", 0); 46 | //! tree.insert("/login", 1); 47 | //! tree.insert("/signup", 2); 48 | //! tree.insert("/settings", 3); 49 | //! tree.insert("/settings/:page", 4); 50 | //! tree.insert("/:user", 5); 51 | //! tree.insert("/:user/:repo", 6); 52 | //! tree.insert("/public/:any*", 7); 53 | //! tree.insert("/:org/:repo/releases/download/:tag/:filename.:ext", 8); 54 | //! tree.insert("/:org/:repo/tags/:day-:month-:year", 9); 55 | //! tree.insert("/:org/:repo/actions/:name\\::verb", 10); 56 | //! tree.insert("/:org/:repo/:page", 11); 57 | //! tree.insert("/:org/:repo/*", 12); 58 | //! tree.insert("/api/+", 13); 59 | //! 60 | //! let (h, p) = tree.find("/").unwrap(); 61 | //! assert_eq!(h, &0); 62 | //! assert_eq!(p.params(), vec![]); 63 | //! 64 | //! let (h, p) = tree.find("/login").unwrap(); 65 | //! assert_eq!(h, &1); 66 | //! assert_eq!(p.params(), vec![]); 67 | //! 68 | //! let (h, p) = tree.find("/settings/admin").unwrap(); 69 | //! assert_eq!(h, &4); 70 | //! assert_eq!(p.params(), vec![("page", "admin")]); 71 | //! 72 | //! let (h, p) = tree.find("/viz-rs").unwrap(); 73 | //! assert_eq!(h, &5); 74 | //! assert_eq!(p.params(), vec![("user", "viz-rs")]); 75 | //! 76 | //! let (h, p) = tree.find("/viz-rs/path-tree").unwrap(); 77 | //! assert_eq!(h, &6); 78 | //! assert_eq!(p.params(), vec![("user", "viz-rs"), ("repo", "path-tree")]); 79 | //! 80 | //! let (h, p) = tree.find("/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-aarch64-apple-darwin.gz").unwrap(); 81 | //! assert_eq!(h, &8); 82 | //! assert_eq!( 83 | //! p.params(), 84 | //! vec![ 85 | //! ("org", "rust-lang"), 86 | //! ("repo", "rust-analyzer"), 87 | //! ("tag", "2022-09-12"), 88 | //! ("filename", "rust-analyzer-aarch64-apple-darwin"), 89 | //! ("ext", "gz") 90 | //! ] 91 | //! ); 92 | //! 93 | //! let (h, p) = tree.find("/rust-lang/rust-analyzer/tags/2022-09-12").unwrap(); 94 | //! assert_eq!(h, &9); 95 | //! assert_eq!( 96 | //! p.params(), 97 | //! vec![ 98 | //! ("org", "rust-lang"), 99 | //! ("repo", "rust-analyzer"), 100 | //! ("day", "2022"), 101 | //! ("month", "09"), 102 | //! ("year", "12") 103 | //! ] 104 | //! ); 105 | //! 106 | //! let (h, p) = tree.find("/rust-lang/rust-analyzer/actions/ci:bench").unwrap(); 107 | //! assert_eq!(h, &10); 108 | //! assert_eq!( 109 | //! p.params(), 110 | //! vec![ 111 | //! ("org", "rust-lang"), 112 | //! ("repo", "rust-analyzer"), 113 | //! ("name", "ci"), 114 | //! ("verb", "bench"), 115 | //! ] 116 | //! ); 117 | //! 118 | //! let (h, p) = tree.find("/rust-lang/rust-analyzer/stargazers").unwrap(); 119 | //! assert_eq!(h, &11); 120 | //! assert_eq!(p.params(), vec![("org", "rust-lang"), ("repo", "rust-analyzer"), ("page", "stargazers")]); 121 | //! 122 | //! let (h, p) = tree.find("/rust-lang/rust-analyzer/stargazers/404").unwrap(); 123 | //! assert_eq!(h, &12); 124 | //! assert_eq!(p.params(), vec![("org", "rust-lang"), ("repo", "rust-analyzer"), ("*1", "stargazers/404")]); 125 | //! 126 | //! let (h, p) = tree.find("/public/js/main.js").unwrap(); 127 | //! assert_eq!(h, &7); 128 | //! assert_eq!(p.params(), vec![("any", "js/main.js")]); 129 | //! 130 | //! let (h, p) = tree.find("/api/v1").unwrap(); 131 | //! assert_eq!(h, &13); 132 | //! assert_eq!(p.params(), vec![("+1", "v1")]); 133 | //! ``` 134 | 135 | #![no_std] 136 | #![forbid(unsafe_code)] 137 | #![warn(rust_2018_idioms, unreachable_pub)] 138 | 139 | extern crate alloc; 140 | 141 | use alloc::{ 142 | string::{String, ToString}, 143 | vec::Vec, 144 | }; 145 | use core::{slice::Iter, str::from_utf8}; 146 | use smallvec::SmallVec; 147 | 148 | mod node; 149 | pub use node::{Key, Node}; 150 | 151 | mod parser; 152 | pub use parser::{Kind, Parser, Piece, Position}; 153 | 154 | /// A path tree. 155 | #[derive(Clone, Debug)] 156 | pub struct PathTree { 157 | id: usize, 158 | routes: Vec<(T, Vec)>, 159 | pub node: Node, 160 | } 161 | 162 | impl Default for PathTree { 163 | fn default() -> Self { 164 | Self::new() 165 | } 166 | } 167 | 168 | impl PathTree { 169 | /// Creates a new [`PathTree`]. 170 | #[must_use] 171 | pub fn new() -> Self { 172 | Self { 173 | id: 0, 174 | routes: Vec::new(), 175 | node: Node::new(Key::String(Vec::new()), None), 176 | } 177 | } 178 | 179 | /// Inserts a part path-value to the tree and returns the id. 180 | #[must_use] 181 | pub fn insert(&mut self, path: &str, value: T) -> usize { 182 | let mut node = &mut self.node; 183 | 184 | let (overwritten, pieces) = if path.is_empty() { 185 | (false, Vec::new()) 186 | } else { 187 | let pieces = Parser::new(path).collect::>(); 188 | node = pieces.iter().fold(node, |node, piece| match piece { 189 | Piece::String(s) => node.insert_bytes(&s[..]), 190 | Piece::Parameter(_, k) => node.insert_parameter(*k), 191 | }); 192 | (true, pieces) 193 | }; 194 | 195 | if let Some(id) = node.value { 196 | self.routes[id].0 = value; 197 | if overwritten { 198 | self.routes[id].1 = pieces; 199 | } 200 | id 201 | } else { 202 | self.routes.push((value, pieces)); 203 | let id = self.id; 204 | node.value = Some(id); 205 | self.id += 1; 206 | id 207 | } 208 | } 209 | 210 | /// Returns the [`Path`] by the given path. 211 | #[must_use] 212 | pub fn find<'a, 'b>(&'a self, path: &'b str) -> Option<(&'a T, Path<'a, 'b>)> { 213 | let bytes = path.as_bytes(); 214 | self.node.find(bytes).and_then(|(id, ranges)| { 215 | self.routes.get(*id).map(|(value, pieces)| { 216 | ( 217 | value, 218 | Path { 219 | id, 220 | pieces, 221 | // opt! 222 | raws: ranges 223 | .into_iter() 224 | .filter_map(|r| from_utf8(&bytes[r]).ok()) 225 | .rev() 226 | .collect(), 227 | }, 228 | ) 229 | }) 230 | }) 231 | } 232 | 233 | /// Gets the route by id. 234 | #[must_use] 235 | #[inline] 236 | pub fn get_route(&self, index: usize) -> Option<&(T, Vec)> { 237 | self.routes.get(index) 238 | } 239 | 240 | /// Generates URL with the params. 241 | #[must_use] 242 | pub fn url_for(&self, index: usize, params: &[&str]) -> Option { 243 | self.get_route(index).and_then(|(_, pieces)| { 244 | let mut bytes = Vec::new(); 245 | let mut iter = params.iter(); 246 | 247 | pieces.iter().for_each(|piece| match piece { 248 | Piece::String(s) => { 249 | bytes.extend_from_slice(s); 250 | } 251 | Piece::Parameter(_, _) => { 252 | if let Some(s) = iter.next() { 253 | bytes.extend_from_slice(s.as_bytes()); 254 | } 255 | } 256 | }); 257 | 258 | from_utf8(&bytes).map(ToString::to_string).ok() 259 | }) 260 | } 261 | 262 | pub fn iter(&self) -> Iter<'_, (T, Vec)> { 263 | self.routes.iter() 264 | } 265 | } 266 | 267 | impl<'a, T> IntoIterator for &'a PathTree { 268 | type Item = &'a (T, Vec); 269 | type IntoIter = Iter<'a, (T, Vec)>; 270 | 271 | fn into_iter(self) -> Self::IntoIter { 272 | self.iter() 273 | } 274 | } 275 | 276 | /// Matched route path infomation. 277 | #[derive(Clone, Debug, Eq, PartialEq)] 278 | pub struct Path<'a, 'b> { 279 | pub id: &'a usize, 280 | pub pieces: &'a [Piece], 281 | pub raws: SmallVec<[&'b str; 4]>, 282 | } 283 | 284 | impl Path<'_, '_> { 285 | /// Gets current path pattern. 286 | /// 287 | /// # Panics 288 | /// 289 | /// Will panic if bytes to string conversion fails. 290 | pub fn pattern(&self) -> String { 291 | let mut bytes = Vec::new(); 292 | 293 | self.pieces.iter().for_each(|piece| match piece { 294 | Piece::String(s) => { 295 | if s == b":" || s == b"+" || s == b"?" { 296 | bytes.push(b'\\'); 297 | } 298 | bytes.extend_from_slice(s); 299 | } 300 | Piece::Parameter(p, k) => match p { 301 | Position::Index(_, _) => { 302 | if *k == Kind::OneOrMore { 303 | bytes.push(b'+'); 304 | } else if *k == Kind::ZeroOrMore || *k == Kind::ZeroOrMoreSegment { 305 | bytes.push(b'*'); 306 | } 307 | } 308 | Position::Named(n) => match k { 309 | Kind::Normal | Kind::Optional | Kind::OptionalSegment => { 310 | bytes.push(b':'); 311 | bytes.extend_from_slice(n); 312 | if *k == Kind::Optional || *k == Kind::OptionalSegment { 313 | bytes.push(b'?'); 314 | } 315 | } 316 | Kind::OneOrMore => { 317 | bytes.push(b'+'); 318 | bytes.extend_from_slice(n); 319 | } 320 | Kind::ZeroOrMore | Kind::ZeroOrMoreSegment => { 321 | bytes.push(b'*'); 322 | bytes.extend_from_slice(n); 323 | } 324 | }, 325 | }, 326 | }); 327 | 328 | from_utf8(&bytes) 329 | .map(ToString::to_string) 330 | .expect("pattern generated failure") 331 | } 332 | 333 | /// Returns the parameters of the current path. 334 | #[must_use] 335 | pub fn params(&self) -> Vec<(&str, &str)> { 336 | self.params_iter().collect() 337 | } 338 | 339 | /// Returns the parameters iterator of the current path. 340 | pub fn params_iter(&self) -> impl Iterator { 341 | #[inline] 342 | fn piece_filter(piece: &Piece) -> Option<&str> { 343 | match piece { 344 | Piece::String(_) => None, 345 | Piece::Parameter(p, _) => from_utf8(match p { 346 | Position::Index(_, n) | Position::Named(n) => n, 347 | }) 348 | .ok(), 349 | } 350 | } 351 | 352 | self.pieces 353 | .iter() 354 | .filter_map(piece_filter) 355 | .zip(self.raws.iter().copied()) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use alloc::{string::String, vec::Vec}; 2 | use core::{ 3 | cmp::Ordering, 4 | fmt::{self, Write}, 5 | ops::Range, 6 | }; 7 | 8 | use smallvec::SmallVec; 9 | 10 | use crate::Kind; 11 | 12 | #[derive(Clone, Debug, Eq, PartialEq)] 13 | pub enum Key { 14 | String(Vec), 15 | Parameter(Kind), 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct Node { 20 | pub key: Key, 21 | pub value: Option, 22 | /// Stores string node 23 | pub nodes0: Option>, 24 | /// Stores parameter node 25 | pub nodes1: Option>, 26 | } 27 | 28 | impl Node { 29 | pub fn new(key: Key, value: Option) -> Self { 30 | Self { 31 | key, 32 | value, 33 | nodes0: None, 34 | nodes1: None, 35 | } 36 | } 37 | 38 | pub fn insert_bytes(&mut self, mut bytes: &[u8]) -> &mut Self { 39 | let diff = match &mut self.key { 40 | Key::String(s) => { 41 | if s.is_empty() { 42 | *s = bytes.to_vec(); 43 | return self; 44 | } 45 | 46 | let cursor = s 47 | .iter() 48 | .zip(bytes.iter()) 49 | .take_while(|(a, b)| a == b) 50 | .count(); 51 | 52 | if cursor == 0 { 53 | true 54 | } else { 55 | // split node 56 | if cursor < s.len() { 57 | let (prefix, suffix) = s.split_at(cursor); 58 | let mut node = Node::new(Key::String(prefix.to_vec()), None); 59 | *s = suffix.to_vec(); 60 | ::core::mem::swap(self, &mut node); 61 | self.nodes0.get_or_insert_with(Vec::new).push(node); 62 | } 63 | if cursor == bytes.len() { 64 | false 65 | } else { 66 | bytes = &bytes[cursor..]; 67 | true 68 | } 69 | } 70 | } 71 | Key::Parameter(_) => true, 72 | }; 73 | 74 | // insert node 75 | if diff { 76 | let nodes = self.nodes0.get_or_insert_with(Vec::new); 77 | return match nodes.binary_search_by(|node| match &node.key { 78 | Key::String(s) => { 79 | // s[0].cmp(&bytes[0]) 80 | // opt! 81 | // lets `/` at end 82 | compare(s[0], bytes[0]) 83 | } 84 | Key::Parameter(_) => unreachable!(), 85 | }) { 86 | Ok(i) => nodes[i].insert_bytes(bytes), 87 | Err(i) => { 88 | nodes.insert(i, Node::new(Key::String(bytes.to_vec()), None)); 89 | &mut nodes[i] 90 | } 91 | }; 92 | } 93 | 94 | self 95 | } 96 | 97 | pub fn insert_parameter(&mut self, kind: Kind) -> &mut Self { 98 | let nodes = self.nodes1.get_or_insert_with(Vec::new); 99 | let i = nodes 100 | .binary_search_by(|node| match node.key { 101 | Key::Parameter(pk) => pk.cmp(&kind), 102 | Key::String(_) => unreachable!(), 103 | }) 104 | .unwrap_or_else(|i| { 105 | nodes.insert(i, Node::new(Key::Parameter(kind), None)); 106 | i 107 | }); 108 | &mut nodes[i] 109 | } 110 | 111 | #[allow(clippy::range_plus_one)] 112 | #[allow(clippy::too_many_lines)] 113 | #[inline] 114 | fn find_with( 115 | &self, 116 | mut start: usize, 117 | mut bytes: &[u8], 118 | ranges: &mut SmallVec<[Range; 8]>, 119 | ) -> Option<&T> { 120 | let mut m = bytes.len(); 121 | match &self.key { 122 | Key::String(s) => { 123 | let n = s.len(); 124 | let mut flag = m >= n; 125 | 126 | // opt! 127 | if flag { 128 | if n == 1 { 129 | flag = s[0] == bytes[0]; 130 | } else { 131 | flag = s == &bytes[..n]; 132 | } 133 | } 134 | 135 | // starts with prefix 136 | if flag { 137 | m -= n; 138 | start += n; 139 | bytes = &bytes[n..]; 140 | 141 | if m == 0 { 142 | if let Some(id) = &self.value { 143 | return Some(id); 144 | } 145 | } else { 146 | // static 147 | if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { 148 | nodes 149 | .binary_search_by(|node| match &node.key { 150 | Key::String(s) => { 151 | // s[0].cmp(&bytes[0]) 152 | // opt! 153 | // lets `/` at end 154 | compare(s[0], bytes[0]) 155 | } 156 | Key::Parameter(_) => unreachable!(), 157 | }) 158 | .ok() 159 | .and_then(|i| nodes[i].find_with(start, bytes, ranges)) 160 | }) { 161 | return Some(id); 162 | } 163 | } 164 | 165 | // parameter 166 | if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { 167 | let b = m > 0; 168 | nodes 169 | .iter() 170 | .filter(|node| match node.key { 171 | Key::Parameter(pk) 172 | if pk == Kind::Normal || pk == Kind::OneOrMore => 173 | { 174 | b 175 | } 176 | _ => true, 177 | }) 178 | .find_map(|node| node.find_with(start, bytes, ranges)) 179 | }) { 180 | return Some(id); 181 | } 182 | } else if n == 1 && s[0] == b'/' { 183 | if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { 184 | nodes 185 | .iter() 186 | .filter(|node| { 187 | matches!(node.key, 188 | Key::Parameter(pk) 189 | if pk == Kind::OptionalSegment 190 | || pk == Kind::ZeroOrMoreSegment 191 | ) 192 | }) 193 | .find_map(|node| node.find_with(start, bytes, ranges)) 194 | }) { 195 | return Some(id); 196 | } 197 | } 198 | } 199 | Key::Parameter(k) => match k { 200 | Kind::Normal | Kind::Optional | Kind::OptionalSegment => { 201 | if m == 0 { 202 | if k == &Kind::Normal { 203 | return None; 204 | } 205 | 206 | // last 207 | if self.nodes0.is_none() && self.nodes1.is_none() { 208 | return self.value.as_ref().inspect(|_| { 209 | ranges.push(start..start); 210 | }); 211 | } 212 | } else { 213 | // static 214 | if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { 215 | nodes.iter().find_map(|node| match &node.key { 216 | Key::String(s) => { 217 | let mut keep_running = true; 218 | bytes 219 | .iter() 220 | // as it turns out doing .copied() here is much slower than dereferencing in the closure 221 | // https://godbolt.org/z/7dnW91T1Y 222 | .take_while(|b| { 223 | if keep_running && **b == b'/' { 224 | keep_running = false; 225 | true 226 | } else { 227 | keep_running 228 | } 229 | }) 230 | .enumerate() 231 | .filter_map(|(n, b)| (s[0] == *b).then_some(n)) 232 | .find_map(|n| { 233 | node.find_with(start + n, &bytes[n..], ranges).inspect( 234 | |_| { 235 | ranges.push(start..start + n); 236 | }, 237 | ) 238 | }) 239 | } 240 | Key::Parameter(_) => unreachable!(), 241 | }) 242 | }) { 243 | return Some(id); 244 | } 245 | 246 | // parameter => `:a:b:c` 247 | if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { 248 | let b = m - 1 > 0; 249 | nodes 250 | .iter() 251 | .filter(|node| match node.key { 252 | Key::Parameter(pk) 253 | if pk == Kind::Normal || pk == Kind::OneOrMore => 254 | { 255 | b 256 | } 257 | _ => true, 258 | }) 259 | .find_map(|node| node.find_with(start + 1, &bytes[1..], ranges)) 260 | }) { 261 | ranges.push(start..start + 1); 262 | return Some(id); 263 | } 264 | } 265 | 266 | // parameter => `:a:b?:c?` 267 | if k == &Kind::Optional || k == &Kind::OptionalSegment { 268 | if let Some(id) = self.nodes1.as_ref().and_then(|nodes| { 269 | let b = m > 0; 270 | nodes 271 | .iter() 272 | .filter(|node| match &node.key { 273 | Key::Parameter(pk) 274 | if pk == &Kind::Normal || pk == &Kind::OneOrMore => 275 | { 276 | b 277 | } 278 | _ => true, 279 | }) 280 | .find_map(|node| node.find_with(start, bytes, ranges)) 281 | }) { 282 | // param should be empty 283 | ranges.push(start + m..start + m); 284 | return Some(id); 285 | } 286 | } 287 | 288 | if let Some(n) = bytes.iter().position(|b| *b == b'/') { 289 | bytes = &bytes[n..]; 290 | } else { 291 | if let Some(id) = &self.value { 292 | ranges.push(start..start + m); 293 | return Some(id); 294 | } 295 | bytes = &bytes[m..]; 296 | } 297 | 298 | if k == &Kind::OptionalSegment { 299 | if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { 300 | nodes 301 | .last() 302 | .filter(|node| match &node.key { 303 | Key::String(s) => s[0] == b'/', 304 | Key::Parameter(_) => unreachable!(), 305 | }) 306 | .and_then(|node| node.find_with(start, bytes, ranges)) 307 | }) { 308 | ranges.push(start..start + m); 309 | return Some(id); 310 | } 311 | } 312 | } 313 | Kind::OneOrMore | Kind::ZeroOrMore | Kind::ZeroOrMoreSegment => { 314 | let is_one_or_more = k == &Kind::OneOrMore; 315 | if m == 0 { 316 | if is_one_or_more { 317 | return None; 318 | } 319 | 320 | if self.nodes0.is_none() && self.nodes1.is_none() { 321 | return self.value.as_ref().inspect(|_| { 322 | ranges.push(start..start); 323 | }); 324 | } 325 | } else { 326 | if self.nodes0.is_none() && self.nodes1.is_none() { 327 | if let Some(id) = &self.value { 328 | ranges.push(start..start + m); 329 | return Some(id); 330 | } 331 | } 332 | 333 | // static 334 | if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { 335 | nodes.iter().find_map(|node| { 336 | if let Key::String(s) = &node.key { 337 | let right_length = if is_one_or_more { 338 | m > s.len() 339 | } else { 340 | m >= s.len() 341 | }; 342 | if right_length { 343 | return bytes 344 | .iter() 345 | .enumerate() 346 | .filter_map(|(n, b)| (s[0] == *b).then_some(n)) 347 | .find_map(|n| { 348 | node.find_with(start + n, &bytes[n..], ranges) 349 | .inspect(|_| { 350 | ranges.push(start..start + n); 351 | }) 352 | }); 353 | } 354 | } 355 | None 356 | }) 357 | }) { 358 | return Some(id); 359 | } 360 | } 361 | 362 | if k == &Kind::ZeroOrMoreSegment { 363 | if let Some(id) = self.nodes0.as_ref().and_then(|nodes| { 364 | nodes 365 | .last() 366 | .filter(|node| match &node.key { 367 | Key::String(s) => s[0] == b'/', 368 | Key::Parameter(_) => unreachable!(), 369 | }) 370 | .and_then(|node| node.find_with(start, bytes, ranges)) 371 | }) { 372 | // param should be empty 373 | ranges.push(start + m..start + m); 374 | return Some(id); 375 | } 376 | } 377 | } 378 | }, 379 | } 380 | None 381 | } 382 | 383 | pub fn find(&self, bytes: &[u8]) -> Option<(&T, SmallVec<[Range; 8]>)> { 384 | let mut ranges = SmallVec::<[Range; 8]>::new_const(); // opt! 385 | self.find_with(0, bytes, &mut ranges).map(|t| (t, ranges)) 386 | } 387 | } 388 | 389 | impl fmt::Debug for Node { 390 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 391 | const EDGE: &str = "├──"; 392 | const LINE: &str = "│ "; 393 | const CORNER: &str = "└──"; 394 | const BLANK: &str = " "; 395 | 396 | fn print_nodes( 397 | f: &mut fmt::Formatter<'_>, 398 | nodes: &[Node], 399 | check: bool, 400 | pad: &str, 401 | space: &str, 402 | ) -> fmt::Result { 403 | for (index, node) in nodes.iter().enumerate() { 404 | let (left, right) = if check && index == nodes.len() - 1 { 405 | (BLANK, CORNER) 406 | } else { 407 | (LINE, EDGE) 408 | }; 409 | f.write_str(pad)?; 410 | f.write_str(space)?; 411 | f.write_str(right)?; 412 | let mut s = String::new(); 413 | s.push_str(pad); 414 | s.push_str(space); 415 | s.push_str(left); 416 | print_tree(f, node, false, &s)?; 417 | } 418 | Ok(()) 419 | } 420 | 421 | fn print_tree( 422 | f: &mut fmt::Formatter<'_>, 423 | node: &Node, 424 | root: bool, 425 | pad: &str, 426 | ) -> fmt::Result { 427 | let space = if root { 428 | f.write_char('\n')?; 429 | "" 430 | } else { 431 | f.write_char(' ')?; 432 | " " 433 | }; 434 | match &node.key { 435 | Key::String(path) => { 436 | f.write_str( 437 | &String::from_utf8_lossy(path) 438 | .replace(':', "\\:") 439 | .replace('?', "\\?") 440 | .replace('+', "\\+"), 441 | )?; 442 | } 443 | Key::Parameter(kind) => { 444 | let c = match kind { 445 | Kind::Normal => ':', 446 | Kind::Optional => '?', 447 | Kind::OptionalSegment => { 448 | f.write_char('?')?; 449 | '?' 450 | } 451 | Kind::OneOrMore => '+', 452 | Kind::ZeroOrMore => '*', 453 | Kind::ZeroOrMoreSegment => { 454 | f.write_char('*')?; 455 | '*' 456 | } 457 | }; 458 | f.write_char(c)?; 459 | } 460 | } 461 | if let Some(value) = &node.value { 462 | f.write_str(" •")?; 463 | value.fmt(f)?; 464 | } 465 | f.write_char('\n')?; 466 | 467 | // nodes0 468 | if let Some(nodes) = &node.nodes0 { 469 | print_nodes(f, nodes, node.nodes1.is_none(), pad, space)?; 470 | } 471 | 472 | // nodes1 473 | if let Some(nodes) = &node.nodes1 { 474 | print_nodes(f, nodes, true, pad, space)?; 475 | } 476 | 477 | Ok(()) 478 | } 479 | 480 | print_tree(f, self, true, "") 481 | } 482 | } 483 | 484 | #[inline] 485 | fn compare(a: u8, b: u8) -> Ordering { 486 | if a == b { 487 | Ordering::Equal 488 | } else if a == b'/' { 489 | Ordering::Greater 490 | } else if b == b'/' { 491 | Ordering::Less 492 | } else { 493 | a.cmp(&b) 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use alloc::{string::ToString, vec::Vec}; 2 | use core::{iter::Peekable, str::CharIndices}; 3 | 4 | #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] 5 | pub enum Kind { 6 | /// `:` 58 7 | /// `:name` 8 | Normal, 9 | /// `?` 63 10 | /// Optional: `:name?-` 11 | Optional, 12 | /// Optional segment: `/:name?/` or `/:name?` 13 | OptionalSegment, 14 | // Optional, 15 | /// `+` 43 16 | OneOrMore, 17 | /// `*` 42 18 | /// Zero or more: `*-` 19 | ZeroOrMore, 20 | /// Zero or more segment: `/*/` or `/*` 21 | ZeroOrMoreSegment, 22 | // TODO: regexp 23 | } 24 | 25 | #[derive(Clone, Debug, Eq, PartialEq)] 26 | pub enum Piece { 27 | String(Vec), 28 | Parameter(Position, Kind), 29 | } 30 | 31 | #[derive(Clone, Debug, Eq, PartialEq)] 32 | pub enum Position { 33 | Index(usize, Vec), 34 | Named(Vec), 35 | } 36 | 37 | pub struct Parser<'a> { 38 | pos: usize, 39 | count: usize, 40 | input: &'a str, 41 | cursor: Peekable>, 42 | } 43 | 44 | impl<'a> Parser<'a> { 45 | #[must_use] 46 | pub fn new(input: &'a str) -> Self { 47 | Self { 48 | input, 49 | pos: 0, 50 | count: 0, 51 | cursor: input.char_indices().peekable(), 52 | } 53 | } 54 | 55 | fn string(&mut self) -> &'a [u8] { 56 | let mut start = self.pos; 57 | while let Some(&(i, c)) = self.cursor.peek() { 58 | match c { 59 | '\\' => { 60 | if start < i { 61 | self.pos = i; 62 | return &self.input.as_bytes()[start..i]; 63 | } 64 | 65 | self.cursor.next(); 66 | if let Some(&(j, c)) = self.cursor.peek() { 67 | // removes `\` 68 | if c == '\\' { 69 | start = j; 70 | } else { 71 | self.cursor.next(); 72 | self.pos = j + c.len_utf8(); 73 | return &self.input.as_bytes()[j..self.pos]; 74 | } 75 | } 76 | } 77 | ':' | '+' | '*' => { 78 | self.pos = i + 1; 79 | return &self.input.as_bytes()[start..i]; 80 | } 81 | _ => { 82 | self.cursor.next(); 83 | } 84 | } 85 | } 86 | 87 | &self.input.as_bytes()[start..] 88 | } 89 | 90 | fn parameter(&mut self) -> (Position, Kind) { 91 | let start = self.pos; 92 | while let Some(&(i, c)) = self.cursor.peek() { 93 | match c { 94 | '-' | '.' | '~' | '/' | '\\' | ':' => { 95 | self.pos = i; 96 | return ( 97 | Position::Named(self.input.as_bytes()[start..i].to_vec()), 98 | Kind::Normal, 99 | ); 100 | } 101 | '?' | '+' | '*' => { 102 | self.cursor.next(); 103 | self.pos = i + 1; 104 | return ( 105 | Position::Named(self.input.as_bytes()[start..i].to_vec()), 106 | if c == '+' { 107 | Kind::OneOrMore 108 | } else { 109 | let f = { 110 | let prefix = start >= 2 111 | && (self.input.get(start - 2..start - 1) == Some("/")); 112 | let suffix = self.cursor.peek().is_none_or(|(_, c)| *c == '/'); 113 | prefix && suffix 114 | }; 115 | if c == '?' { 116 | if f { 117 | Kind::OptionalSegment 118 | } else { 119 | Kind::Optional 120 | } 121 | } else if f { 122 | Kind::ZeroOrMoreSegment 123 | } else { 124 | Kind::ZeroOrMore 125 | } 126 | }, 127 | ); 128 | } 129 | _ => { 130 | self.cursor.next(); 131 | } 132 | } 133 | } 134 | 135 | ( 136 | Position::Named(self.input.as_bytes()[start..].to_vec()), 137 | Kind::Normal, 138 | ) 139 | } 140 | } 141 | 142 | impl Iterator for Parser<'_> { 143 | type Item = Piece; 144 | 145 | fn next(&mut self) -> Option { 146 | match self.cursor.peek() { 147 | Some(&(i, c)) => match c { 148 | ':' => { 149 | self.cursor.next(); 150 | self.pos = i + 1; 151 | let (position, kind) = self.parameter(); 152 | Some(Piece::Parameter(position, kind)) 153 | } 154 | '+' | '*' => { 155 | self.cursor.next(); 156 | self.count += 1; 157 | self.pos = i + 1; 158 | Some(Piece::Parameter( 159 | Position::Index(self.count, { 160 | let mut s = Vec::new(); 161 | s.push(c as u8); 162 | s.extend_from_slice(self.count.to_string().as_bytes()); 163 | s 164 | }), 165 | if c == '+' { 166 | Kind::OneOrMore 167 | } else { 168 | let f = { 169 | let prefix = i >= 1 && (self.input.get(i - 1..i) == Some("/")); 170 | let suffix = self.cursor.peek().is_none_or(|(_, c)| *c == '/'); 171 | prefix && suffix 172 | }; 173 | if f { 174 | Kind::ZeroOrMoreSegment 175 | } else { 176 | Kind::ZeroOrMore 177 | } 178 | }, 179 | )) 180 | } 181 | _ => Some(Piece::String(self.string().to_vec())), 182 | }, 183 | None => None, 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/fixtures/github.rs: -------------------------------------------------------------------------------- 1 | pub const ROUTES_WITH_COLON: [&str; 315] = [ 2 | "/app", 3 | "/app-manifests/:code/conversions", 4 | "/app/installations", 5 | "/app/installations/:installation_id", 6 | "/app/installations/:installation_id/access_tokens", 7 | "/applications/:client_id/grants/:access_token", 8 | "/applications/:client_id/tokens/:access_token", 9 | "/applications/grants", 10 | "/applications/grants/:grant_id", 11 | "/apps/:app_slug", 12 | "/authorizations", 13 | "/authorizations/:authorization_id", 14 | "/authorizations/clients/:client_id", 15 | "/authorizations/clients/:client_id/:fingerprint", 16 | "/codes_of_conduct", 17 | "/codes_of_conduct/:key", 18 | "/content_references/:content_reference_id/attachments", 19 | "/emojis", 20 | "/events", 21 | "/feeds", 22 | "/gists", 23 | "/gists/public", 24 | "/gists/starred", 25 | "/gists/:gist_id", 26 | "/gists/:gist_id/comments", 27 | "/gists/:gist_id/comments/:comment_id", 28 | "/gists/:gist_id/commits", 29 | "/gists/:gist_id/forks", 30 | "/gists/:gist_id/star", 31 | "/gists/:gist_id/:sha", 32 | "/gitignore/templates", 33 | "/gitignore/templates/:name", 34 | "/installation/repositories", 35 | "/issues", 36 | "/legacy/issues/search/:owner/:repository/:state/:keyword", 37 | "/legacy/repos/search/:keyword", 38 | "/legacy/user/email/:email", 39 | "/legacy/user/search/:keyword", 40 | "/licenses", 41 | "/licenses/:license", 42 | "/markdown", 43 | "/markdown/raw", 44 | "/marketplace_listing/accounts/:account_id", 45 | "/marketplace_listing/plans", 46 | "/marketplace_listing/plans/:plan_id/accounts", 47 | "/marketplace_listing/stubbed/accounts/:account_id", 48 | "/marketplace_listing/stubbed/plans", 49 | "/marketplace_listing/stubbed/plans/:plan_id/accounts", 50 | "/meta", 51 | "/networks/:owner/:repo/events", 52 | "/notifications", 53 | "/notifications/threads/:thread_id", 54 | "/notifications/threads/:thread_id/subscription", 55 | "/organizations", 56 | "/orgs/:org", 57 | "/orgs/:org/blocks", 58 | "/orgs/:org/blocks/:username", 59 | "/orgs/:org/events", 60 | "/orgs/:org/hooks", 61 | "/orgs/:org/hooks/:hook_id", 62 | "/orgs/:org/hooks/:hook_id/pings", 63 | "/orgs/:org/installation", 64 | "/orgs/:org/interaction-limits", 65 | "/orgs/:org/invitations", 66 | "/orgs/:org/invitations/:invitation_id/teams", 67 | "/orgs/:org/issues", 68 | "/orgs/:org/members", 69 | "/orgs/:org/members/:username", 70 | "/orgs/:org/memberships/:username", 71 | "/orgs/:org/migrations", 72 | "/orgs/:org/migrations/:migration_id", 73 | "/orgs/:org/migrations/:migration_id/archive", 74 | "/orgs/:org/migrations/:migration_id/repos/:repo_name/lock", 75 | "/orgs/:org/outside_collaborators", 76 | "/orgs/:org/outside_collaborators/:username", 77 | "/orgs/:org/projects", 78 | "/orgs/:org/public_members", 79 | "/orgs/:org/public_members/:username", 80 | "/orgs/:org/repos", 81 | "/orgs/:org/teams", 82 | "/projects/:project_id", 83 | "/projects/:project_id/collaborators", 84 | "/projects/:project_id/collaborators/:username", 85 | "/projects/:project_id/collaborators/:username/permission", 86 | "/projects/:project_id/columns", 87 | "/projects/columns/cards/:card_id", 88 | "/projects/columns/cards/:card_id/moves", 89 | "/projects/columns/:column_id", 90 | "/projects/columns/:column_id/cards", 91 | "/projects/columns/:column_id/moves", 92 | "/rate_limit", 93 | "/reactions/:reaction_id", 94 | "/repos/:owner/:repo", 95 | "/repos/:owner/:repo/assignees", 96 | "/repos/:owner/:repo/assignees/:assignee", 97 | "/repos/:owner/:repo/branches", 98 | "/repos/:owner/:repo/branches/:branch", 99 | "/repos/:owner/:repo/branches/:branch/protection", 100 | "/repos/:owner/:repo/branches/:branch/protection/enforce_admins", 101 | "/repos/:owner/:repo/branches/:branch/protection/required_pull_request_reviews", 102 | "/repos/:owner/:repo/branches/:branch/protection/required_signatures", 103 | "/repos/:owner/:repo/branches/:branch/protection/required_status_checks", 104 | "/repos/:owner/:repo/branches/:branch/protection/required_status_checks/contexts", 105 | "/repos/:owner/:repo/branches/:branch/protection/restrictions", 106 | "/repos/:owner/:repo/branches/:branch/protection/restrictions/teams", 107 | "/repos/:owner/:repo/branches/:branch/protection/restrictions/users", 108 | "/repos/:owner/:repo/check-runs", 109 | "/repos/:owner/:repo/check-runs/:check_run_id", 110 | "/repos/:owner/:repo/check-runs/:check_run_id/annotations", 111 | "/repos/:owner/:repo/check-suites", 112 | "/repos/:owner/:repo/check-suites/preferences", 113 | "/repos/:owner/:repo/check-suites/:check_suite_id", 114 | "/repos/:owner/:repo/check-suites/:check_suite_id/check-runs", 115 | "/repos/:owner/:repo/check-suites/:check_suite_id/rerequest", 116 | "/repos/:owner/:repo/collaborators", 117 | "/repos/:owner/:repo/collaborators/:username", 118 | "/repos/:owner/:repo/collaborators/:username/permission", 119 | "/repos/:owner/:repo/comments", 120 | "/repos/:owner/:repo/comments/:comment_id", 121 | "/repos/:owner/:repo/comments/:comment_id/reactions", 122 | "/repos/:owner/:repo/commits", 123 | "/repos/:owner/:repo/commits/:ref", 124 | "/repos/:owner/:repo/commits/:ref/check-runs", 125 | "/repos/:owner/:repo/commits/:ref/check-suites", 126 | "/repos/:owner/:repo/commits/:ref/comments", 127 | "/repos/:owner/:repo/commits/:ref/status", 128 | "/repos/:owner/:repo/commits/:ref/statuses", 129 | // "/repos/:owner/:repo/commits/:sha", 130 | // "/repos/:owner/:repo/commits/:sha/comments", 131 | "/repos/:owner/:repo/community/code_of_conduct", 132 | "/repos/:owner/:repo/community/profile", 133 | // "/repos/:owner/:repo/compare/:base...:head", 134 | "/repos/:owner/:repo/contents/:path", 135 | "/repos/:owner/:repo/contributors", 136 | "/repos/:owner/:repo/deployments", 137 | "/repos/:owner/:repo/deployments/:deployment_id", 138 | "/repos/:owner/:repo/deployments/:deployment_id/statuses", 139 | "/repos/:owner/:repo/deployments/:deployment_id/statuses/:status_id", 140 | "/repos/:owner/:repo/downloads", 141 | "/repos/:owner/:repo/downloads/:download_id", 142 | "/repos/:owner/:repo/events", 143 | "/repos/:owner/:repo/forks", 144 | "/repos/:owner/:repo/git/blobs", 145 | "/repos/:owner/:repo/git/blobs/:file_sha", 146 | "/repos/:owner/:repo/git/commits", 147 | "/repos/:owner/:repo/git/commits/:commit_sha", 148 | "/repos/:owner/:repo/git/refs", 149 | // "/repos/:owner/:repo/git/refs/:namespace", 150 | "/repos/:owner/:repo/git/refs/:ref", 151 | "/repos/:owner/:repo/git/tags", 152 | "/repos/:owner/:repo/git/tags/:tag_sha", 153 | "/repos/:owner/:repo/git/trees", 154 | "/repos/:owner/:repo/git/trees/:tree_sha", 155 | "/repos/:owner/:repo/hooks", 156 | "/repos/:owner/:repo/hooks/:hook_id", 157 | "/repos/:owner/:repo/hooks/:hook_id/pings", 158 | "/repos/:owner/:repo/hooks/:hook_id/tests", 159 | "/repos/:owner/:repo/import", 160 | "/repos/:owner/:repo/import/authors", 161 | "/repos/:owner/:repo/import/authors/:author_id", 162 | "/repos/:owner/:repo/import/large_files", 163 | "/repos/:owner/:repo/import/lfs", 164 | "/repos/:owner/:repo/installation", 165 | "/repos/:owner/:repo/interaction-limits", 166 | "/repos/:owner/:repo/invitations", 167 | "/repos/:owner/:repo/invitations/:invitation_id", 168 | "/repos/:owner/:repo/issues", 169 | "/repos/:owner/:repo/issues/comments", 170 | "/repos/:owner/:repo/issues/comments/:comment_id", 171 | "/repos/:owner/:repo/issues/comments/:comment_id/reactions", 172 | "/repos/:owner/:repo/issues/events", 173 | "/repos/:owner/:repo/issues/events/:event_id", 174 | "/repos/:owner/:repo/issues/:number", 175 | "/repos/:owner/:repo/issues/:number/assignees", 176 | "/repos/:owner/:repo/issues/:number/comments", 177 | "/repos/:owner/:repo/issues/:number/events", 178 | "/repos/:owner/:repo/issues/:number/labels", 179 | "/repos/:owner/:repo/issues/:number/labels/:name", 180 | "/repos/:owner/:repo/issues/:number/lock", 181 | "/repos/:owner/:repo/issues/:number/reactions", 182 | "/repos/:owner/:repo/issues/:number/timeline", 183 | "/repos/:owner/:repo/keys", 184 | "/repos/:owner/:repo/keys/:key_id", 185 | "/repos/:owner/:repo/labels", 186 | // "/repos/:owner/:repo/labels/:current_name", 187 | "/repos/:owner/:repo/labels/:name", 188 | "/repos/:owner/:repo/languages", 189 | "/repos/:owner/:repo/license", 190 | "/repos/:owner/:repo/merges", 191 | "/repos/:owner/:repo/milestones", 192 | "/repos/:owner/:repo/milestones/:number", 193 | "/repos/:owner/:repo/milestones/:number/labels", 194 | "/repos/:owner/:repo/notifications", 195 | "/repos/:owner/:repo/pages", 196 | "/repos/:owner/:repo/pages/builds", 197 | "/repos/:owner/:repo/pages/builds/latest", 198 | "/repos/:owner/:repo/pages/builds/:build_id", 199 | "/repos/:owner/:repo/projects", 200 | "/repos/:owner/:repo/pulls", 201 | "/repos/:owner/:repo/pulls/comments", 202 | "/repos/:owner/:repo/pulls/comments/:comment_id", 203 | "/repos/:owner/:repo/pulls/comments/:comment_id/reactions", 204 | "/repos/:owner/:repo/pulls/:number", 205 | "/repos/:owner/:repo/pulls/:number/comments", 206 | "/repos/:owner/:repo/pulls/:number/commits", 207 | "/repos/:owner/:repo/pulls/:number/files", 208 | "/repos/:owner/:repo/pulls/:number/merge", 209 | "/repos/:owner/:repo/pulls/:number/requested_reviewers", 210 | "/repos/:owner/:repo/pulls/:number/reviews", 211 | "/repos/:owner/:repo/pulls/:number/reviews/:review_id", 212 | "/repos/:owner/:repo/pulls/:number/reviews/:review_id/comments", 213 | "/repos/:owner/:repo/pulls/:number/reviews/:review_id/dismissals", 214 | "/repos/:owner/:repo/pulls/:number/reviews/:review_id/events", 215 | "/repos/:owner/:repo/readme", 216 | "/repos/:owner/:repo/releases", 217 | "/repos/:owner/:repo/releases/assets/:asset_id", 218 | "/repos/:owner/:repo/releases/latest", 219 | "/repos/:owner/:repo/releases/tags/:tag", 220 | "/repos/:owner/:repo/releases/:release_id", 221 | "/repos/:owner/:repo/releases/:release_id/assets", 222 | "/repos/:owner/:repo/stargazers", 223 | "/repos/:owner/:repo/stats/code_frequency", 224 | "/repos/:owner/:repo/stats/commit_activity", 225 | "/repos/:owner/:repo/stats/contributors", 226 | "/repos/:owner/:repo/stats/participation", 227 | "/repos/:owner/:repo/stats/punch_card", 228 | "/repos/:owner/:repo/statuses/:sha", 229 | "/repos/:owner/:repo/subscribers", 230 | "/repos/:owner/:repo/subscription", 231 | "/repos/:owner/:repo/tags", 232 | "/repos/:owner/:repo/teams", 233 | "/repos/:owner/:repo/topics", 234 | "/repos/:owner/:repo/traffic/clones", 235 | "/repos/:owner/:repo/traffic/popular/paths", 236 | "/repos/:owner/:repo/traffic/popular/referrers", 237 | "/repos/:owner/:repo/traffic/views", 238 | "/repos/:owner/:repo/transfer", 239 | "/repos/:owner/:repo/:archive_format/:ref", 240 | "/repositories", 241 | "/scim/v2/organizations/:org/Users", 242 | "/scim/v2/organizations/:org/Users/:external_identity_guid", 243 | "/search/code", 244 | "/search/commits", 245 | "/search/issues", 246 | "/search/labels", 247 | "/search/repositories", 248 | "/search/topics", 249 | "/search/users", 250 | "/teams/:team_id", 251 | "/teams/:team_id/discussions", 252 | "/teams/:team_id/discussions/:discussion_number", 253 | "/teams/:team_id/discussions/:discussion_number/comments", 254 | "/teams/:team_id/discussions/:discussion_number/comments/:comment_number", 255 | "/teams/:team_id/discussions/:discussion_number/comments/:comment_number/reactions", 256 | "/teams/:team_id/discussions/:discussion_number/reactions", 257 | "/teams/:team_id/invitations", 258 | "/teams/:team_id/members", 259 | "/teams/:team_id/members/:username", 260 | "/teams/:team_id/memberships/:username", 261 | "/teams/:team_id/projects", 262 | "/teams/:team_id/projects/:project_id", 263 | "/teams/:team_id/repos", 264 | "/teams/:team_id/repos/:owner/:repo", 265 | "/teams/:team_id/teams", 266 | "/user", 267 | "/user/blocks", 268 | "/user/blocks/:username", 269 | "/user/email/visibility", 270 | "/user/emails", 271 | "/user/followers", 272 | "/user/following", 273 | "/user/following/:username", 274 | "/user/gpg_keys", 275 | "/user/gpg_keys/:gpg_key_id", 276 | "/user/installations", 277 | "/user/installations/:installation_id/repositories", 278 | "/user/installations/:installation_id/repositories/:repository_id", 279 | "/user/issues", 280 | "/user/keys", 281 | "/user/keys/:key_id", 282 | "/user/marketplace_purchases", 283 | "/user/marketplace_purchases/stubbed", 284 | "/user/memberships/orgs", 285 | "/user/memberships/orgs/:org", 286 | "/user/migrations", 287 | "/user/migrations/:migration_id", 288 | "/user/migrations/:migration_id/archive", 289 | "/user/migrations/:migration_id/repos/:repo_name/lock", 290 | "/user/orgs", 291 | "/user/projects", 292 | "/user/public_emails", 293 | "/user/repos", 294 | "/user/repository_invitations", 295 | "/user/repository_invitations/:invitation_id", 296 | "/user/starred", 297 | "/user/starred/:owner/:repo", 298 | "/user/subscriptions", 299 | "/user/subscriptions/:owner/:repo", 300 | "/user/teams", 301 | "/users", 302 | "/users/:username", 303 | "/users/:username/events", 304 | "/users/:username/events/orgs/:org", 305 | "/users/:username/events/public", 306 | "/users/:username/followers", 307 | "/users/:username/following", 308 | "/users/:username/following/:target_user", 309 | "/users/:username/gists", 310 | "/users/:username/gpg_keys", 311 | "/users/:username/hovercard", 312 | "/users/:username/installation", 313 | "/users/:username/keys", 314 | "/users/:username/orgs", 315 | "/users/:username/projects", 316 | "/users/:username/received_events", 317 | "/users/:username/received_events/public", 318 | "/users/:username/repos", 319 | "/users/:username/starred", 320 | "/users/:username/subscriptions", 321 | "/:url", 322 | ]; 323 | 324 | pub const ROUTES_WITH_BRACES: [&str; 315] = [ 325 | "/app", 326 | "/app-manifests/{code}/conversions", 327 | "/app/installations", 328 | "/app/installations/{installation_id}", 329 | "/app/installations/{installation_id}/access_tokens", 330 | "/applications/{client_id}/grants/{access_token}", 331 | "/applications/{client_id}/tokens/{access_token}", 332 | "/applications/grants", 333 | "/applications/grants/{grant_id}", 334 | "/apps/{app_slug}", 335 | "/authorizations", 336 | "/authorizations/{authorization_id}", 337 | "/authorizations/clients/{client_id}", 338 | "/authorizations/clients/{client_id}/{fingerprint}", 339 | "/codes_of_conduct", 340 | "/codes_of_conduct/{key}", 341 | "/content_references/{content_reference_id}/attachments", 342 | "/emojis", 343 | "/events", 344 | "/feeds", 345 | "/gists", 346 | "/gists/public", 347 | "/gists/starred", 348 | "/gists/{gist_id}", 349 | "/gists/{gist_id}/comments", 350 | "/gists/{gist_id}/comments/{comment_id}", 351 | "/gists/{gist_id}/commits", 352 | "/gists/{gist_id}/forks", 353 | "/gists/{gist_id}/star", 354 | "/gists/{gist_id}/{sha}", 355 | "/gitignore/templates", 356 | "/gitignore/templates/{name}", 357 | "/installation/repositories", 358 | "/issues", 359 | "/legacy/issues/search/{owner}/{repository}/{state}/{keyword}", 360 | "/legacy/repos/search/{keyword}", 361 | "/legacy/user/email/{email}", 362 | "/legacy/user/search/{keyword}", 363 | "/licenses", 364 | "/licenses/{license}", 365 | "/markdown", 366 | "/markdown/raw", 367 | "/marketplace_listing/accounts/{account_id}", 368 | "/marketplace_listing/plans", 369 | "/marketplace_listing/plans/{plan_id}/accounts", 370 | "/marketplace_listing/stubbed/accounts/{account_id}", 371 | "/marketplace_listing/stubbed/plans", 372 | "/marketplace_listing/stubbed/plans/{plan_id}/accounts", 373 | "/meta", 374 | "/networks/{owner}/{repo}/events", 375 | "/notifications", 376 | "/notifications/threads/{thread_id}", 377 | "/notifications/threads/{thread_id}/subscription", 378 | "/organizations", 379 | "/orgs/{org}", 380 | "/orgs/{org}/blocks", 381 | "/orgs/{org}/blocks/{username}", 382 | "/orgs/{org}/events", 383 | "/orgs/{org}/hooks", 384 | "/orgs/{org}/hooks/{hook_id}", 385 | "/orgs/{org}/hooks/{hook_id}/pings", 386 | "/orgs/{org}/installation", 387 | "/orgs/{org}/interaction-limits", 388 | "/orgs/{org}/invitations", 389 | "/orgs/{org}/invitations/{invitation_id}/teams", 390 | "/orgs/{org}/issues", 391 | "/orgs/{org}/members", 392 | "/orgs/{org}/members/{username}", 393 | "/orgs/{org}/memberships/{username}", 394 | "/orgs/{org}/migrations", 395 | "/orgs/{org}/migrations/{migration_id}", 396 | "/orgs/{org}/migrations/{migration_id}/archive", 397 | "/orgs/{org}/migrations/{migration_id}/repos/{repo_name}/lock", 398 | "/orgs/{org}/outside_collaborators", 399 | "/orgs/{org}/outside_collaborators/{username}", 400 | "/orgs/{org}/projects", 401 | "/orgs/{org}/public_members", 402 | "/orgs/{org}/public_members/{username}", 403 | "/orgs/{org}/repos", 404 | "/orgs/{org}/teams", 405 | "/projects/{project_id}", 406 | "/projects/{project_id}/collaborators", 407 | "/projects/{project_id}/collaborators/{username}", 408 | "/projects/{project_id}/collaborators/{username}/permission", 409 | "/projects/{project_id}/columns", 410 | "/projects/columns/cards/{card_id}", 411 | "/projects/columns/cards/{card_id}/moves", 412 | "/projects/columns/{column_id}", 413 | "/projects/columns/{column_id}/cards", 414 | "/projects/columns/{column_id}/moves", 415 | "/rate_limit", 416 | "/reactions/{reaction_id}", 417 | "/repos/{owner}/{repo}", 418 | "/repos/{owner}/{repo}/assignees", 419 | "/repos/{owner}/{repo}/assignees/{assignee}", 420 | "/repos/{owner}/{repo}/branches", 421 | "/repos/{owner}/{repo}/branches/{branch}", 422 | "/repos/{owner}/{repo}/branches/{branch}/protection", 423 | "/repos/{owner}/{repo}/branches/{branch}/protection/enforce_admins", 424 | "/repos/{owner}/{repo}/branches/{branch}/protection/required_pull_request_reviews", 425 | "/repos/{owner}/{repo}/branches/{branch}/protection/required_signatures", 426 | "/repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks", 427 | "/repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks/contexts", 428 | "/repos/{owner}/{repo}/branches/{branch}/protection/restrictions", 429 | "/repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams", 430 | "/repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users", 431 | "/repos/{owner}/{repo}/check-runs", 432 | "/repos/{owner}/{repo}/check-runs/{check_run_id}", 433 | "/repos/{owner}/{repo}/check-runs/{check_run_id}/annotations", 434 | "/repos/{owner}/{repo}/check-suites", 435 | "/repos/{owner}/{repo}/check-suites/preferences", 436 | "/repos/{owner}/{repo}/check-suites/{check_suite_id}", 437 | "/repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs", 438 | "/repos/{owner}/{repo}/check-suites/{check_suite_id}/rerequest", 439 | "/repos/{owner}/{repo}/collaborators", 440 | "/repos/{owner}/{repo}/collaborators/{username}", 441 | "/repos/{owner}/{repo}/collaborators/{username}/permission", 442 | "/repos/{owner}/{repo}/comments", 443 | "/repos/{owner}/{repo}/comments/{comment_id}", 444 | "/repos/{owner}/{repo}/comments/{comment_id}/reactions", 445 | "/repos/{owner}/{repo}/commits", 446 | "/repos/{owner}/{repo}/commits/{ref}", 447 | "/repos/{owner}/{repo}/commits/{ref}/check-runs", 448 | "/repos/{owner}/{repo}/commits/{ref}/check-suites", 449 | "/repos/{owner}/{repo}/commits/{ref}/comments", 450 | "/repos/{owner}/{repo}/commits/{ref}/status", 451 | "/repos/{owner}/{repo}/commits/{ref}/statuses", 452 | // "/repos/{owner}/{repo}/commits/{sha}", 453 | // "/repos/{owner}/{repo}/commits/{sha}/comments", 454 | "/repos/{owner}/{repo}/community/code_of_conduct", 455 | "/repos/{owner}/{repo}/community/profile", 456 | // "/repos/{owner}/{repo}/compare/{base}...{head}", 457 | "/repos/{owner}/{repo}/contents/{path}", 458 | "/repos/{owner}/{repo}/contributors", 459 | "/repos/{owner}/{repo}/deployments", 460 | "/repos/{owner}/{repo}/deployments/{deployment_id}", 461 | "/repos/{owner}/{repo}/deployments/{deployment_id}/statuses", 462 | "/repos/{owner}/{repo}/deployments/{deployment_id}/statuses/{status_id}", 463 | "/repos/{owner}/{repo}/downloads", 464 | "/repos/{owner}/{repo}/downloads/{download_id}", 465 | "/repos/{owner}/{repo}/events", 466 | "/repos/{owner}/{repo}/forks", 467 | "/repos/{owner}/{repo}/git/blobs", 468 | "/repos/{owner}/{repo}/git/blobs/{file_sha}", 469 | "/repos/{owner}/{repo}/git/commits", 470 | "/repos/{owner}/{repo}/git/commits/{commit_sha}", 471 | "/repos/{owner}/{repo}/git/refs", 472 | // "/repos/{owner}/{repo}/git/refs/{namespace}", 473 | "/repos/{owner}/{repo}/git/refs/{ref}", 474 | "/repos/{owner}/{repo}/git/tags", 475 | "/repos/{owner}/{repo}/git/tags/{tag_sha}", 476 | "/repos/{owner}/{repo}/git/trees", 477 | "/repos/{owner}/{repo}/git/trees/{tree_sha}", 478 | "/repos/{owner}/{repo}/hooks", 479 | "/repos/{owner}/{repo}/hooks/{hook_id}", 480 | "/repos/{owner}/{repo}/hooks/{hook_id}/pings", 481 | "/repos/{owner}/{repo}/hooks/{hook_id}/tests", 482 | "/repos/{owner}/{repo}/import", 483 | "/repos/{owner}/{repo}/import/authors", 484 | "/repos/{owner}/{repo}/import/authors/{author_id}", 485 | "/repos/{owner}/{repo}/import/large_files", 486 | "/repos/{owner}/{repo}/import/lfs", 487 | "/repos/{owner}/{repo}/installation", 488 | "/repos/{owner}/{repo}/interaction-limits", 489 | "/repos/{owner}/{repo}/invitations", 490 | "/repos/{owner}/{repo}/invitations/{invitation_id}", 491 | "/repos/{owner}/{repo}/issues", 492 | "/repos/{owner}/{repo}/issues/comments", 493 | "/repos/{owner}/{repo}/issues/comments/{comment_id}", 494 | "/repos/{owner}/{repo}/issues/comments/{comment_id}/reactions", 495 | "/repos/{owner}/{repo}/issues/events", 496 | "/repos/{owner}/{repo}/issues/events/{event_id}", 497 | "/repos/{owner}/{repo}/issues/{number}", 498 | "/repos/{owner}/{repo}/issues/{number}/assignees", 499 | "/repos/{owner}/{repo}/issues/{number}/comments", 500 | "/repos/{owner}/{repo}/issues/{number}/events", 501 | "/repos/{owner}/{repo}/issues/{number}/labels", 502 | "/repos/{owner}/{repo}/issues/{number}/labels/{name}", 503 | "/repos/{owner}/{repo}/issues/{number}/lock", 504 | "/repos/{owner}/{repo}/issues/{number}/reactions", 505 | "/repos/{owner}/{repo}/issues/{number}/timeline", 506 | "/repos/{owner}/{repo}/keys", 507 | "/repos/{owner}/{repo}/keys/{key_id}", 508 | "/repos/{owner}/{repo}/labels", 509 | // "/repos/{owner}/{repo}/labels/{current_name}", 510 | "/repos/{owner}/{repo}/labels/{name}", 511 | "/repos/{owner}/{repo}/languages", 512 | "/repos/{owner}/{repo}/license", 513 | "/repos/{owner}/{repo}/merges", 514 | "/repos/{owner}/{repo}/milestones", 515 | "/repos/{owner}/{repo}/milestones/{number}", 516 | "/repos/{owner}/{repo}/milestones/{number}/labels", 517 | "/repos/{owner}/{repo}/notifications", 518 | "/repos/{owner}/{repo}/pages", 519 | "/repos/{owner}/{repo}/pages/builds", 520 | "/repos/{owner}/{repo}/pages/builds/latest", 521 | "/repos/{owner}/{repo}/pages/builds/{build_id}", 522 | "/repos/{owner}/{repo}/projects", 523 | "/repos/{owner}/{repo}/pulls", 524 | "/repos/{owner}/{repo}/pulls/comments", 525 | "/repos/{owner}/{repo}/pulls/comments/{comment_id}", 526 | "/repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions", 527 | "/repos/{owner}/{repo}/pulls/{number}", 528 | "/repos/{owner}/{repo}/pulls/{number}/comments", 529 | "/repos/{owner}/{repo}/pulls/{number}/commits", 530 | "/repos/{owner}/{repo}/pulls/{number}/files", 531 | "/repos/{owner}/{repo}/pulls/{number}/merge", 532 | "/repos/{owner}/{repo}/pulls/{number}/requested_reviewers", 533 | "/repos/{owner}/{repo}/pulls/{number}/reviews", 534 | "/repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}", 535 | "/repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/comments", 536 | "/repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals", 537 | "/repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/events", 538 | "/repos/{owner}/{repo}/readme", 539 | "/repos/{owner}/{repo}/releases", 540 | "/repos/{owner}/{repo}/releases/assets/{asset_id}", 541 | "/repos/{owner}/{repo}/releases/latest", 542 | "/repos/{owner}/{repo}/releases/tags/{tag}", 543 | "/repos/{owner}/{repo}/releases/{release_id}", 544 | "/repos/{owner}/{repo}/releases/{release_id}/assets", 545 | "/repos/{owner}/{repo}/stargazers", 546 | "/repos/{owner}/{repo}/stats/code_frequency", 547 | "/repos/{owner}/{repo}/stats/commit_activity", 548 | "/repos/{owner}/{repo}/stats/contributors", 549 | "/repos/{owner}/{repo}/stats/participation", 550 | "/repos/{owner}/{repo}/stats/punch_card", 551 | "/repos/{owner}/{repo}/statuses/{sha}", 552 | "/repos/{owner}/{repo}/subscribers", 553 | "/repos/{owner}/{repo}/subscription", 554 | "/repos/{owner}/{repo}/tags", 555 | "/repos/{owner}/{repo}/teams", 556 | "/repos/{owner}/{repo}/topics", 557 | "/repos/{owner}/{repo}/traffic/clones", 558 | "/repos/{owner}/{repo}/traffic/popular/paths", 559 | "/repos/{owner}/{repo}/traffic/popular/referrers", 560 | "/repos/{owner}/{repo}/traffic/views", 561 | "/repos/{owner}/{repo}/transfer", 562 | "/repos/{owner}/{repo}/{archive_format}/{ref}", 563 | "/repositories", 564 | "/scim/v2/organizations/{org}/Users", 565 | "/scim/v2/organizations/{org}/Users/{external_identity_guid}", 566 | "/search/code", 567 | "/search/commits", 568 | "/search/issues", 569 | "/search/labels", 570 | "/search/repositories", 571 | "/search/topics", 572 | "/search/users", 573 | "/teams/{team_id}", 574 | "/teams/{team_id}/discussions", 575 | "/teams/{team_id}/discussions/{discussion_number}", 576 | "/teams/{team_id}/discussions/{discussion_number}/comments", 577 | "/teams/{team_id}/discussions/{discussion_number}/comments/{comment_number}", 578 | "/teams/{team_id}/discussions/{discussion_number}/comments/{comment_number}/reactions", 579 | "/teams/{team_id}/discussions/{discussion_number}/reactions", 580 | "/teams/{team_id}/invitations", 581 | "/teams/{team_id}/members", 582 | "/teams/{team_id}/members/{username}", 583 | "/teams/{team_id}/memberships/{username}", 584 | "/teams/{team_id}/projects", 585 | "/teams/{team_id}/projects/{project_id}", 586 | "/teams/{team_id}/repos", 587 | "/teams/{team_id}/repos/{owner}/{repo}", 588 | "/teams/{team_id}/teams", 589 | "/user", 590 | "/user/blocks", 591 | "/user/blocks/{username}", 592 | "/user/email/visibility", 593 | "/user/emails", 594 | "/user/followers", 595 | "/user/following", 596 | "/user/following/{username}", 597 | "/user/gpg_keys", 598 | "/user/gpg_keys/{gpg_key_id}", 599 | "/user/installations", 600 | "/user/installations/{installation_id}/repositories", 601 | "/user/installations/{installation_id}/repositories/{repository_id}", 602 | "/user/issues", 603 | "/user/keys", 604 | "/user/keys/{key_id}", 605 | "/user/marketplace_purchases", 606 | "/user/marketplace_purchases/stubbed", 607 | "/user/memberships/orgs", 608 | "/user/memberships/orgs/{org}", 609 | "/user/migrations", 610 | "/user/migrations/{migration_id}", 611 | "/user/migrations/{migration_id}/archive", 612 | "/user/migrations/{migration_id}/repos/{repo_name}/lock", 613 | "/user/orgs", 614 | "/user/projects", 615 | "/user/public_emails", 616 | "/user/repos", 617 | "/user/repository_invitations", 618 | "/user/repository_invitations/{invitation_id}", 619 | "/user/starred", 620 | "/user/starred/{owner}/{repo}", 621 | "/user/subscriptions", 622 | "/user/subscriptions/{owner}/{repo}", 623 | "/user/teams", 624 | "/users", 625 | "/users/{username}", 626 | "/users/{username}/events", 627 | "/users/{username}/events/orgs/{org}", 628 | "/users/{username}/events/public", 629 | "/users/{username}/followers", 630 | "/users/{username}/following", 631 | "/users/{username}/following/{target_user}", 632 | "/users/{username}/gists", 633 | "/users/{username}/gpg_keys", 634 | "/users/{username}/hovercard", 635 | "/users/{username}/installation", 636 | "/users/{username}/keys", 637 | "/users/{username}/orgs", 638 | "/users/{username}/projects", 639 | "/users/{username}/received_events", 640 | "/users/{username}/received_events/public", 641 | "/users/{username}/repos", 642 | "/users/{username}/starred", 643 | "/users/{username}/subscriptions", 644 | "/{url}", 645 | ]; 646 | 647 | pub const ROUTES_URLS: [&str; 315] = [ 648 | "/app", 649 | "/app-manifests/0/conversions", 650 | "/app/installations", 651 | "/app/installations/12345", 652 | "/app/installations/12345/access_tokens", 653 | "/applications/67890/grants/777cc25bacbd7cd4f4ddd631006a998fa74b5ed3", 654 | "/applications/67890/tokens/777cc25bacbd7cd4f4ddd631006a998fa74b5ed3", 655 | "/applications/grants", 656 | "/applications/grants/1", 657 | "/apps/rust-lang", 658 | "/authorizations", 659 | "/authorizations/144", 660 | "/authorizations/clients/67890", 661 | "/authorizations/clients/67890/233", 662 | "/codes_of_conduct", 663 | "/codes_of_conduct/377", 664 | "/content_references/610/attachments", 665 | "/emojis", 666 | "/events", 667 | "/feeds", 668 | "/gists", 669 | "/gists/public", 670 | "/gists/starred", 671 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4", 672 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/comments", 673 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/comments/2274119", 674 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/commits", 675 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/forks", 676 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/star", 677 | "/gists/7c220d4d6d95b389816bc9d3fbb7a5d4/2aa4c46cfdd726e97360c2734835aa3515e8c858", 678 | "/gitignore/templates", 679 | "/gitignore/templates/rust", 680 | "/installation/repositories", 681 | "/issues", 682 | "/legacy/issues/search/rust-lang/rust/987/1597", 683 | "/legacy/repos/search/1597", 684 | "/legacy/user/email/rust@rust-lang.org", 685 | "/legacy/user/search/1597", 686 | "/licenses", 687 | "/licenses/mit", 688 | "/markdown", 689 | "/markdown/raw", 690 | "/marketplace_listing/accounts/rust-lang", 691 | "/marketplace_listing/plans", 692 | "/marketplace_listing/plans/987/accounts", 693 | "/marketplace_listing/stubbed/accounts/rust-lang", 694 | "/marketplace_listing/stubbed/plans", 695 | "/marketplace_listing/stubbed/plans/987/accounts", 696 | "/meta", 697 | "/networks/rust-lang/rust/events", 698 | "/notifications", 699 | "/notifications/threads/233", 700 | "/notifications/threads/233/subscription", 701 | "/organizations", 702 | "/orgs/rust-lang", 703 | "/orgs/rust-lang/blocks", 704 | "/orgs/rust-lang/blocks/alexcrichton", 705 | "/orgs/rust-lang/events", 706 | "/orgs/rust-lang/hooks", 707 | "/orgs/rust-lang/hooks/1357908642", 708 | "/orgs/rust-lang/hooks/1357908642/pings", 709 | "/orgs/rust-lang/installation", 710 | "/orgs/rust-lang/interaction-limits", 711 | "/orgs/rust-lang/invitations", 712 | "/orgs/rust-lang/invitations/c9f8304351ad4223e4f618e9a329b2b94776b25e/teams", 713 | "/orgs/rust-lang/issues", 714 | "/orgs/rust-lang/members", 715 | "/orgs/rust-lang/members/alexcrichton", 716 | "/orgs/rust-lang/memberships/alexcrichton", 717 | "/orgs/rust-lang/migrations", 718 | "/orgs/rust-lang/migrations/233", 719 | "/orgs/rust-lang/migrations/233/archive", 720 | "/orgs/rust-lang/migrations/233/repos/rust/lock", 721 | "/orgs/rust-lang/outside_collaborators", 722 | "/orgs/rust-lang/outside_collaborators/alexcrichton", 723 | "/orgs/rust-lang/projects", 724 | "/orgs/rust-lang/public_members", 725 | "/orgs/rust-lang/public_members/alexcrichton", 726 | "/orgs/rust-lang/repos", 727 | "/orgs/rust-lang/teams", 728 | "/projects/13", 729 | "/projects/13/collaborators", 730 | "/projects/13/collaborators/alexcrichton", 731 | "/projects/13/collaborators/alexcrichton/permission", 732 | "/projects/13/columns", 733 | "/projects/columns/cards/16266729", 734 | "/projects/columns/cards/16266729/moves", 735 | "/projects/columns/3953507", 736 | "/projects/columns/3953507/cards", 737 | "/projects/columns/3953507/moves", 738 | "/rate_limit", 739 | "/reactions/3", 740 | "/repos/rust-lang/rust", 741 | "/repos/rust-lang/rust/assignees", 742 | "/repos/rust-lang/rust/assignees/rust", 743 | "/repos/rust-lang/rust/branches", 744 | "/repos/rust-lang/rust/branches/master", 745 | "/repos/rust-lang/rust/branches/master/protection", 746 | "/repos/rust-lang/rust/branches/master/protection/enforce_admins", 747 | "/repos/rust-lang/rust/branches/master/protection/required_pull_request_reviews", 748 | "/repos/rust-lang/rust/branches/master/protection/required_signatures", 749 | "/repos/rust-lang/rust/branches/master/protection/required_status_checks", 750 | "/repos/rust-lang/rust/branches/master/protection/required_status_checks/contexts", 751 | "/repos/rust-lang/rust/branches/master/protection/restrictions", 752 | "/repos/rust-lang/rust/branches/master/protection/restrictions/teams", 753 | "/repos/rust-lang/rust/branches/master/protection/restrictions/users", 754 | "/repos/rust-lang/rust/check-runs", 755 | "/repos/rust-lang/rust/check-runs/987", 756 | "/repos/rust-lang/rust/check-runs/987/annotations", 757 | "/repos/rust-lang/rust/check-suites", 758 | "/repos/rust-lang/rust/check-suites/preferences", 759 | "/repos/rust-lang/rust/check-suites/610", 760 | "/repos/rust-lang/rust/check-suites/610/check-runs", 761 | "/repos/rust-lang/rust/check-suites/610/rerequest", 762 | "/repos/rust-lang/rust/collaborators", 763 | "/repos/rust-lang/rust/collaborators/alexcrichton", 764 | "/repos/rust-lang/rust/collaborators/alexcrichton/permission", 765 | "/repos/rust-lang/rust/comments", 766 | "/repos/rust-lang/rust/comments/2274119", 767 | "/repos/rust-lang/rust/comments/2274119/reactions", 768 | "/repos/rust-lang/rust/commits", 769 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858", 770 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/check-runs", 771 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/check-suites", 772 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/comments", 773 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/status", 774 | "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/statuses", 775 | // "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858", 776 | // "/repos/rust-lang/rust/commits/2aa4c46cfdd726e97360c2734835aa3515e8c858/comments", 777 | "/repos/rust-lang/rust/community/code_of_conduct", 778 | "/repos/rust-lang/rust/community/profile", 779 | // "/repos/rust-lang/rust/compare/master...dev", 780 | "/repos/rust-lang/rust/contents/rust", 781 | "/repos/rust-lang/rust/contributors", 782 | "/repos/rust-lang/rust/deployments", 783 | "/repos/rust-lang/rust/deployments/610", 784 | "/repos/rust-lang/rust/deployments/610/statuses", 785 | "/repos/rust-lang/rust/deployments/610/statuses/2", 786 | "/repos/rust-lang/rust/downloads", 787 | "/repos/rust-lang/rust/downloads/1", 788 | "/repos/rust-lang/rust/events", 789 | "/repos/rust-lang/rust/forks", 790 | "/repos/rust-lang/rust/git/blobs", 791 | "/repos/rust-lang/rust/git/blobs/a1b2c3d4e5", 792 | "/repos/rust-lang/rust/git/commits", 793 | "/repos/rust-lang/rust/git/commits/a1b2c3d4e5", 794 | "/repos/rust-lang/rust/git/refs", 795 | // "/repos/rust-lang/rust/git/refs/a1b2c3d4e5", 796 | "/repos/rust-lang/rust/git/refs/2aa4c46cfdd726e97360c2734835aa3515e8c858", 797 | "/repos/rust-lang/rust/git/tags", 798 | "/repos/rust-lang/rust/git/tags/v1.33.0", 799 | "/repos/rust-lang/rust/git/trees", 800 | "/repos/rust-lang/rust/git/trees/a1b2c3d4e5", 801 | "/repos/rust-lang/rust/hooks", 802 | "/repos/rust-lang/rust/hooks/1357908642", 803 | "/repos/rust-lang/rust/hooks/1357908642/pings", 804 | "/repos/rust-lang/rust/hooks/1357908642/tests", 805 | "/repos/rust-lang/rust/import", 806 | "/repos/rust-lang/rust/import/authors", 807 | "/repos/rust-lang/rust/import/authors/rust", 808 | "/repos/rust-lang/rust/import/large_files", 809 | "/repos/rust-lang/rust/import/lfs", 810 | "/repos/rust-lang/rust/installation", 811 | "/repos/rust-lang/rust/interaction-limits", 812 | "/repos/rust-lang/rust/invitations", 813 | "/repos/rust-lang/rust/invitations/c9f8304351ad4223e4f618e9a329b2b94776b25e", 814 | "/repos/rust-lang/rust/issues", 815 | "/repos/rust-lang/rust/issues/comments", 816 | "/repos/rust-lang/rust/issues/comments/2274119", 817 | "/repos/rust-lang/rust/issues/comments/2274119/reactions", 818 | "/repos/rust-lang/rust/issues/events", 819 | "/repos/rust-lang/rust/issues/events/97531", 820 | "/repos/rust-lang/rust/issues/59040", 821 | "/repos/rust-lang/rust/issues/59040/assignees", 822 | "/repos/rust-lang/rust/issues/59040/comments", 823 | "/repos/rust-lang/rust/issues/59040/events", 824 | "/repos/rust-lang/rust/issues/59040/labels", 825 | "/repos/rust-lang/rust/issues/59040/labels/rust", 826 | "/repos/rust-lang/rust/issues/59040/lock", 827 | "/repos/rust-lang/rust/issues/59040/reactions", 828 | "/repos/rust-lang/rust/issues/59040/timeline", 829 | "/repos/rust-lang/rust/keys", 830 | "/repos/rust-lang/rust/keys/asnyc-await", 831 | "/repos/rust-lang/rust/labels", 832 | // "/repos/rust-lang/rust/labels/86420", 833 | "/repos/rust-lang/rust/labels/rust", 834 | "/repos/rust-lang/rust/languages", 835 | "/repos/rust-lang/rust/license", 836 | "/repos/rust-lang/rust/merges", 837 | "/repos/rust-lang/rust/milestones", 838 | "/repos/rust-lang/rust/milestones/59040", 839 | "/repos/rust-lang/rust/milestones/59040/labels", 840 | "/repos/rust-lang/rust/notifications", 841 | "/repos/rust-lang/rust/pages", 842 | "/repos/rust-lang/rust/pages/builds", 843 | "/repos/rust-lang/rust/pages/builds/latest", 844 | "/repos/rust-lang/rust/pages/builds/233", 845 | "/repos/rust-lang/rust/projects", 846 | "/repos/rust-lang/rust/pulls", 847 | "/repos/rust-lang/rust/pulls/comments", 848 | "/repos/rust-lang/rust/pulls/comments/2274119", 849 | "/repos/rust-lang/rust/pulls/comments/2274119/reactions", 850 | "/repos/rust-lang/rust/pulls/59040", 851 | "/repos/rust-lang/rust/pulls/59040/comments", 852 | "/repos/rust-lang/rust/pulls/59040/commits", 853 | "/repos/rust-lang/rust/pulls/59040/files", 854 | "/repos/rust-lang/rust/pulls/59040/merge", 855 | "/repos/rust-lang/rust/pulls/59040/requested_reviewers", 856 | "/repos/rust-lang/rust/pulls/59040/reviews", 857 | "/repos/rust-lang/rust/pulls/59040/reviews/377", 858 | "/repos/rust-lang/rust/pulls/59040/reviews/377/comments", 859 | "/repos/rust-lang/rust/pulls/59040/reviews/377/dismissals", 860 | "/repos/rust-lang/rust/pulls/59040/reviews/377/events", 861 | "/repos/rust-lang/rust/readme", 862 | "/repos/rust-lang/rust/releases", 863 | "/repos/rust-lang/rust/releases/assets/987", 864 | "/repos/rust-lang/rust/releases/latest", 865 | "/repos/rust-lang/rust/releases/tags/v1.33.0", 866 | "/repos/rust-lang/rust/releases/610", 867 | "/repos/rust-lang/rust/releases/610/assets", 868 | "/repos/rust-lang/rust/stargazers", 869 | "/repos/rust-lang/rust/stats/code_frequency", 870 | "/repos/rust-lang/rust/stats/commit_activity", 871 | "/repos/rust-lang/rust/stats/contributors", 872 | "/repos/rust-lang/rust/stats/participation", 873 | "/repos/rust-lang/rust/stats/punch_card", 874 | "/repos/rust-lang/rust/statuses/2aa4c46cfdd726e97360c2734835aa3515e8c858", 875 | "/repos/rust-lang/rust/subscribers", 876 | "/repos/rust-lang/rust/subscription", 877 | "/repos/rust-lang/rust/tags", 878 | "/repos/rust-lang/rust/teams", 879 | "/repos/rust-lang/rust/topics", 880 | "/repos/rust-lang/rust/traffic/clones", 881 | "/repos/rust-lang/rust/traffic/popular/paths", 882 | "/repos/rust-lang/rust/traffic/popular/referrers", 883 | "/repos/rust-lang/rust/traffic/views", 884 | "/repos/rust-lang/rust/transfer", 885 | "/repos/rust-lang/rust/gzip/2aa4c46cfdd726e97360c2734835aa3515e8c858", 886 | "/repositories", 887 | "/scim/v2/organizations/rust-lang/Users", 888 | "/scim/v2/organizations/rust-lang/Users/admin", 889 | "/search/code", 890 | "/search/commits", 891 | "/search/issues", 892 | "/search/labels", 893 | "/search/repositories", 894 | "/search/topics", 895 | "/search/users", 896 | "/teams/987654321", 897 | "/teams/987654321/discussions", 898 | "/teams/987654321/discussions/13579", 899 | "/teams/987654321/discussions/13579/comments", 900 | "/teams/987654321/discussions/13579/comments/24680", 901 | "/teams/987654321/discussions/13579/comments/24680/reactions", 902 | "/teams/987654321/discussions/13579/reactions", 903 | "/teams/987654321/invitations", 904 | "/teams/987654321/members", 905 | "/teams/987654321/members/alexcrichton", 906 | "/teams/987654321/memberships/alexcrichton", 907 | "/teams/987654321/projects", 908 | "/teams/987654321/projects/13", 909 | "/teams/987654321/repos", 910 | "/teams/987654321/repos/rust-lang/rust", 911 | "/teams/987654321/teams", 912 | "/user", 913 | "/user/blocks", 914 | "/user/blocks/alexcrichton", 915 | "/user/email/visibility", 916 | "/user/emails", 917 | "/user/followers", 918 | "/user/following", 919 | "/user/following/alexcrichton", 920 | "/user/gpg_keys", 921 | "/user/gpg_keys/abcde12345", 922 | "/user/installations", 923 | "/user/installations/12345/repositories", 924 | "/user/installations/12345/repositories/rust_id", 925 | "/user/issues", 926 | "/user/keys", 927 | "/user/keys/asnyc-await", 928 | "/user/marketplace_purchases", 929 | "/user/marketplace_purchases/stubbed", 930 | "/user/memberships/orgs", 931 | "/user/memberships/orgs/rust-lang", 932 | "/user/migrations", 933 | "/user/migrations/233", 934 | "/user/migrations/233/archive", 935 | "/user/migrations/233/repos/rust/lock", 936 | "/user/orgs", 937 | "/user/projects", 938 | "/user/public_emails", 939 | "/user/repos", 940 | "/user/repository_invitations", 941 | "/user/repository_invitations/c9f8304351ad4223e4f618e9a329b2b94776b25e", 942 | "/user/starred", 943 | "/user/starred/rust-lang/rust", 944 | "/user/subscriptions", 945 | "/user/subscriptions/rust-lang/rust", 946 | "/user/teams", 947 | "/users", 948 | "/users/alexcrichton", 949 | "/users/alexcrichton/events", 950 | "/users/alexcrichton/events/orgs/rust-lang", 951 | "/users/alexcrichton/events/public", 952 | "/users/alexcrichton/followers", 953 | "/users/alexcrichton/following", 954 | "/users/alexcrichton/following/aturon", 955 | "/users/alexcrichton/gists", 956 | "/users/alexcrichton/gpg_keys", 957 | "/users/alexcrichton/hovercard", 958 | "/users/alexcrichton/installation", 959 | "/users/alexcrichton/keys", 960 | "/users/alexcrichton/orgs", 961 | "/users/alexcrichton/projects", 962 | "/users/alexcrichton/received_events", 963 | "/users/alexcrichton/received_events/public", 964 | "/users/alexcrichton/repos", 965 | "/users/alexcrichton/starred", 966 | "/users/alexcrichton/subscriptions", 967 | "/rust-lang", 968 | ]; 969 | -------------------------------------------------------------------------------- /tests/node.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_lines)] 2 | 3 | use path_tree::{Key, Kind, Node}; 4 | 5 | #[test] 6 | fn github_nodes() { 7 | let mut node = Node::::new(Key::String(b"/".to_vec()), None); 8 | 9 | let mut n = node.insert_bytes(b"/"); 10 | n = n.insert_parameter(Kind::Normal); 11 | n = n.insert_bytes(b"/"); 12 | n.insert_parameter(Kind::Normal); 13 | 14 | node.insert_bytes(b"/api"); 15 | node.insert_bytes(b"/about"); 16 | node.insert_bytes(b"/login"); 17 | node.insert_bytes(b"/signup"); 18 | node.insert_bytes(b"/pricing"); 19 | 20 | node.insert_bytes(b"/features"); 21 | node.insert_bytes(b"/features/actions"); 22 | node.insert_bytes(b"/features/packages"); 23 | node.insert_bytes(b"/features/security"); 24 | node.insert_bytes(b"/features/codespaces"); 25 | node.insert_bytes(b"/features/copilot"); 26 | node.insert_bytes(b"/features/code-review"); 27 | node.insert_bytes(b"/features/issues"); 28 | node.insert_bytes(b"/features/discussions"); 29 | 30 | node.insert_bytes(b"/enterprise"); 31 | node.insert_bytes(b"/team"); 32 | node.insert_bytes(b"/customer-stories"); 33 | node.insert_bytes(b"/sponsors"); 34 | node.insert_bytes(b"/readme"); 35 | node.insert_bytes(b"/topics"); 36 | node.insert_bytes(b"/trending"); 37 | node.insert_bytes(b"/collections"); 38 | node.insert_bytes(b"/search"); 39 | node.insert_bytes(b"/pulls"); 40 | node.insert_bytes(b"/issues"); 41 | node.insert_bytes(b"/marketplace"); 42 | node.insert_bytes(b"/explore"); 43 | 44 | node.insert_bytes(b"/sponsors/explore"); 45 | node.insert_bytes(b"/sponsors/accounts"); 46 | let n = node.insert_bytes(b"/sponsors/"); 47 | n.insert_parameter(Kind::Normal); 48 | 49 | node.insert_bytes(b"/about/careers"); 50 | node.insert_bytes(b"/about/press"); 51 | node.insert_bytes(b"/about/diversity"); 52 | 53 | node.insert_bytes(b"/settings"); 54 | node.insert_bytes(b"/settings/admin"); 55 | node.insert_bytes(b"/settings/appearance"); 56 | node.insert_bytes(b"/settings/accessibility"); 57 | node.insert_bytes(b"/settings/notifications"); 58 | 59 | node.insert_bytes(b"/settings/billing"); 60 | node.insert_bytes(b"/settings/billing/plans"); 61 | node.insert_bytes(b"/settings/security"); 62 | node.insert_bytes(b"/settings/keys"); 63 | node.insert_bytes(b"/settings/organizations"); 64 | 65 | node.insert_bytes(b"/settings/blocked_users"); 66 | node.insert_bytes(b"/settings/interaction_limits"); 67 | node.insert_bytes(b"/settings/code_review_limits"); 68 | 69 | node.insert_bytes(b"/settings/repositories"); 70 | node.insert_bytes(b"/settings/codespaces"); 71 | node.insert_bytes(b"/settings/deleted_packages"); 72 | node.insert_bytes(b"/settings/copilot"); 73 | node.insert_bytes(b"/settings/pages"); 74 | node.insert_bytes(b"/settings/replies"); 75 | 76 | node.insert_bytes(b"/settings/security_analysis"); 77 | 78 | node.insert_bytes(b"/settings/installations"); 79 | node.insert_bytes(b"/settings/reminders"); 80 | 81 | node.insert_bytes(b"/settings/security-log"); 82 | node.insert_bytes(b"/settings/sponsors-log"); 83 | 84 | node.insert_bytes(b"/settings/apps"); 85 | node.insert_bytes(b"/settings/developers"); 86 | node.insert_bytes(b"/settings/tokens"); 87 | 88 | node.insert_bytes(b"/404"); 89 | node.insert_bytes(b"/500"); 90 | node.insert_bytes(b"/503"); 91 | 92 | assert_eq!( 93 | format!("{node:?}"), 94 | r" 95 | / 96 | ├── 404 97 | ├── 50 98 | │ ├── 0 99 | │ └── 3 100 | ├── a 101 | │ ├── bout 102 | │ │ └── / 103 | │ │ ├── careers 104 | │ │ ├── diversity 105 | │ │ └── press 106 | │ └── pi 107 | ├── c 108 | │ ├── ollections 109 | │ └── ustomer-stories 110 | ├── e 111 | │ ├── nterprise 112 | │ └── xplore 113 | ├── features 114 | │ └── / 115 | │ ├── actions 116 | │ ├── co 117 | │ │ ├── de 118 | │ │ │ ├── -review 119 | │ │ │ └── spaces 120 | │ │ └── pilot 121 | │ ├── discussions 122 | │ ├── issues 123 | │ ├── packages 124 | │ └── security 125 | ├── issues 126 | ├── login 127 | ├── marketplace 128 | ├── p 129 | │ ├── ricing 130 | │ └── ulls 131 | ├── readme 132 | ├── s 133 | │ ├── e 134 | │ │ ├── arch 135 | │ │ └── ttings 136 | │ │ └── / 137 | │ │ ├── a 138 | │ │ │ ├── ccessibility 139 | │ │ │ ├── dmin 140 | │ │ │ └── pp 141 | │ │ │ ├── earance 142 | │ │ │ └── s 143 | │ │ ├── b 144 | │ │ │ ├── illing 145 | │ │ │ │ └── /plans 146 | │ │ │ └── locked_users 147 | │ │ ├── co 148 | │ │ │ ├── de 149 | │ │ │ │ ├── _review_limits 150 | │ │ │ │ └── spaces 151 | │ │ │ └── pilot 152 | │ │ ├── de 153 | │ │ │ ├── leted_packages 154 | │ │ │ └── velopers 155 | │ │ ├── in 156 | │ │ │ ├── stallations 157 | │ │ │ └── teraction_limits 158 | │ │ ├── keys 159 | │ │ ├── notifications 160 | │ │ ├── organizations 161 | │ │ ├── pages 162 | │ │ ├── re 163 | │ │ │ ├── minders 164 | │ │ │ └── p 165 | │ │ │ ├── lies 166 | │ │ │ └── ositories 167 | │ │ ├── s 168 | │ │ │ ├── ecurity 169 | │ │ │ │ ├── -log 170 | │ │ │ │ └── _analysis 171 | │ │ │ └── ponsors-log 172 | │ │ └── tokens 173 | │ ├── ignup 174 | │ └── ponsors 175 | │ └── / 176 | │ ├── accounts 177 | │ ├── explore 178 | │ └── : 179 | ├── t 180 | │ ├── eam 181 | │ ├── opics 182 | │ └── rending 183 | └── : 184 | └── / 185 | └── : 186 | " 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /tests/parser.rs: -------------------------------------------------------------------------------- 1 | use path_tree::{Kind, Parser, Piece, Position}; 2 | 3 | #[test] 4 | fn parses() { 5 | assert_eq!( 6 | Parser::new(r"/shop/product/\::filter/color\::color/size\::size").collect::>(), 7 | [ 8 | Piece::String(b"/shop/product/".to_vec()), 9 | Piece::String(b":".to_vec()), 10 | Piece::Parameter(Position::Named(b"filter".to_vec()), Kind::Normal), 11 | Piece::String(b"/color".to_vec()), 12 | Piece::String(b":".to_vec()), 13 | Piece::Parameter(Position::Named(b"color".to_vec()), Kind::Normal), 14 | Piece::String(b"/size".to_vec()), 15 | Piece::String(b":".to_vec()), 16 | Piece::Parameter(Position::Named(b"size".to_vec()), Kind::Normal), 17 | ], 18 | ); 19 | 20 | assert_eq!( 21 | Parser::new("/api/v1/:param/abc/*").collect::>(), 22 | [ 23 | Piece::String(b"/api/v1/".to_vec()), 24 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 25 | Piece::String(b"/abc/".to_vec()), 26 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 27 | ], 28 | ); 29 | 30 | assert_eq!( 31 | Parser::new("/api/v1/:param/+").collect::>(), 32 | [ 33 | Piece::String(b"/api/v1/".to_vec()), 34 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 35 | Piece::String(b"/".to_vec()), 36 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 37 | ], 38 | ); 39 | 40 | assert_eq!( 41 | Parser::new("/api/v1/:param?").collect::>(), 42 | [ 43 | Piece::String(b"/api/v1/".to_vec()), 44 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::OptionalSegment), 45 | ], 46 | ); 47 | 48 | assert_eq!( 49 | Parser::new("/api/v1/:param?").collect::>(), 50 | [ 51 | Piece::String(b"/api/v1/".to_vec()), 52 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::OptionalSegment), 53 | ], 54 | ); 55 | 56 | assert_eq!( 57 | Parser::new("/api/v1/:param").collect::>(), 58 | [ 59 | Piece::String(b"/api/v1/".to_vec()), 60 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 61 | ], 62 | ); 63 | 64 | assert_eq!( 65 | Parser::new("/api/v1/*").collect::>(), 66 | [ 67 | Piece::String(b"/api/v1/".to_vec()), 68 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 69 | ], 70 | ); 71 | 72 | assert_eq!( 73 | Parser::new("/api/v1/:param-:param2").collect::>(), 74 | [ 75 | Piece::String(b"/api/v1/".to_vec()), 76 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 77 | Piece::String(b"-".to_vec()), 78 | Piece::Parameter(Position::Named(b"param2".to_vec()), Kind::Normal), 79 | ], 80 | ); 81 | 82 | assert_eq!( 83 | Parser::new("/api/v1/:filename.:extension").collect::>(), 84 | [ 85 | Piece::String(b"/api/v1/".to_vec()), 86 | Piece::Parameter(Position::Named(b"filename".to_vec()), Kind::Normal), 87 | Piece::String(b".".to_vec()), 88 | Piece::Parameter(Position::Named(b"extension".to_vec()), Kind::Normal), 89 | ], 90 | ); 91 | 92 | assert_eq!( 93 | Parser::new("/api/v1/string").collect::>(), 94 | [Piece::String(b"/api/v1/string".to_vec()),], 95 | ); 96 | 97 | assert_eq!( 98 | Parser::new(r"/\::param?").collect::>(), 99 | [ 100 | Piece::String(b"/".to_vec()), 101 | Piece::String(b":".to_vec()), 102 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Optional), 103 | ], 104 | ); 105 | 106 | assert_eq!( 107 | Parser::new("/:param1:param2?:param3").collect::>(), 108 | [ 109 | Piece::String(b"/".to_vec()), 110 | Piece::Parameter(Position::Named(b"param1".to_vec()), Kind::Normal), 111 | Piece::Parameter(Position::Named(b"param2".to_vec()), Kind::Optional), 112 | Piece::Parameter(Position::Named(b"param3".to_vec()), Kind::Normal), 113 | ], 114 | ); 115 | 116 | assert_eq!( 117 | Parser::new("/test:sign:param").collect::>(), 118 | [ 119 | Piece::String(b"/test".to_vec()), 120 | Piece::Parameter(Position::Named(b"sign".to_vec()), Kind::Normal), 121 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 122 | ], 123 | ); 124 | 125 | assert_eq!( 126 | Parser::new("/foo:param?bar").collect::>(), 127 | [ 128 | Piece::String(b"/foo".to_vec()), 129 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Optional), 130 | Piece::String(b"bar".to_vec()), 131 | ], 132 | ); 133 | 134 | assert_eq!( 135 | Parser::new("/foo*bar").collect::>(), 136 | [ 137 | Piece::String(b"/foo".to_vec()), 138 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 139 | Piece::String(b"bar".to_vec()), 140 | ], 141 | ); 142 | 143 | assert_eq!( 144 | Parser::new("/foo+bar").collect::>(), 145 | [ 146 | Piece::String(b"/foo".to_vec()), 147 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 148 | Piece::String(b"bar".to_vec()), 149 | ], 150 | ); 151 | 152 | assert_eq!( 153 | Parser::new("/a*cde*g/").collect::>(), 154 | [ 155 | Piece::String(b"/a".to_vec()), 156 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 157 | Piece::String(b"cde".to_vec()), 158 | Piece::Parameter(Position::Index(2, b"*2".to_vec()), Kind::ZeroOrMore), 159 | Piece::String(b"g/".to_vec()), 160 | ], 161 | ); 162 | 163 | assert_eq!( 164 | Parser::new(r"/name\::name").collect::>(), 165 | [ 166 | Piece::String(b"/name".to_vec()), 167 | Piece::String(b":".to_vec()), 168 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 169 | ] 170 | ); 171 | 172 | assert_eq!( 173 | Parser::new("/@:name").collect::>(), 174 | [ 175 | Piece::String(b"/@".to_vec()), 176 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 177 | ] 178 | ); 179 | 180 | assert_eq!( 181 | Parser::new("/-:name").collect::>(), 182 | [ 183 | Piece::String(b"/-".to_vec()), 184 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 185 | ] 186 | ); 187 | 188 | assert_eq!( 189 | Parser::new("/.:name").collect::>(), 190 | [ 191 | Piece::String(b"/.".to_vec()), 192 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 193 | ] 194 | ); 195 | 196 | assert_eq!( 197 | Parser::new("/_:name").collect::>(), 198 | [ 199 | Piece::String(b"/_".to_vec()), 200 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 201 | ] 202 | ); 203 | 204 | assert_eq!( 205 | Parser::new("/~:name").collect::>(), 206 | [ 207 | Piece::String(b"/~".to_vec()), 208 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 209 | ] 210 | ); 211 | 212 | assert_eq!( 213 | Parser::new("/v1/some/resource/name\\:customVerb").collect::>(), 214 | [ 215 | Piece::String(b"/v1/some/resource/name".to_vec()), 216 | Piece::String(b":".to_vec()), 217 | Piece::String(b"customVerb".to_vec()), 218 | ], 219 | ); 220 | 221 | assert_eq!( 222 | Parser::new("/v1/some/resource/:name\\:customVerb").collect::>(), 223 | [ 224 | Piece::String(b"/v1/some/resource/".to_vec()), 225 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 226 | Piece::String(b":".to_vec()), 227 | Piece::String(b"customVerb".to_vec()), 228 | ], 229 | ); 230 | 231 | assert_eq!( 232 | Parser::new("/v1/some/resource/name\\:customVerb??/:param/*").collect::>(), 233 | [ 234 | Piece::String(b"/v1/some/resource/name".to_vec()), 235 | Piece::String(b":".to_vec()), 236 | Piece::String(b"customVerb??/".to_vec()), 237 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 238 | Piece::String(b"/".to_vec()), 239 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment) 240 | ], 241 | ); 242 | 243 | assert_eq!( 244 | Parser::new("/api/*/:param/:param2").collect::>(), 245 | [ 246 | Piece::String(b"/api/".to_vec()), 247 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 248 | Piece::String(b"/".to_vec()), 249 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 250 | Piece::String(b"/".to_vec()), 251 | Piece::Parameter(Position::Named(b"param2".to_vec()), Kind::Normal) 252 | ], 253 | ); 254 | 255 | assert_eq!( 256 | Parser::new("/test:optional?:optional2?").collect::>(), 257 | [ 258 | Piece::String(b"/test".to_vec()), 259 | Piece::Parameter(Position::Named(b"optional".to_vec()), Kind::Optional), 260 | Piece::Parameter(Position::Named(b"optional2".to_vec()), Kind::Optional) 261 | ], 262 | ); 263 | 264 | assert_eq!( 265 | Parser::new("/config/+.json").collect::>(), 266 | [ 267 | Piece::String(b"/config/".to_vec()), 268 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 269 | Piece::String(b".json".to_vec()), 270 | ] 271 | ); 272 | 273 | assert_eq!( 274 | Parser::new("/config/*.json").collect::>(), 275 | [ 276 | Piece::String(b"/config/".to_vec()), 277 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 278 | Piece::String(b".json".to_vec()), 279 | ] 280 | ); 281 | 282 | assert_eq!( 283 | Parser::new("/api/:day.:month?.:year?").collect::>(), 284 | [ 285 | Piece::String(b"/api/".to_vec()), 286 | Piece::Parameter(Position::Named(b"day".to_vec()), Kind::Normal), 287 | Piece::String(b".".to_vec()), 288 | Piece::Parameter(Position::Named(b"month".to_vec()), Kind::Optional), 289 | Piece::String(b".".to_vec()), 290 | Piece::Parameter(Position::Named(b"year".to_vec()), Kind::Optional), 291 | ] 292 | ); 293 | 294 | assert_eq!( 295 | Parser::new("/api/:day/:month?/:year?").collect::>(), 296 | [ 297 | Piece::String(b"/api/".to_vec()), 298 | Piece::Parameter(Position::Named(b"day".to_vec()), Kind::Normal), 299 | Piece::String(b"/".to_vec()), 300 | Piece::Parameter(Position::Named(b"month".to_vec()), Kind::OptionalSegment), 301 | Piece::String(b"/".to_vec()), 302 | Piece::Parameter(Position::Named(b"year".to_vec()), Kind::OptionalSegment), 303 | ] 304 | ); 305 | 306 | assert_eq!( 307 | Parser::new("/*v1*/proxy").collect::>(), 308 | [ 309 | Piece::String(b"/".to_vec()), 310 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 311 | Piece::String(b"v1".to_vec()), 312 | Piece::Parameter(Position::Index(2, b"*2".to_vec()), Kind::ZeroOrMore), 313 | Piece::String(b"/proxy".to_vec()), 314 | ] 315 | ); 316 | 317 | assert_eq!( 318 | Parser::new("/:a*v1:b+/proxy").collect::>(), 319 | [ 320 | Piece::String(b"/".to_vec()), 321 | Piece::Parameter(Position::Named(b"a".to_vec()), Kind::ZeroOrMore), 322 | Piece::String(b"v1".to_vec()), 323 | Piece::Parameter(Position::Named(b"b".to_vec()), Kind::OneOrMore), 324 | Piece::String(b"/proxy".to_vec()), 325 | ] 326 | ); 327 | } 328 | -------------------------------------------------------------------------------- /tests/tree.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_must_use)] 2 | #![allow(clippy::too_many_lines)] 3 | 4 | use path_tree::{Kind, PathTree, Piece, Position}; 5 | use rand::seq::SliceRandom; 6 | 7 | #[test] 8 | fn statics() { 9 | const ROUTES: [&str; 12] = [ 10 | "/", 11 | "/hi", 12 | "/contact", 13 | "/co", 14 | "/c", 15 | "/a", 16 | "/ab", 17 | "/doc/", 18 | "/doc/go_faq.html", 19 | "/doc/go1.html", 20 | "/α", 21 | "/β", 22 | ]; 23 | 24 | let mut routes = ROUTES.to_vec(); 25 | 26 | routes.shuffle(&mut rand::rng()); 27 | 28 | let mut tree = PathTree::::new(); 29 | 30 | for (i, u) in routes.iter().enumerate() { 31 | tree.insert(u, i); 32 | } 33 | 34 | for (i, u) in routes.iter().enumerate() { 35 | let (h, _) = tree.find(u).unwrap(); 36 | assert_eq!(h, &i); 37 | } 38 | } 39 | 40 | #[test] 41 | fn wildcards() { 42 | const ROUTES: [&str; 20] = [ 43 | "/", 44 | "/cmd/:tool/:sub", 45 | "/cmd/:tool/", 46 | "/cmd/vet", 47 | "/src/:filepath*", 48 | "/src1/", 49 | "/src1/:filepath*", 50 | "/src2:filepath*", 51 | "/search/", 52 | "/search/:query", 53 | "/search/invalid", 54 | "/user_:name", 55 | "/user_:name/about", 56 | "/user_x", 57 | "/files/:dir/:filepath*", 58 | "/doc/", 59 | "/doc/rust_faq.html", 60 | "/doc/rust1.html", 61 | "/info/:user/public", 62 | "/info/:user/project/:project", 63 | ]; 64 | 65 | let mut routes = (0..20).zip(ROUTES.iter()).collect::>(); 66 | 67 | routes.shuffle(&mut rand::rng()); 68 | 69 | let mut tree = PathTree::::new(); 70 | 71 | for (i, u) in &routes { 72 | tree.insert(u, *i); 73 | } 74 | 75 | let valid_res = vec![ 76 | ("/", 0, vec![]), 77 | ("/cmd/test/", 2, vec![("tool", "test")]), 78 | ("/cmd/test/3", 1, vec![("tool", "test"), ("sub", "3")]), 79 | ("/src/", 4, vec![("filepath", "")]), 80 | ("/src/some/file.png", 4, vec![("filepath", "some/file.png")]), 81 | ( 82 | "/search/someth!ng+in+ünìcodé", 83 | 9, 84 | vec![("query", "someth!ng+in+ünìcodé")], 85 | ), 86 | ("/user_rust", 11, vec![("name", "rust")]), 87 | ("/user_rust/about", 12, vec![("name", "rust")]), 88 | ( 89 | "/files/js/inc/framework.js", 90 | 14, 91 | vec![("dir", "js"), ("filepath", "inc/framework.js")], 92 | ), 93 | ("/info/gordon/public", 18, vec![("user", "gordon")]), 94 | ( 95 | "/info/gordon/project/rust", 96 | 19, 97 | vec![("user", "gordon"), ("project", "rust")], 98 | ), 99 | ]; 100 | 101 | for (u, v, p) in valid_res { 102 | let (h, r) = tree.find(u).unwrap(); 103 | assert_eq!(*h, v); 104 | assert_eq!(r.params(), p); 105 | } 106 | } 107 | 108 | #[test] 109 | fn single_named_parameter() { 110 | // Pattern: /users/:id 111 | // 112 | // /users/gordon match 113 | // /users/you match 114 | // /users/gordon/profile no match 115 | // /users/ no match 116 | let mut tree = PathTree::new(); 117 | 118 | tree.insert("/users/:id", 0); 119 | 120 | let res = vec![ 121 | ("/", false), 122 | ("/users/gordon", true), 123 | ("/users/you", true), 124 | ("/users/gordon/profile", false), 125 | ("/users/", false), 126 | ("/users", false), 127 | ]; 128 | 129 | for (u, b) in res { 130 | let n = tree.find(u); 131 | assert_eq!(n.is_some(), b); 132 | } 133 | } 134 | 135 | #[test] 136 | fn repeated_single_named_param() { 137 | let mut tree = PathTree::new(); 138 | 139 | tree.insert("/users/:id", 0); 140 | tree.insert("/users/:user_id", 1); 141 | 142 | let (h, r) = tree.find("/users/gordon").unwrap(); 143 | assert_eq!(*h, 1); 144 | assert_eq!(r.params(), vec![("user_id", "gordon")]); 145 | } 146 | 147 | #[test] 148 | fn static_and_named_parameter() { 149 | // Pattern: /a/b/c 150 | // Pattern: /a/c/d 151 | // Pattern: /a/c/a 152 | // Pattern: /:id/c/e 153 | // 154 | // /a/b/c match 155 | // /a/c/d match 156 | // /a/c/a match 157 | // /a/c/e match 158 | let mut tree = PathTree::new(); 159 | 160 | tree.insert("/a/b/c", "/a/b/c"); 161 | tree.insert("/a/c/d", "/a/c/d"); 162 | tree.insert("/a/c/a", "/a/c/a"); 163 | tree.insert("/:id/c/e", "/:id/c/e"); 164 | 165 | let res = vec![ 166 | ("/", false, "", vec![]), 167 | ("/a/b/c", true, "/a/b/c", vec![]), 168 | ("/a/c/d", true, "/a/c/d", vec![]), 169 | ("/a/c/a", true, "/a/c/a", vec![]), 170 | ("/a/c/e", true, "/:id/c/e", vec![("id", "a")]), 171 | ]; 172 | 173 | for (u, b, a, p) in res { 174 | let n = tree.find(u); 175 | assert_eq!(n.is_some(), b); 176 | if let Some((h, r)) = n { 177 | assert_eq!(*h, a); 178 | assert_eq!(r.params(), p); 179 | } 180 | } 181 | } 182 | 183 | #[test] 184 | fn multi_named_parameters() { 185 | // Pattern: /:lang/:keyword 186 | // Pattern: /:id 187 | // 188 | // /rust match 189 | // /rust/let match 190 | // /rust/let/const no match 191 | // /rust/let/ no match 192 | // /rust/ no match 193 | // / no match 194 | let mut tree = PathTree::new(); 195 | 196 | tree.insert("/:lang/:keyword", true); 197 | tree.insert("/:id", true); 198 | 199 | let res = vec![ 200 | ("/", false, false, vec![]), 201 | ("/rust/", false, false, vec![]), 202 | ("/rust/let/", false, false, vec![]), 203 | ("/rust/let/const", false, false, vec![]), 204 | ( 205 | "/rust/let", 206 | true, 207 | true, 208 | vec![("lang", "rust"), ("keyword", "let")], 209 | ), 210 | ("/rust", true, true, vec![("id", "rust")]), 211 | ]; 212 | 213 | for (u, b, a, p) in res { 214 | let n = tree.find(u); 215 | assert_eq!(n.is_some(), b); 216 | if let Some((h, r)) = n { 217 | assert_eq!(*h, a); 218 | assert_eq!(r.params(), p); 219 | } 220 | } 221 | } 222 | 223 | #[test] 224 | fn catch_all_parameter() { 225 | // Pattern: /src/*filepath 226 | // 227 | // /src no match 228 | // /src/ match 229 | // /src/somefile.go match 230 | // /src/subdir/somefile.go match 231 | let mut tree = PathTree::new(); 232 | 233 | tree.insert("/src/:filepath*", "* files"); 234 | 235 | let res = vec![ 236 | ("/src", false, vec![]), 237 | ("/src/", true, vec![("filepath", "")]), 238 | ("/src/somefile.rs", true, vec![("filepath", "somefile.rs")]), 239 | ( 240 | "/src/subdir/somefile.rs", 241 | true, 242 | vec![("filepath", "subdir/somefile.rs")], 243 | ), 244 | ("/src.rs", false, vec![]), 245 | ("/rust", false, vec![]), 246 | ]; 247 | 248 | for (u, b, p) in res { 249 | let n = tree.find(u); 250 | assert_eq!(n.is_some(), b); 251 | if let Some((h, r)) = n { 252 | assert_eq!(*h, "* files"); 253 | assert_eq!(r.params(), p); 254 | } 255 | } 256 | 257 | tree.insert("/src/", "dir"); 258 | 259 | let n = tree.find("/src/"); 260 | assert!(n.is_some()); 261 | if let Some((h, r)) = n { 262 | assert_eq!(*h, "dir"); 263 | assert_eq!(r.params(), vec![]); 264 | } 265 | } 266 | 267 | #[test] 268 | fn catch_all_parameter_with_prefix() { 269 | // Pattern: /commit_*sha 270 | // 271 | // /commit no match 272 | // /commit_ match 273 | // /commit_/ match 274 | // /commit_/foo match 275 | // /commit_123 match 276 | // /commit_123/ match 277 | // /commit_123/foo match 278 | let mut tree = PathTree::new(); 279 | 280 | tree.insert("/commit_:sha*", "* sha"); 281 | tree.insert("/commit/:sha", "hex"); 282 | tree.insert("/commit/:sha0/compare/:sha1", "compare"); 283 | tree.insert("/src/", "dir"); 284 | 285 | let n = tree.find("/src/"); 286 | assert!(n.is_some()); 287 | if let Some((h, r)) = n { 288 | assert_eq!(*h, "dir"); 289 | assert_eq!(r.params(), vec![]); 290 | } 291 | 292 | let n = tree.find("/commit/123"); 293 | assert!(n.is_some()); 294 | if let Some((h, r)) = n { 295 | assert_eq!(*h, "hex"); 296 | assert_eq!(r.params(), vec![("sha", "123")]); 297 | } 298 | 299 | let n = tree.find("/commit/123/compare/321"); 300 | assert!(n.is_some()); 301 | if let Some((h, r)) = n { 302 | assert_eq!(*h, "compare"); 303 | assert_eq!(r.params(), vec![("sha0", "123"), ("sha1", "321")]); 304 | } 305 | 306 | let res = vec![ 307 | ("/commit", false, vec![]), 308 | ("/commit_", true, vec![("sha", "")]), 309 | ("/commit_/", true, vec![("sha", "/")]), 310 | ("/commit_/foo", true, vec![("sha", "/foo")]), 311 | ("/commit123", false, vec![]), 312 | ("/commit_123", true, vec![("sha", "123")]), 313 | ("/commit_123/", true, vec![("sha", "123/")]), 314 | ("/commit_123/foo", true, vec![("sha", "123/foo")]), 315 | ]; 316 | 317 | for (u, b, p) in res { 318 | let n = tree.find(u); 319 | assert_eq!(n.is_some(), b); 320 | if let Some((h, r)) = n { 321 | assert_eq!(*h, "* sha"); 322 | assert_eq!(r.params(), p); 323 | } 324 | } 325 | } 326 | 327 | #[test] 328 | fn static_and_catch_all_parameter() { 329 | // Pattern: /a/b/c 330 | // Pattern: /a/c/d 331 | // Pattern: /a/c/a 332 | // Pattern: /a/*c 333 | // 334 | // /a/b/c match 335 | // /a/c/d match 336 | // /a/c/a match 337 | // /a/c/e match 338 | let mut tree = PathTree::new(); 339 | 340 | tree.insert("/a/b/c", "/a/b/c"); 341 | tree.insert("/a/c/d", "/a/c/d"); 342 | tree.insert("/a/c/a", "/a/c/a"); 343 | tree.insert("/a/*", "/a/*c"); 344 | 345 | let res = vec![ 346 | ("/", false, "", vec![]), 347 | ("/a/b/c", true, "/a/b/c", vec![]), 348 | ("/a/c/d", true, "/a/c/d", vec![]), 349 | ("/a/c/a", true, "/a/c/a", vec![]), 350 | ("/a/c/e", true, "/a/*c", vec![("*1", "c/e")]), 351 | ]; 352 | 353 | for (u, b, a, p) in res { 354 | let n = tree.find(u); 355 | assert_eq!(n.is_some(), b); 356 | if let Some((h, r)) = n { 357 | assert_eq!(*h, a); 358 | assert_eq!(r.params(), p); 359 | } 360 | } 361 | } 362 | 363 | #[test] 364 | fn root_catch_all_parameter() { 365 | // Pattern: / 366 | // Pattern: /* 367 | // Pattern: /users/* 368 | // 369 | // / match * 370 | // /download match * 371 | // /users/jordan match users * 372 | let mut tree = PathTree:: usize>::new(); 373 | 374 | tree.insert("/", || 1); 375 | tree.insert("/*", || 2); 376 | tree.insert("/users/*", || 3); 377 | 378 | let res = vec![ 379 | ("/", true, 1, vec![]), 380 | ("/download", true, 2, vec![("*1", "download")]), 381 | ("/users/jordan", true, 3, vec![("*1", "jordan")]), 382 | ]; 383 | 384 | for (u, b, a, p) in res { 385 | let n = tree.find(u); 386 | assert_eq!(n.is_some(), b); 387 | if let Some((h, r)) = n { 388 | assert_eq!((h)(), a); 389 | assert_eq!(r.params(), p); 390 | } 391 | } 392 | } 393 | 394 | #[test] 395 | fn root_catch_all_parameter_1() { 396 | // Pattern: /* 397 | // 398 | // / match * 399 | // /download match * 400 | // /users/jordan match * 401 | let mut tree = PathTree:: usize>::new(); 402 | 403 | tree.insert("/*", || 1); 404 | 405 | let res = vec![ 406 | ("/", true, 1, vec![("*1", "")]), 407 | ("/download", true, 1, vec![("*1", "download")]), 408 | ("/users/jordan", true, 1, vec![("*1", "users/jordan")]), 409 | ]; 410 | 411 | for (u, b, a, p) in res { 412 | let n = tree.find(u); 413 | assert_eq!(n.is_some(), b); 414 | if let Some((h, r)) = n { 415 | assert_eq!((h)(), a); 416 | assert_eq!(r.params(), p); 417 | } 418 | } 419 | 420 | tree.insert("/", || 0); 421 | let n = tree.find("/"); 422 | assert!(n.is_some()); 423 | if let Some((h, r)) = n { 424 | assert_eq!((h)(), 0); 425 | assert_eq!(r.params(), vec![]); 426 | } 427 | } 428 | 429 | #[test] 430 | fn test_named_routes_with_non_ascii_paths() { 431 | let mut tree = PathTree::::new(); 432 | tree.insert("/", 0); 433 | tree.insert("/*", 1); 434 | tree.insert("/matchme/:slug/", 2); 435 | 436 | // ASCII only (single-byte characters) 437 | let n = tree.find("/matchme/abc-s-def/"); 438 | assert!(n.is_some()); 439 | let (h, r) = n.unwrap(); 440 | assert_eq!(*h, 2); 441 | assert_eq!(r.params(), vec![("slug", "abc-s-def")]); 442 | 443 | // with multibyte character 444 | let n = tree.find("/matchme/abc-ß-def/"); 445 | assert!(n.is_some()); 446 | let (h, r) = n.unwrap(); 447 | assert_eq!(*h, 2); 448 | assert_eq!(r.params(), vec![("slug", "abc-ß-def")]); 449 | 450 | // with emoji (fancy multibyte character) 451 | let n = tree.find("/matchme/abc-⭐-def/"); 452 | assert!(n.is_some()); 453 | let (h, r) = n.unwrap(); 454 | assert_eq!(*h, 2); 455 | assert_eq!(r.params(), vec![("slug", "abc-⭐-def")]); 456 | 457 | // with multibyte character right before the slash (char boundary check) 458 | let n = tree.find("/matchme/abc-def-ß/"); 459 | assert!(n.is_some()); 460 | let (h, r) = n.unwrap(); 461 | assert_eq!(*h, 2); 462 | assert_eq!(r.params(), vec![("slug", "abc-def-ß")]); 463 | } 464 | 465 | #[test] 466 | fn test_named_wildcard_collide() { 467 | let mut tree = PathTree::::new(); 468 | tree.insert("/git/:org/:repo", 1); 469 | tree.insert("/git/*", 2); 470 | 471 | let n = tree.find("/git/rust-lang/rust"); 472 | assert!(n.is_some()); 473 | let (h, r) = n.unwrap(); 474 | assert_eq!(*h, 1); 475 | assert_eq!(r.params(), vec![("org", "rust-lang"), ("repo", "rust")]); 476 | 477 | let n = tree.find("/git/rust-lang"); 478 | assert!(n.is_some()); 479 | let (h, r) = n.unwrap(); 480 | assert_eq!(*h, 2); 481 | assert_eq!(r.params(), vec![("*1", "rust-lang")]); 482 | } 483 | 484 | #[test] 485 | fn match_params() { 486 | // / 487 | // └── api/v1/ 488 | // └── : 489 | // └── / 490 | // └── ** •0 491 | let mut tree = PathTree::::new(); 492 | 493 | tree.insert("/api/v1/:param/*", 1); 494 | 495 | assert_eq!(tree.find("/api/v1/entity"), None); 496 | let (h, p) = tree.find("/api/v1/entity/").unwrap(); 497 | assert_eq!(*p.id, 0); 498 | assert_eq!(*h, 1); 499 | assert_eq!(p.params(), vec![("param", "entity"), ("*1", "")]); 500 | assert_eq!(p.pattern(), "/api/v1/:param/*"); 501 | assert_eq!( 502 | p.pieces, 503 | &vec![ 504 | Piece::String(b"/api/v1/".to_vec()), 505 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 506 | Piece::String(b"/".to_vec()), 507 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 508 | ] 509 | ); 510 | 511 | let (h, p) = tree.find("/api/v1/entity/1").unwrap(); 512 | assert_eq!(*h, 1); 513 | assert_eq!(p.params(), vec![("param", "entity"), ("*1", "1")]); 514 | 515 | assert_eq!(tree.find("/api/v"), None); 516 | assert_eq!(tree.find("/api/v2"), None); 517 | assert_eq!(tree.find("/api/v1/"), None); 518 | 519 | let (h, p) = tree.find("/api/v1/entity/1/foo/bar").unwrap(); 520 | assert_eq!(*h, 1); 521 | assert_eq!(p.params(), vec![("param", "entity"), ("*1", "1/foo/bar")]); 522 | 523 | // / 524 | // └── api/v1/ 525 | // └── : 526 | // └── / 527 | // └── + •0 528 | let mut tree = PathTree::::new(); 529 | 530 | tree.insert("/api/v1/:param/+", 1); 531 | 532 | assert_eq!(tree.find("/api/v1/entity"), None); 533 | assert_eq!(tree.find("/api/v1/entity/"), None); 534 | 535 | let (h, p) = tree.find("/api/v1/entity/1").unwrap(); 536 | assert_eq!(*h, 1); 537 | assert_eq!(p.params(), vec![("param", "entity"), ("+1", "1")]); 538 | 539 | assert_eq!(tree.find("/api/v"), None); 540 | assert_eq!(tree.find("/api/v2"), None); 541 | assert_eq!(tree.find("/api/v1/"), None); 542 | 543 | let (h, p) = tree.find("/api/v1/entity/1/foo/bar").unwrap(); 544 | assert_eq!(*h, 1); 545 | assert_eq!(p.params(), vec![("param", "entity"), ("+1", "1/foo/bar")]); 546 | 547 | // / 548 | // └── api/v1/ 549 | // └── ?? •0 550 | let mut tree = PathTree::::new(); 551 | 552 | tree.insert("/api/v1/:param?", 1); 553 | 554 | let (h, p) = tree.find("/api/v1/").unwrap(); 555 | assert_eq!(*p.id, 0); 556 | assert_eq!(*h, 1); 557 | assert_eq!(p.params(), vec![("param", "")]); 558 | assert_eq!(p.pattern(), "/api/v1/:param?"); 559 | assert_eq!( 560 | p.pieces, 561 | &vec![ 562 | Piece::String(b"/api/v1/".to_vec()), 563 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::OptionalSegment), 564 | ] 565 | ); 566 | 567 | assert_eq!(tree.find("/api/v1/entity/1/foo/bar"), None); 568 | assert_eq!(tree.find("/api/v"), None); 569 | assert_eq!(tree.find("/api/v2"), None); 570 | assert_eq!(tree.find("/api/xyz"), None); 571 | 572 | // / 573 | // └── v1/some/resource/name 574 | // └── \: 575 | // └── customVerb •0 576 | let mut tree = PathTree::::new(); 577 | 578 | tree.insert("/v1/some/resource/name\\:customVerb", 1); 579 | 580 | let (h, p) = tree.find("/v1/some/resource/name:customVerb").unwrap(); 581 | assert_eq!(*p.id, 0); 582 | assert_eq!(*h, 1); 583 | assert_eq!(p.params(), vec![]); 584 | assert_eq!(p.pattern(), "/v1/some/resource/name\\:customVerb"); 585 | assert_eq!( 586 | p.pieces, 587 | &vec![ 588 | Piece::String(b"/v1/some/resource/name".to_vec()), 589 | Piece::String(b":".to_vec()), 590 | Piece::String(b"customVerb".to_vec()), 591 | ] 592 | ); 593 | assert_eq!(tree.find("/v1/some/resource/name:test"), None); 594 | 595 | // / 596 | // └── v1/some/resource/ 597 | // └── : 598 | // └── \: 599 | // └── customVerb •0 600 | let mut tree = PathTree::::new(); 601 | 602 | tree.insert(r"/v1/some/resource/:name\:customVerb", 1); 603 | 604 | let (h, p) = tree.find("/v1/some/resource/test:customVerb").unwrap(); 605 | assert_eq!(*p.id, 0); 606 | assert_eq!(*h, 1); 607 | assert_eq!(p.params(), vec![("name", "test")]); 608 | assert_eq!( 609 | p.pieces, 610 | vec![ 611 | Piece::String(b"/v1/some/resource/".to_vec()), 612 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 613 | Piece::String(b":".to_vec()), 614 | Piece::String(b"customVerb".to_vec()), 615 | ] 616 | ); 617 | assert_eq!(tree.find("/v1/some/resource/test:test"), None); 618 | 619 | // / 620 | // └── v1/some/resource/name 621 | // └── \: 622 | // └── customVerb\? 623 | // └── \? 624 | // └── / 625 | // └── : 626 | // └── / 627 | // └── ** •0 628 | let mut tree = PathTree::::new(); 629 | 630 | tree.insert(r"/v1/some/resource/name\\\\:customVerb?\?/:param/*", 1); 631 | 632 | let (h, p) = tree 633 | .find("/v1/some/resource/name:customVerb??/test/optionalWildCard/character") 634 | .unwrap(); 635 | assert_eq!(*h, 1); 636 | assert_eq!( 637 | p.params(), 638 | vec![("param", "test"), ("*1", "optionalWildCard/character")] 639 | ); 640 | 641 | let (h, p) = tree 642 | .find("/v1/some/resource/name:customVerb??/test/") 643 | .unwrap(); 644 | assert_eq!(*h, 1); 645 | assert_eq!(p.params(), vec![("param", "test"), ("*1", "")]); 646 | 647 | assert_eq!(tree.find("/v1/some/resource/name:customVerb??/test"), None); 648 | 649 | // / 650 | // └── api/v1/ 651 | // └── ** •0 652 | let mut tree = PathTree::::new(); 653 | 654 | tree.insert("/api/v1/*", 1); 655 | 656 | assert_eq!(tree.find("/api/v1"), None); 657 | 658 | let (h, p) = tree.find("/api/v1/").unwrap(); 659 | assert_eq!(*h, 1); 660 | assert_eq!(p.params(), vec![("*1", "")]); 661 | 662 | let (h, p) = tree.find("/api/v1/entity").unwrap(); 663 | assert_eq!(*h, 1); 664 | assert_eq!(p.params(), vec![("*1", "entity")]); 665 | 666 | let (h, p) = tree.find("/api/v1/entity/1/2").unwrap(); 667 | assert_eq!(*h, 1); 668 | assert_eq!(p.params(), vec![("*1", "entity/1/2")]); 669 | 670 | let (h, p) = tree.find("/api/v1/Entity/1/2").unwrap(); 671 | assert_eq!(*h, 1); 672 | assert_eq!(p.params(), vec![("*1", "Entity/1/2")]); 673 | 674 | // / 675 | // └── api/v1/ 676 | // └── : •0 677 | let mut tree = PathTree::::new(); 678 | 679 | tree.insert("/api/v1/:param", 1); 680 | 681 | assert_eq!(tree.find("/api/v1"), None); 682 | assert_eq!(tree.find("/api/v1/"), None); 683 | 684 | let (h, p) = tree.find("/api/v1/entity").unwrap(); 685 | assert_eq!(*h, 1); 686 | assert_eq!(p.params(), vec![("param", "entity")]); 687 | 688 | assert_eq!(tree.find("/api/v1/entity/1/2"), None); 689 | assert_eq!(tree.find("/api/v1/Entity/1/2"), None); 690 | 691 | // / 692 | // └── api/v1/ 693 | // └── : 694 | // ├── - 695 | // │ └── : •1 696 | // ├── . 697 | // │ └── : •3 698 | // ├── \: 699 | // │ └── : •5 700 | // ├── _ 701 | // │ └── : •4 702 | // ├── ~ 703 | // │ └── : •2 704 | // └── / 705 | // └── : •0 706 | let mut tree = PathTree::::new(); 707 | 708 | tree.insert("/api/v1/:param/:param2", 3); 709 | tree.insert("/api/v1/:param-:param2", 1); 710 | tree.insert("/api/v1/:param~:param2", 2); 711 | tree.insert("/api/v1/:param.:param2", 4); 712 | tree.insert("/api/v1/:param\\_:param2", 5); 713 | tree.insert("/api/v1/:param\\::param2", 6); 714 | 715 | let (h, p) = tree.find("/api/v1/entity-entity2").unwrap(); 716 | assert_eq!(*h, 1); 717 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 718 | 719 | let (h, p) = tree.find("/api/v1/entity~entity2").unwrap(); 720 | assert_eq!(*h, 2); 721 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 722 | 723 | let (h, p) = tree.find("/api/v1/entity.entity2").unwrap(); 724 | assert_eq!(*h, 4); 725 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 726 | 727 | let (h, p) = tree.find("/api/v1/entity_entity2").unwrap(); 728 | assert_eq!(*h, 5); 729 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 730 | 731 | let (h, p) = tree.find("/api/v1/entity:entity2").unwrap(); 732 | assert_eq!(*h, 6); 733 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 734 | 735 | let (h, p) = tree.find("/api/v1/entity/entity2").unwrap(); 736 | assert_eq!(*h, 3); 737 | assert_eq!(p.params(), vec![("param", "entity"), ("param2", "entity2")]); 738 | 739 | assert_eq!(tree.find("/api/v1"), None); 740 | assert_eq!(tree.find("/api/v1/"), None); 741 | 742 | let (h, p) = tree.find("/api/v1/test.pdf").unwrap(); 743 | assert_eq!(*h, 4); 744 | assert_eq!(p.params(), vec![("param", "test"), ("param2", "pdf")]); 745 | 746 | // / 747 | // └── api/v1/const •0 748 | let mut tree = PathTree::::new(); 749 | 750 | tree.insert("/api/v1/const", 1); 751 | 752 | let (h, p) = tree.find("/api/v1/const").unwrap(); 753 | assert_eq!(*p.id, 0); 754 | assert_eq!(*h, 1); 755 | assert!(p.params().is_empty()); 756 | assert_eq!(p.pattern(), "/api/v1/const"); 757 | assert_eq!(p.pieces, vec![Piece::String(b"/api/v1/const".to_vec())]); 758 | 759 | assert_eq!(tree.find("/api/v1/cons"), None); 760 | assert_eq!(tree.find("/api/v1/conststatic"), None); 761 | assert_eq!(tree.find("/api/v1/let"), None); 762 | assert_eq!(tree.find("/api/v1/"), None); 763 | assert_eq!(tree.find("/api/v1"), None); 764 | 765 | // / 766 | // └── api/ 767 | // └── : 768 | // └── /fixedEnd •0 769 | let mut tree = PathTree::::new(); 770 | 771 | tree.insert("/api/:param/fixedEnd", 1); 772 | 773 | let (h, p) = tree.find("/api/abc/fixedEnd").unwrap(); 774 | assert_eq!(*p.id, 0); 775 | assert_eq!(*h, 1); 776 | assert_eq!( 777 | p.pieces, 778 | &vec![ 779 | Piece::String(b"/api/".to_vec()), 780 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 781 | Piece::String(b"/fixedEnd".to_vec()), 782 | ] 783 | ); 784 | assert_eq!(p.params(), vec![("param", "abc")]); 785 | assert_eq!(p.pattern(), "/api/:param/fixedEnd"); 786 | 787 | assert_eq!(tree.find("/api/abc/def/fixedEnd"), None); 788 | 789 | // / 790 | // └── shop/product/ 791 | // └── \: 792 | // └── : 793 | // └── /color 794 | // └── \: 795 | // └── : 796 | // └── /size 797 | // └── \: 798 | // └── : •0 799 | let mut tree = PathTree::::new(); 800 | 801 | tree.insert(r"/shop/product/\::filter/color\::color/size\::size", 1); 802 | 803 | let (h, p) = tree.find("/shop/product/:test/color:blue/size:xs").unwrap(); 804 | assert_eq!(*p.id, 0); 805 | assert_eq!(*h, 1); 806 | assert_eq!( 807 | p.pieces, 808 | &vec![ 809 | Piece::String(b"/shop/product/".to_vec()), 810 | Piece::String(b":".to_vec()), 811 | Piece::Parameter(Position::Named(b"filter".to_vec()), Kind::Normal), 812 | Piece::String(b"/color".to_vec()), 813 | Piece::String(b":".to_vec()), 814 | Piece::Parameter(Position::Named(b"color".to_vec()), Kind::Normal), 815 | Piece::String(b"/size".to_vec()), 816 | Piece::String(b":".to_vec()), 817 | Piece::Parameter(Position::Named(b"size".to_vec()), Kind::Normal), 818 | ] 819 | ); 820 | assert_eq!( 821 | p.pattern(), 822 | r"/shop/product/\::filter/color\::color/size\::size" 823 | ); 824 | assert_eq!( 825 | p.params(), 826 | vec![("filter", "test"), ("color", "blue"), ("size", "xs")] 827 | ); 828 | 829 | assert_eq!(tree.find("/shop/product/test/color:blue/size:xs"), None); 830 | 831 | // / 832 | // └── \: 833 | // └── ? •0 834 | let mut tree = PathTree::::new(); 835 | 836 | tree.insert("/\\::param?", 1); 837 | 838 | let (h, p) = tree.find("/:hello").unwrap(); 839 | assert_eq!(*p.id, 0); 840 | assert_eq!(*h, 1); 841 | assert_eq!(p.params(), vec![("param", "hello")]); 842 | assert_eq!(p.pattern(), "/\\::param?"); 843 | assert_eq!( 844 | p.pieces, 845 | &vec![ 846 | Piece::String(b"/".to_vec()), 847 | Piece::String(b":".to_vec()), 848 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Optional), 849 | ] 850 | ); 851 | 852 | let (h, p) = tree.find("/:").unwrap(); 853 | assert_eq!(*h, 1); 854 | assert_eq!(p.params(), vec![("param", "")]); 855 | 856 | assert_eq!(tree.find("/"), None); 857 | 858 | // / 859 | // └── test 860 | // └── : 861 | // └── : •0 862 | let mut tree = PathTree::::new(); 863 | 864 | tree.insert("/test:sign:param", 1); 865 | 866 | let (h, p) = tree.find("/test-abc").unwrap(); 867 | assert_eq!(*p.id, 0); 868 | assert_eq!(*h, 1); 869 | assert_eq!(p.params(), vec![("sign", "-"), ("param", "abc")]); 870 | assert_eq!(p.pattern(), "/test:sign:param"); 871 | assert_eq!( 872 | p.pieces, 873 | &vec![ 874 | Piece::String(b"/test".to_vec()), 875 | Piece::Parameter(Position::Named(b"sign".to_vec()), Kind::Normal), 876 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 877 | ] 878 | ); 879 | 880 | let (h, p) = tree.find("/test-_").unwrap(); 881 | assert_eq!(*h, 1); 882 | assert_eq!(p.params(), vec![("sign", "-"), ("param", "_")]); 883 | 884 | assert_eq!(tree.find("/test-"), None); 885 | assert_eq!(tree.find("/test"), None); 886 | 887 | // / 888 | // └── : 889 | // └── ? 890 | // └── : •0 891 | let mut tree = PathTree::::new(); 892 | 893 | tree.insert("/:param1:param2?:param3", 1); 894 | 895 | let (h, p) = tree.find("/abbbc").unwrap(); 896 | assert_eq!(*p.id, 0); 897 | assert_eq!(*h, 1); 898 | assert_eq!( 899 | p.params(), 900 | vec![("param1", "a"), ("param2", "b"), ("param3", "bbc")] 901 | ); 902 | assert_eq!(p.pattern(), "/:param1:param2?:param3"); 903 | assert_eq!( 904 | p.pieces, 905 | &vec![ 906 | Piece::String(b"/".to_vec()), 907 | Piece::Parameter(Position::Named(b"param1".to_vec()), Kind::Normal), 908 | Piece::Parameter(Position::Named(b"param2".to_vec()), Kind::Optional), 909 | Piece::Parameter(Position::Named(b"param3".to_vec()), Kind::Normal), 910 | ] 911 | ); 912 | 913 | let (h, p) = tree.find("/ab").unwrap(); 914 | assert_eq!(*h, 1); 915 | assert_eq!( 916 | p.params(), 917 | vec![("param1", "a"), ("param2", ""), ("param3", "b")] 918 | ); 919 | 920 | assert_eq!(tree.find("/a"), None); 921 | 922 | // / 923 | // └── test 924 | // └── ? 925 | // └── : •0 926 | let mut tree = PathTree::::new(); 927 | 928 | tree.insert("/test:optional?:mandatory", 1); 929 | 930 | let (h, p) = tree.find("/testo").unwrap(); 931 | assert_eq!(*p.id, 0); 932 | assert_eq!(*h, 1); 933 | assert_eq!(p.params(), vec![("optional", ""), ("mandatory", "o")]); 934 | assert_eq!(p.pattern(), "/test:optional?:mandatory"); 935 | assert_eq!( 936 | p.pieces, 937 | &vec![ 938 | Piece::String(b"/test".to_vec()), 939 | Piece::Parameter(Position::Named(b"optional".to_vec()), Kind::Optional), 940 | Piece::Parameter(Position::Named(b"mandatory".to_vec()), Kind::Normal), 941 | ] 942 | ); 943 | 944 | let (h, p) = tree.find("/testoaaa").unwrap(); 945 | assert_eq!(*h, 1); 946 | assert_eq!(p.params(), vec![("optional", "o"), ("mandatory", "aaa")]); 947 | 948 | assert_eq!(tree.find("/test"), None); 949 | assert_eq!(tree.find("/tes"), None); 950 | 951 | // / 952 | // └── test 953 | // └── ? 954 | // └── ? •0 955 | let mut tree = PathTree::::new(); 956 | 957 | tree.insert("/test:optional?:optional2?", 1); 958 | 959 | let (h, p) = tree.find("/testo").unwrap(); 960 | assert_eq!(*p.id, 0); 961 | assert_eq!(*h, 1); 962 | assert_eq!(p.params(), vec![("optional", "o"), ("optional2", "")]); 963 | assert_eq!(p.pattern(), "/test:optional?:optional2?"); 964 | assert_eq!( 965 | p.pieces, 966 | &vec![ 967 | Piece::String(b"/test".to_vec()), 968 | Piece::Parameter(Position::Named(b"optional".to_vec()), Kind::Optional), 969 | Piece::Parameter(Position::Named(b"optional2".to_vec()), Kind::Optional), 970 | ] 971 | ); 972 | 973 | let (h, p) = tree.find("/testoaaa").unwrap(); 974 | assert_eq!(*h, 1); 975 | assert_eq!(p.params(), vec![("optional", "o"), ("optional2", "aaa")]); 976 | 977 | let (h, p) = tree.find("/test").unwrap(); 978 | assert_eq!(*h, 1); 979 | assert_eq!(p.params(), vec![("optional", ""), ("optional2", "")]); 980 | 981 | assert_eq!(tree.find("/tes"), None); 982 | 983 | // / 984 | // └── foo 985 | // └── ? 986 | // └── bar •0 987 | let mut tree = PathTree::::new(); 988 | 989 | tree.insert("/foo:param?bar", 1); 990 | 991 | let (h, p) = tree.find("/foofalsebar").unwrap(); 992 | assert_eq!(*p.id, 0); 993 | assert_eq!(*h, 1); 994 | assert_eq!(p.params(), vec![("param", "false")]); 995 | assert_eq!(p.pattern(), "/foo:param?bar"); 996 | assert_eq!( 997 | p.pieces, 998 | &vec![ 999 | Piece::String(b"/foo".to_vec()), 1000 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Optional), 1001 | Piece::String(b"bar".to_vec()), 1002 | ] 1003 | ); 1004 | 1005 | let (h, p) = tree.find("/foobar").unwrap(); 1006 | assert_eq!(*h, 1); 1007 | assert_eq!(p.params(), vec![("param", "")]); 1008 | 1009 | assert_eq!(tree.find("/fooba"), None); 1010 | assert_eq!(tree.find("/foo"), None); 1011 | 1012 | // / 1013 | // └── foo 1014 | // └── * 1015 | // └── bar •0 1016 | let mut tree = PathTree::::new(); 1017 | 1018 | tree.insert("/foo*bar", 1); 1019 | 1020 | let (h, p) = tree.find("/foofalsebar").unwrap(); 1021 | assert_eq!(*p.id, 0); 1022 | assert_eq!(*h, 1); 1023 | assert_eq!(p.params(), vec![("*1", "false")]); 1024 | assert_eq!(p.pattern(), "/foo*bar"); 1025 | assert_eq!( 1026 | p.pieces, 1027 | &vec![ 1028 | Piece::String(b"/foo".to_vec()), 1029 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 1030 | Piece::String(b"bar".to_vec()), 1031 | ] 1032 | ); 1033 | 1034 | let (h, p) = tree.find("/foobar").unwrap(); 1035 | assert_eq!(*h, 1); 1036 | assert_eq!(p.params(), vec![("*1", "")]); 1037 | 1038 | let (h, p) = tree.find("/foo/bar").unwrap(); 1039 | assert_eq!(*h, 1); 1040 | assert_eq!(p.params(), vec![("*1", "/")]); 1041 | 1042 | let (h, p) = tree.find("/foo/baz/bar").unwrap(); 1043 | assert_eq!(*h, 1); 1044 | assert_eq!(p.params(), vec![("*1", "/baz/")]); 1045 | 1046 | assert_eq!(tree.find("/fooba"), None); 1047 | assert_eq!(tree.find("/foo"), None); 1048 | 1049 | // / 1050 | // └── foo 1051 | // └── + 1052 | // └── bar •0 1053 | let mut tree = PathTree::::new(); 1054 | 1055 | tree.insert("/foo+bar", 1); 1056 | 1057 | let (h, p) = tree.find("/foofalsebar").unwrap(); 1058 | assert_eq!(*p.id, 0); 1059 | assert_eq!(*h, 1); 1060 | assert_eq!(p.params(), vec![("+1", "false")]); 1061 | assert_eq!(p.pattern(), "/foo+bar"); 1062 | assert_eq!( 1063 | p.pieces, 1064 | &vec![ 1065 | Piece::String(b"/foo".to_vec()), 1066 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 1067 | Piece::String(b"bar".to_vec()), 1068 | ] 1069 | ); 1070 | 1071 | assert_eq!(tree.find("/foobar"), None); 1072 | 1073 | let (h, p) = tree.find("/foo/bar").unwrap(); 1074 | assert_eq!(*h, 1); 1075 | assert_eq!(p.params(), vec![("+1", "/")]); 1076 | 1077 | let (h, p) = tree.find("/foo/baz/bar").unwrap(); 1078 | assert_eq!(*h, 1); 1079 | assert_eq!(p.params(), vec![("+1", "/baz/")]); 1080 | 1081 | assert_eq!(tree.find("/fooba"), None); 1082 | assert_eq!(tree.find("/foo"), None); 1083 | 1084 | // / 1085 | // └── a 1086 | // └── * 1087 | // └── cde 1088 | // └── * 1089 | // └── g/ •0 1090 | let mut tree = PathTree::::new(); 1091 | 1092 | tree.insert("/a*cde*g/", 1); 1093 | 1094 | assert_eq!(tree.find("/abbbcdefffg"), None); 1095 | 1096 | let (h, p) = tree.find("/abbbcdefffg/").unwrap(); 1097 | assert_eq!(h, &1); 1098 | assert_eq!( 1099 | p.pieces, 1100 | vec![ 1101 | Piece::String(b"/a".to_vec()), 1102 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 1103 | Piece::String(b"cde".to_vec()), 1104 | Piece::Parameter(Position::Index(2, b"*2".to_vec()), Kind::ZeroOrMore), 1105 | Piece::String(b"g/".to_vec()), 1106 | ] 1107 | ); 1108 | assert_eq!(p.pattern(), "/a*cde*g/"); 1109 | assert_eq!(p.params(), vec![("*1", "bbb"), ("*2", "fff")]); 1110 | 1111 | let (_, p) = tree.find("/acdeg/").unwrap(); 1112 | assert_eq!(p.params(), vec![("*1", ""), ("*2", "")]); 1113 | 1114 | let (_, p) = tree.find("/abcdeg/").unwrap(); 1115 | assert_eq!(p.params(), vec![("*1", "b"), ("*2", "")]); 1116 | 1117 | let (_, p) = tree.find("/acdefg/").unwrap(); 1118 | assert_eq!(p.params(), vec![("*1", ""), ("*2", "f")]); 1119 | 1120 | let (_, p) = tree.find("/abcdefg/").unwrap(); 1121 | assert_eq!(p.params(), vec![("*1", "b"), ("*2", "f")]); 1122 | 1123 | let (_, p) = tree.find("/a/cde/g/").unwrap(); 1124 | assert_eq!(p.params(), vec![("*1", "/"), ("*2", "/")]); 1125 | 1126 | let (_, p) = tree.find("/a/b/cde/f/g/").unwrap(); 1127 | assert_eq!(p.params(), vec![("*1", "/b/"), ("*2", "/f/")]); 1128 | 1129 | // / 1130 | // └── * 1131 | // └── v1 1132 | // └── * 1133 | // └── proxy/ •0 1134 | let mut tree = PathTree::::new(); 1135 | 1136 | tree.insert("/*v1*/proxy", 1); 1137 | 1138 | let (h, p) = tree.find("/customer/v1/cart/proxy").unwrap(); 1139 | assert_eq!(h, &1); 1140 | assert_eq!( 1141 | p.pieces, 1142 | vec![ 1143 | Piece::String(b"/".to_vec()), 1144 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 1145 | Piece::String(b"v1".to_vec()), 1146 | Piece::Parameter(Position::Index(2, b"*2".to_vec()), Kind::ZeroOrMore), 1147 | Piece::String(b"/proxy".to_vec()), 1148 | ] 1149 | ); 1150 | assert_eq!(p.pattern(), "/*v1*/proxy"); 1151 | assert_eq!(p.params(), vec![("*1", "customer/"), ("*2", "/cart")]); 1152 | 1153 | let (_, p) = tree.find("/v1/proxy").unwrap(); 1154 | assert_eq!(p.params(), vec![("*1", ""), ("*2", "")]); 1155 | 1156 | assert_eq!(tree.find("/v1/"), None); 1157 | 1158 | // / 1159 | // ├── - 1160 | // │ └── : •2 1161 | // ├── . 1162 | // │ └── : •3 1163 | // ├── @ 1164 | // │ └── : •1 1165 | // ├── _ 1166 | // │ └── : •5 1167 | // ├── name 1168 | // │ └── \: 1169 | // │ └── : •0 1170 | // ├── ~ 1171 | // │ └── : •4 1172 | // └── : •6 1173 | let mut tree = PathTree::::new(); 1174 | 1175 | tree.insert("/name\\::name", 1); 1176 | tree.insert("/@:name", 2); 1177 | tree.insert("/-:name", 3); 1178 | tree.insert("/.:name", 4); 1179 | tree.insert("/~:name", 5); 1180 | tree.insert("/_:name", 6); 1181 | tree.insert("/:name", 7); 1182 | 1183 | let (h, p) = tree.find("/name:john").unwrap(); 1184 | assert_eq!(h, &1); 1185 | assert_eq!( 1186 | p.pieces, 1187 | vec![ 1188 | Piece::String(b"/name".to_vec()), 1189 | Piece::String(b":".to_vec()), 1190 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1191 | ] 1192 | ); 1193 | assert_eq!(p.pattern(), "/name\\::name"); 1194 | assert_eq!(p.params(), vec![("name", "john")]); 1195 | 1196 | let (h, p) = tree.find("/@john").unwrap(); 1197 | assert_eq!(h, &2); 1198 | assert_eq!( 1199 | p.pieces, 1200 | vec![ 1201 | Piece::String(b"/@".to_vec()), 1202 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1203 | ] 1204 | ); 1205 | assert_eq!(p.pattern(), "/@:name"); 1206 | assert_eq!(p.params(), vec![("name", "john")]); 1207 | 1208 | let (h, p) = tree.find("/-john").unwrap(); 1209 | assert_eq!(h, &3); 1210 | assert_eq!( 1211 | p.pieces, 1212 | vec![ 1213 | Piece::String(b"/-".to_vec()), 1214 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1215 | ] 1216 | ); 1217 | assert_eq!(p.pattern(), "/-:name"); 1218 | assert_eq!(p.params(), vec![("name", "john")]); 1219 | 1220 | let (h, p) = tree.find("/.john").unwrap(); 1221 | assert_eq!(h, &4); 1222 | assert_eq!( 1223 | p.pieces, 1224 | vec![ 1225 | Piece::String(b"/.".to_vec()), 1226 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1227 | ] 1228 | ); 1229 | assert_eq!(p.pattern(), "/.:name"); 1230 | assert_eq!(p.params(), vec![("name", "john")]); 1231 | 1232 | let (h, p) = tree.find("/~john").unwrap(); 1233 | assert_eq!(h, &5); 1234 | assert_eq!( 1235 | p.pieces, 1236 | vec![ 1237 | Piece::String(b"/~".to_vec()), 1238 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1239 | ] 1240 | ); 1241 | assert_eq!(p.pattern(), "/~:name"); 1242 | assert_eq!(p.params(), vec![("name", "john")]); 1243 | 1244 | let (h, p) = tree.find("/_john").unwrap(); 1245 | assert_eq!(h, &6); 1246 | assert_eq!( 1247 | p.pieces, 1248 | vec![ 1249 | Piece::String(b"/_".to_vec()), 1250 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1251 | ] 1252 | ); 1253 | assert_eq!(p.pattern(), "/_:name"); 1254 | assert_eq!(p.params(), vec![("name", "john")]); 1255 | 1256 | let (h, p) = tree.find("/john").unwrap(); 1257 | assert_eq!(h, &7); 1258 | assert_eq!( 1259 | p.pieces, 1260 | vec![ 1261 | Piece::String(b"/".to_vec()), 1262 | Piece::Parameter(Position::Named(b"name".to_vec()), Kind::Normal), 1263 | ] 1264 | ); 1265 | assert_eq!(p.pattern(), "/:name"); 1266 | assert_eq!(p.params(), vec![("name", "john")]); 1267 | 1268 | // / 1269 | // └── api/v1/ 1270 | // └── : 1271 | // └── /abc/ 1272 | // └── ** •0 1273 | let mut tree = PathTree::::new(); 1274 | 1275 | tree.insert("/api/v1/:param/abc/*", 1); 1276 | 1277 | let (h, p) = tree.find("/api/v1/well/abc/wildcard").unwrap(); 1278 | assert_eq!(h, &1); 1279 | assert_eq!( 1280 | p.pieces, 1281 | vec![ 1282 | Piece::String(b"/api/v1/".to_vec()), 1283 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 1284 | Piece::String(b"/abc/".to_vec()), 1285 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 1286 | ] 1287 | ); 1288 | assert_eq!(p.pattern(), "/api/v1/:param/abc/*"); 1289 | assert_eq!(p.params(), vec![("param", "well"), ("*1", "wildcard")]); 1290 | 1291 | let (_, p) = tree.find("/api/v1/well/abc/").unwrap(); 1292 | assert_eq!(p.params(), vec![("param", "well"), ("*1", "")]); 1293 | 1294 | assert_eq!(tree.find("/api/v1/well/abc"), None); 1295 | assert_eq!(tree.find("/api/v1/well/ttt"), None); 1296 | 1297 | // / 1298 | // └── api/ 1299 | // └── : 1300 | // └── / 1301 | // └── ?? 1302 | // └── / 1303 | // └── ?? •0 1304 | let mut tree = PathTree::::new(); 1305 | 1306 | tree.insert("/api/:day/:month?/:year?", 1); 1307 | 1308 | assert_eq!(tree.find("/api/1"), None); 1309 | 1310 | let (h, p) = tree.find("/api/1/").unwrap(); 1311 | assert_eq!(h, &1); 1312 | assert_eq!( 1313 | p.pieces, 1314 | vec![ 1315 | Piece::String(b"/api/".to_vec()), 1316 | Piece::Parameter(Position::Named(b"day".to_vec()), Kind::Normal), 1317 | Piece::String(b"/".to_vec()), 1318 | Piece::Parameter(Position::Named(b"month".to_vec()), Kind::OptionalSegment), 1319 | Piece::String(b"/".to_vec()), 1320 | Piece::Parameter(Position::Named(b"year".to_vec()), Kind::OptionalSegment), 1321 | ] 1322 | ); 1323 | assert_eq!(p.pattern(), "/api/:day/:month?/:year?"); 1324 | assert_eq!(p.params(), vec![("day", "1"), ("month", ""), ("year", "")]); 1325 | 1326 | let (_, p) = tree.find("/api/1//").unwrap(); 1327 | assert_eq!(p.params(), vec![("day", "1"), ("month", ""), ("year", "")]); 1328 | 1329 | let (_, p) = tree.find("/api/1/-/").unwrap(); 1330 | assert_eq!(p.params(), vec![("day", "1"), ("month", "-"), ("year", "")]); 1331 | 1332 | assert_eq!(tree.find("/api/1-"), None); 1333 | 1334 | let (_, p) = tree.find("/api/1-/").unwrap(); 1335 | assert_eq!(p.params(), vec![("day", "1-"), ("month", ""), ("year", "")]); 1336 | 1337 | let (_, p) = tree.find("/api/1/2").unwrap(); 1338 | assert_eq!(p.params(), vec![("day", "1"), ("month", "2"), ("year", "")]); 1339 | 1340 | let (_, p) = tree.find("/api/1/2/3").unwrap(); 1341 | assert_eq!( 1342 | p.params(), 1343 | vec![("day", "1"), ("month", "2"), ("year", "3")] 1344 | ); 1345 | 1346 | // / 1347 | // └── api/ 1348 | // └── : 1349 | // └── . 1350 | // └── ? 1351 | // └── . 1352 | // └── ? •0 1353 | let mut tree = PathTree::::new(); 1354 | 1355 | tree.insert("/api/:day.:month?.:year?", 1); 1356 | tree.insert("/api/:day-:month?-:year?", 2); 1357 | 1358 | assert_eq!(tree.find("/api/1"), None); 1359 | assert_eq!(tree.find("/api/1/"), None); 1360 | assert_eq!(tree.find("/api/1."), None); 1361 | 1362 | let (h, p) = tree.find("/api/1..").unwrap(); 1363 | assert_eq!(h, &1); 1364 | assert_eq!( 1365 | p.pieces, 1366 | vec![ 1367 | Piece::String(b"/api/".to_vec()), 1368 | Piece::Parameter(Position::Named(b"day".to_vec()), Kind::Normal), 1369 | Piece::String(b".".to_vec()), 1370 | Piece::Parameter(Position::Named(b"month".to_vec()), Kind::Optional), 1371 | Piece::String(b".".to_vec()), 1372 | Piece::Parameter(Position::Named(b"year".to_vec()), Kind::Optional), 1373 | ] 1374 | ); 1375 | assert_eq!(p.pattern(), "/api/:day.:month?.:year?"); 1376 | assert_eq!(p.params(), vec![("day", "1"), ("month", ""), ("year", "")]); 1377 | 1378 | let (h, p) = tree.find("/api/1.2.").unwrap(); 1379 | assert_eq!(h, &1); 1380 | assert_eq!(p.params(), vec![("day", "1"), ("month", "2"), ("year", "")]); 1381 | 1382 | let (h, p) = tree.find("/api/1.2.3").unwrap(); 1383 | assert_eq!(h, &1); 1384 | assert_eq!( 1385 | p.params(), 1386 | vec![("day", "1"), ("month", "2"), ("year", "3")] 1387 | ); 1388 | 1389 | let (h, p) = tree.find("/api/1--").unwrap(); 1390 | assert_eq!(h, &2); 1391 | assert_eq!( 1392 | p.pieces, 1393 | vec![ 1394 | Piece::String(b"/api/".to_vec()), 1395 | Piece::Parameter(Position::Named(b"day".to_vec()), Kind::Normal), 1396 | Piece::String(b"-".to_vec()), 1397 | Piece::Parameter(Position::Named(b"month".to_vec()), Kind::Optional), 1398 | Piece::String(b"-".to_vec()), 1399 | Piece::Parameter(Position::Named(b"year".to_vec()), Kind::Optional), 1400 | ] 1401 | ); 1402 | assert_eq!(p.pattern(), "/api/:day-:month?-:year?"); 1403 | assert_eq!(p.params(), vec![("day", "1"), ("month", ""), ("year", "")]); 1404 | 1405 | let (h, p) = tree.find("/api/1-2-").unwrap(); 1406 | assert_eq!(h, &2); 1407 | assert_eq!(p.params(), vec![("day", "1"), ("month", "2"), ("year", "")]); 1408 | 1409 | let (h, p) = tree.find("/api/1-2-3").unwrap(); 1410 | assert_eq!(h, &2); 1411 | assert_eq!( 1412 | p.params(), 1413 | vec![("day", "1"), ("month", "2"), ("year", "3")] 1414 | ); 1415 | 1416 | assert_eq!(tree.find("/api/1.2-3"), None); 1417 | 1418 | // / 1419 | // └── config/ 1420 | // ├── abc.json •0 1421 | // ├── + 1422 | // │ └── .json •1 1423 | // └── * 1424 | // └── .json •2 1425 | let mut tree = PathTree::::new(); 1426 | 1427 | tree.insert("/config/abc.json", 1); 1428 | tree.insert("/config/+.json", 2); 1429 | tree.insert("/config/*.json", 3); 1430 | 1431 | let (h, p) = tree.find("/config/abc.json").unwrap(); 1432 | assert_eq!(h, &1); 1433 | assert_eq!(p.pieces, vec![Piece::String(b"/config/abc.json".to_vec())]); 1434 | assert_eq!(p.pattern(), "/config/abc.json"); 1435 | assert_eq!(p.params(), vec![]); 1436 | 1437 | let (h, p) = tree.find("/config/a.json").unwrap(); 1438 | assert_eq!(h, &2); 1439 | assert_eq!( 1440 | p.pieces, 1441 | vec![ 1442 | Piece::String(b"/config/".to_vec()), 1443 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 1444 | Piece::String(b".json".to_vec()), 1445 | ] 1446 | ); 1447 | assert_eq!(p.pattern(), "/config/+.json"); 1448 | assert_eq!(p.params(), vec![("+1", "a")]); 1449 | 1450 | let (h, p) = tree.find("/config/ab.json").unwrap(); 1451 | assert_eq!(h, &2); 1452 | assert_eq!(p.params(), vec![("+1", "ab")]); 1453 | 1454 | let (h, p) = tree.find("/config/a/b.json").unwrap(); 1455 | assert_eq!(h, &2); 1456 | assert_eq!(p.params(), vec![("+1", "a/b")]); 1457 | 1458 | let (h, p) = tree.find("/config/a/b/abc.json").unwrap(); 1459 | assert_eq!(h, &2); 1460 | assert_eq!(p.params(), vec![("+1", "a/b/abc")]); 1461 | 1462 | let (h, p) = tree.find("/config/.json").unwrap(); 1463 | assert_eq!(h, &3); 1464 | assert_eq!( 1465 | p.pieces, 1466 | vec![ 1467 | Piece::String(b"/config/".to_vec()), 1468 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMore), 1469 | Piece::String(b".json".to_vec()), 1470 | ] 1471 | ); 1472 | assert_eq!(p.pattern(), "/config/*.json"); 1473 | assert_eq!(p.params(), vec![("*1", "")]); 1474 | 1475 | // / 1476 | // └── api/ 1477 | // └── ** 1478 | // └── / 1479 | // └── ?? •0 1480 | let mut tree = PathTree::::new(); 1481 | 1482 | tree.insert("/api/*/:param?", 1); 1483 | 1484 | let (h, p) = tree.find("/api/").unwrap(); 1485 | assert_eq!(h, &1); 1486 | assert_eq!( 1487 | p.pieces, 1488 | vec![ 1489 | Piece::String(b"/api/".to_vec()), 1490 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 1491 | Piece::String(b"/".to_vec()), 1492 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::OptionalSegment), 1493 | ] 1494 | ); 1495 | assert_eq!(p.pattern(), "/api/*/:param?"); 1496 | assert_eq!(p.params(), vec![("*1", ""), ("param", "")]); 1497 | 1498 | let (_, p) = tree.find("/api/joker").unwrap(); 1499 | assert_eq!(p.params(), vec![("*1", ""), ("param", "joker")]); 1500 | 1501 | let (_, p) = tree.find("/api/joker/").unwrap(); 1502 | assert_eq!(p.params(), vec![("*1", "joker"), ("param", "")]); 1503 | 1504 | let (_, p) = tree.find("/api/joker/batman").unwrap(); 1505 | assert_eq!(p.params(), vec![("*1", "joker"), ("param", "batman")]); 1506 | 1507 | let (_, p) = tree.find("/api/joker/batman/robin").unwrap(); 1508 | assert_eq!(p.params(), vec![("*1", "joker/batman"), ("param", "robin")]); 1509 | 1510 | let (_, p) = tree.find("/api/joker/batman/robin/1").unwrap(); 1511 | assert_eq!( 1512 | p.params(), 1513 | vec![("*1", "joker/batman/robin"), ("param", "1")] 1514 | ); 1515 | 1516 | // / 1517 | // └── api/ 1518 | // └── ** 1519 | // └── / 1520 | // └── : •0 1521 | let mut tree = PathTree::::new(); 1522 | 1523 | tree.insert("/api/*/:param", 1); 1524 | 1525 | let (h, p) = tree.find("/api/test/abc").unwrap(); 1526 | assert_eq!(h, &1); 1527 | assert_eq!( 1528 | p.pieces, 1529 | vec![ 1530 | Piece::String(b"/api/".to_vec()), 1531 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 1532 | Piece::String(b"/".to_vec()), 1533 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 1534 | ] 1535 | ); 1536 | assert_eq!(p.pattern(), "/api/*/:param"); 1537 | assert_eq!(p.params(), vec![("*1", "test"), ("param", "abc")]); 1538 | 1539 | let (_, p) = tree.find("/api/joker/batman/robin/1").unwrap(); 1540 | assert_eq!( 1541 | p.params(), 1542 | vec![("*1", "joker/batman/robin"), ("param", "1")] 1543 | ); 1544 | 1545 | let (_, p) = tree.find("/api//joker").unwrap(); 1546 | assert_eq!(p.params(), vec![("*1", ""), ("param", "joker")]); 1547 | 1548 | assert_eq!(tree.find("/api/joker"), None); 1549 | assert_eq!(tree.find("/api/"), None); 1550 | 1551 | // / 1552 | // └── api/ 1553 | // └── + 1554 | // └── / 1555 | // └── : •0 1556 | let mut tree = PathTree::::new(); 1557 | 1558 | tree.insert("/api/+/:param", 1); 1559 | 1560 | let (h, p) = tree.find("/api/test/abc").unwrap(); 1561 | assert_eq!(h, &1); 1562 | assert_eq!( 1563 | p.pieces, 1564 | vec![ 1565 | Piece::String(b"/api/".to_vec()), 1566 | Piece::Parameter(Position::Index(1, b"+1".to_vec()), Kind::OneOrMore), 1567 | Piece::String(b"/".to_vec()), 1568 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 1569 | ] 1570 | ); 1571 | assert_eq!(p.pattern(), "/api/+/:param"); 1572 | assert_eq!(p.params(), vec![("+1", "test"), ("param", "abc")]); 1573 | 1574 | let (_, p) = tree.find("/api/joker/batman/robin/1").unwrap(); 1575 | assert_eq!( 1576 | p.params(), 1577 | vec![("+1", "joker/batman/robin"), ("param", "1")] 1578 | ); 1579 | 1580 | assert_eq!(tree.find("/api/joker"), None); 1581 | assert_eq!(tree.find("/api/"), None); 1582 | 1583 | // / 1584 | // └── api/ 1585 | // └── ** 1586 | // └── / 1587 | // └── : 1588 | // └── / 1589 | // └── : •0 1590 | let mut tree = PathTree::::new(); 1591 | 1592 | tree.insert("/api/*/:param/:param2", 1); 1593 | 1594 | let (h, p) = tree.find("/api/test/abc/1").unwrap(); 1595 | assert_eq!(h, &1); 1596 | assert_eq!( 1597 | p.pieces, 1598 | vec![ 1599 | Piece::String(b"/api/".to_vec()), 1600 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 1601 | Piece::String(b"/".to_vec()), 1602 | Piece::Parameter(Position::Named(b"param".to_vec()), Kind::Normal), 1603 | Piece::String(b"/".to_vec()), 1604 | Piece::Parameter(Position::Named(b"param2".to_vec()), Kind::Normal), 1605 | ] 1606 | ); 1607 | assert_eq!(p.pattern(), "/api/*/:param/:param2"); 1608 | assert_eq!( 1609 | p.params(), 1610 | vec![("*1", "test"), ("param", "abc"), ("param2", "1")] 1611 | ); 1612 | 1613 | assert_eq!(tree.find("/api/joker/batman"), None); 1614 | 1615 | let (_, p) = tree.find("/api/joker/batman-robin/1").unwrap(); 1616 | assert_eq!( 1617 | p.params(), 1618 | vec![("*1", "joker"), ("param", "batman-robin"), ("param2", "1")] 1619 | ); 1620 | 1621 | assert_eq!(tree.find("/api/joker-batman-robin-1"), None); 1622 | assert_eq!(tree.find("/api/test/abc"), None); 1623 | 1624 | let (_, p) = tree.find("/api/joker/batman/robin").unwrap(); 1625 | assert_eq!( 1626 | p.params(), 1627 | vec![("*1", "joker"), ("param", "batman"), ("param2", "robin")] 1628 | ); 1629 | 1630 | let (_, p) = tree.find("/api/joker/batman/robin/1").unwrap(); 1631 | assert_eq!( 1632 | p.params(), 1633 | vec![("*1", "joker/batman"), ("param", "robin"), ("param2", "1")] 1634 | ); 1635 | 1636 | let (_, p) = tree.find("/api/joker/batman/robin/1/2").unwrap(); 1637 | assert_eq!( 1638 | p.params(), 1639 | vec![ 1640 | ("*1", "joker/batman/robin"), 1641 | ("param", "1"), 1642 | ("param2", "2") 1643 | ] 1644 | ); 1645 | 1646 | assert_eq!(tree.find("/api"), None); 1647 | assert_eq!(tree.find("/api/:test"), None); 1648 | } 1649 | 1650 | #[test] 1651 | fn basic() { 1652 | let mut tree = PathTree::::new(); 1653 | 1654 | tree.insert("/", 0); 1655 | tree.insert("/login", 1); 1656 | tree.insert("/signup", 2); 1657 | tree.insert("/settings", 3); 1658 | tree.insert("/settings/:page", 4); 1659 | tree.insert("/:user", 5); 1660 | tree.insert("/:user/:repo", 6); 1661 | tree.insert("/public/:any*", 7); 1662 | tree.insert("/:org/:repo/releases/download/:tag/:filename.:ext", 8); 1663 | tree.insert("/:org/:repo/tags/:day-:month-:year", 9); 1664 | tree.insert("/:org/:repo/actions/:name\\::verb", 10); 1665 | tree.insert("/:org/:repo/:page", 11); 1666 | tree.insert("/:org/:repo/*", 12); 1667 | tree.insert("/api/+", 13); 1668 | 1669 | assert_eq!( 1670 | format!("{:?}", &tree.node), 1671 | r" 1672 | / •0 1673 | ├── api/ 1674 | │ └── + •13 1675 | ├── login •1 1676 | ├── public/ 1677 | │ └── ** •7 1678 | ├── s 1679 | │ ├── ettings •3 1680 | │ │ └── / 1681 | │ │ └── : •4 1682 | │ └── ignup •2 1683 | └── : •5 1684 | └── / 1685 | └── : •6 1686 | └── / 1687 | ├── actions/ 1688 | │ └── : 1689 | │ └── \: 1690 | │ └── : •10 1691 | ├── releases/download/ 1692 | │ └── : 1693 | │ └── / 1694 | │ └── : 1695 | │ └── . 1696 | │ └── : •8 1697 | ├── tags/ 1698 | │ └── : 1699 | │ └── - 1700 | │ └── : 1701 | │ └── - 1702 | │ └── : •9 1703 | ├── : •11 1704 | └── ** •12 1705 | " 1706 | ); 1707 | 1708 | let (h, p) = tree.find("/").unwrap(); 1709 | assert_eq!(h, &0); 1710 | assert_eq!(p.params(), vec![]); 1711 | 1712 | tree.insert("", 14); 1713 | let (h, p) = tree.find("/").unwrap(); 1714 | assert_eq!(h, &14); 1715 | assert_eq!(p.params(), vec![]); 1716 | 1717 | tree.insert("/", 15); 1718 | let (h, p) = tree.find("/").unwrap(); 1719 | assert_eq!(h, &15); 1720 | assert_eq!(p.params(), vec![]); 1721 | 1722 | let (h, p) = tree.find("/login").unwrap(); 1723 | assert_eq!(h, &1); 1724 | assert_eq!(p.params(), vec![]); 1725 | 1726 | let (h, p) = tree.find("/settings/admin").unwrap(); 1727 | assert_eq!(h, &4); 1728 | assert_eq!(p.params(), vec![("page", "admin")]); 1729 | 1730 | let (h, p) = tree.find("/viz-rs").unwrap(); 1731 | assert_eq!(h, &5); 1732 | assert_eq!(p.params(), vec![("user", "viz-rs")]); 1733 | 1734 | let (h, p) = tree.find("/viz-rs/path-tree").unwrap(); 1735 | assert_eq!(h, &6); 1736 | assert_eq!(p.params(), vec![("user", "viz-rs"), ("repo", "path-tree")]); 1737 | 1738 | let (h, p) = tree.find("/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-aarch64-apple-darwin.gz").unwrap(); 1739 | assert_eq!(h, &8); 1740 | assert_eq!( 1741 | p.params(), 1742 | vec![ 1743 | ("org", "rust-lang"), 1744 | ("repo", "rust-analyzer"), 1745 | ("tag", "2022-09-12"), 1746 | ("filename", "rust-analyzer-aarch64-apple-darwin"), 1747 | ("ext", "gz") 1748 | ] 1749 | ); 1750 | 1751 | let (h, p) = tree 1752 | .find("/rust-lang/rust-analyzer/tags/2022-09-12") 1753 | .unwrap(); 1754 | assert_eq!(h, &9); 1755 | assert_eq!( 1756 | p.params(), 1757 | vec![ 1758 | ("org", "rust-lang"), 1759 | ("repo", "rust-analyzer"), 1760 | ("day", "2022"), 1761 | ("month", "09"), 1762 | ("year", "12") 1763 | ] 1764 | ); 1765 | 1766 | let (h, p) = tree 1767 | .find("/rust-lang/rust-analyzer/actions/ci:bench") 1768 | .unwrap(); 1769 | assert_eq!(h, &10); 1770 | assert_eq!( 1771 | p.params(), 1772 | vec![ 1773 | ("org", "rust-lang"), 1774 | ("repo", "rust-analyzer"), 1775 | ("name", "ci"), 1776 | ("verb", "bench"), 1777 | ] 1778 | ); 1779 | 1780 | let (h, p) = tree.find("/rust-lang/rust-analyzer/stargazers").unwrap(); 1781 | assert_eq!(h, &11); 1782 | assert_eq!( 1783 | p.params(), 1784 | vec![ 1785 | ("org", "rust-lang"), 1786 | ("repo", "rust-analyzer"), 1787 | ("page", "stargazers") 1788 | ] 1789 | ); 1790 | 1791 | let (h, p) = tree 1792 | .find("/rust-lang/rust-analyzer/stargazers/404") 1793 | .unwrap(); 1794 | assert_eq!(h, &12); 1795 | assert_eq!( 1796 | p.params(), 1797 | vec![ 1798 | ("org", "rust-lang"), 1799 | ("repo", "rust-analyzer"), 1800 | ("*1", "stargazers/404") 1801 | ] 1802 | ); 1803 | 1804 | let (h, p) = tree.find("/public/js/main.js").unwrap(); 1805 | assert_eq!(h, &7); 1806 | assert_eq!(p.params(), vec![("any", "js/main.js")]); 1807 | 1808 | let (h, p) = tree.find("/api/v1").unwrap(); 1809 | assert_eq!(h, &13); 1810 | assert_eq!(p.params(), vec![("+1", "v1")]); 1811 | } 1812 | 1813 | #[test] 1814 | fn github_tree() { 1815 | let mut tree = PathTree::::new(); 1816 | 1817 | tree.insert("/", 0); 1818 | tree.insert("/api", 1); 1819 | tree.insert("/about", 2); 1820 | tree.insert("/login", 3); 1821 | tree.insert("/signup", 4); 1822 | tree.insert("/pricing", 5); 1823 | 1824 | tree.insert("/features", 6); 1825 | tree.insert("/features/actions", 600); 1826 | tree.insert("/features/packages", 601); 1827 | tree.insert("/features/security", 602); 1828 | tree.insert("/features/codespaces", 603); 1829 | tree.insert("/features/copilot", 604); 1830 | tree.insert("/features/code-review", 605); 1831 | tree.insert("/features/issues", 606); 1832 | tree.insert("/features/discussions", 607); 1833 | 1834 | tree.insert("/enterprise", 7); 1835 | tree.insert("/team", 8); 1836 | tree.insert("/customer-stories", 9); 1837 | tree.insert("/sponsors", 10); 1838 | tree.insert("/readme", 11); 1839 | tree.insert("/topics", 12); 1840 | tree.insert("/trending", 13); 1841 | tree.insert("/collections", 14); 1842 | tree.insert("/search", 15); 1843 | tree.insert("/pulls", 16); 1844 | tree.insert("/issues", 17); 1845 | tree.insert("/marketplace", 18); 1846 | tree.insert("/explore", 19); 1847 | 1848 | tree.insert("/sponsors/explore", 100); 1849 | tree.insert("/sponsors/accounts", 101); 1850 | tree.insert("/sponsors/:repo", 102); 1851 | tree.insert("/sponsors/:repo/:user?", 103); 1852 | tree.insert("/sponsors/:repo/+", 104); 1853 | tree.insert("/sponsors/:repo/:user", 105); 1854 | tree.insert("/sponsors/:repo/issues/*", 106); 1855 | tree.insert("/sponsors/:repo/+/:file", 107); 1856 | tree.insert("/sponsors/:repo/+/:filename.:ext", 108); 1857 | 1858 | tree.insert("/about/careers", 200); 1859 | tree.insert("/about/press", 201); 1860 | tree.insert("/about/diversity", 202); 1861 | 1862 | tree.insert("/settings", 20); 1863 | tree.insert("/settings/admin", 2000); 1864 | tree.insert("/settings/appearance", 2001); 1865 | tree.insert("/settings/accessibility", 2002); 1866 | tree.insert("/settings/notifications", 2003); 1867 | 1868 | tree.insert("/settings/billing", 2004); 1869 | tree.insert("/settings/billing/plans", 2005); 1870 | tree.insert("/settings/security", 2006); 1871 | tree.insert("/settings/keys", 2007); 1872 | tree.insert("/settings/organizations", 2008); 1873 | 1874 | tree.insert("/settings/blocked_users", 2009); 1875 | tree.insert("/settings/interaction_limits", 2010); 1876 | tree.insert("/settings/code_review_limits", 2011); 1877 | 1878 | tree.insert("/settings/repositories", 2012); 1879 | tree.insert("/settings/codespaces", 2013); 1880 | tree.insert("/settings/deleted_packages", 2014); 1881 | tree.insert("/settings/copilot", 2015); 1882 | tree.insert("/settings/pages", 2016); 1883 | tree.insert("/settings/replies", 2017); 1884 | 1885 | tree.insert("/settings/security_analysis", 2018); 1886 | 1887 | tree.insert("/settings/installations", 2019); 1888 | tree.insert("/settings/reminders", 2020); 1889 | 1890 | tree.insert("/settings/security-log", 2021); 1891 | tree.insert("/settings/sponsors-log", 2022); 1892 | 1893 | tree.insert("/settings/apps", 2023); 1894 | tree.insert("/settings/developers", 2024); 1895 | tree.insert("/settings/tokens", 2025); 1896 | 1897 | tree.insert("/404", 21); 1898 | tree.insert("/500", 22); 1899 | tree.insert("/503", 23); 1900 | 1901 | tree.insert("/:org", 24); 1902 | tree.insert("/:org/:repo", 2400); 1903 | tree.insert("/:org/:repo/issues", 2410); 1904 | tree.insert("/:org/:repo/issues/:id", 2411); 1905 | tree.insert("/:org/:repo/issues/new", 2412); 1906 | tree.insert("/:org/:repo/pulls", 2420); 1907 | tree.insert("/:org/:repo/pull/:id", 2421); 1908 | tree.insert("/:org/:repo/compare", 2422); 1909 | tree.insert("/:org/:repo/discussions", 2430); 1910 | tree.insert("/:org/:repo/discussions/:id", 2431); 1911 | tree.insert("/:org/:repo/actions", 2440); 1912 | tree.insert("/:org/:repo/actions/workflows/:id", 2441); 1913 | tree.insert("/:org/:repo/actions/runs/:id", 2442); 1914 | tree.insert("/:org/:repo/wiki", 2450); 1915 | tree.insert("/:org/:repo/wiki/:id", 2451); 1916 | tree.insert("/:org/:repo/security", 2460); 1917 | tree.insert("/:org/:repo/security/policy", 2461); 1918 | tree.insert("/:org/:repo/security/advisories", 2462); 1919 | tree.insert("/:org/:repo/pulse", 2470); 1920 | tree.insert("/:org/:repo/graphs/contributors", 2480); 1921 | tree.insert("/:org/:repo/graphs/commit-activity", 2481); 1922 | tree.insert("/:org/:repo/graphs/code-frequency", 2482); 1923 | tree.insert("/:org/:repo/community", 2490); 1924 | tree.insert("/:org/:repo/network", 2491); 1925 | tree.insert("/:org/:repo/network/dependencies", 2492); 1926 | tree.insert("/:org/:repo/network/dependents", 2493); 1927 | tree.insert("/:org/:repo/network/members", 2494); 1928 | tree.insert("/:org/:repo/stargazers", 2495); 1929 | tree.insert("/:org/:repo/stargazers/yoou_know", 2496); 1930 | tree.insert("/:org/:repo/watchers", 2497); 1931 | tree.insert("/:org/:repo/releases", 2498); 1932 | tree.insert("/:org/:repo/releases/tag/:id", 2499); 1933 | tree.insert("/:org/:repo/tags", 2500); 1934 | tree.insert("/:org/:repo/tags/:id", 2501); 1935 | tree.insert("/:org/:repo/tree/:id", 2502); 1936 | tree.insert("/:org/:repo/commit/:id", 2503); 1937 | 1938 | tree.insert("/new", 2504); 1939 | tree.insert("/new/import", 2505); 1940 | tree.insert("/organizations/new", 2506); 1941 | tree.insert("/organizations/plan", 2507); 1942 | 1943 | tree.insert("/:org/:repo/*", 3000); 1944 | tree.insert("/:org/:repo/releases/*", 3001); 1945 | let id = tree.insert("/:org/:repo/releases/download/:tag/:filename.:ext", 3002); 1946 | assert_eq!( 1947 | tree.url_for(id, &["viz-rs", "path-tree", "v0.5.0", "v0.5.0", "gz"]) 1948 | .unwrap(), 1949 | "/viz-rs/path-tree/releases/download/v0.5.0/v0.5.0.gz" 1950 | ); 1951 | 1952 | assert_eq!( 1953 | format!("{:?}", &tree.node), 1954 | r" 1955 | / •0 1956 | ├── 404 •67 1957 | ├── 50 1958 | │ ├── 0 •68 1959 | │ └── 3 •69 1960 | ├── a 1961 | │ ├── bout •2 1962 | │ │ └── / 1963 | │ │ ├── careers •37 1964 | │ │ ├── diversity •39 1965 | │ │ └── press •38 1966 | │ └── pi •1 1967 | ├── c 1968 | │ ├── ollections •22 1969 | │ └── ustomer-stories •17 1970 | ├── e 1971 | │ ├── nterprise •15 1972 | │ └── xplore •27 1973 | ├── features •6 1974 | │ └── / 1975 | │ ├── actions •7 1976 | │ ├── co 1977 | │ │ ├── de 1978 | │ │ │ ├── -review •12 1979 | │ │ │ └── spaces •10 1980 | │ │ └── pilot •11 1981 | │ ├── discussions •14 1982 | │ ├── issues •13 1983 | │ ├── packages •8 1984 | │ └── security •9 1985 | ├── issues •25 1986 | ├── login •3 1987 | ├── marketplace •26 1988 | ├── new •106 1989 | │ └── /import •107 1990 | ├── organizations/ 1991 | │ ├── new •108 1992 | │ └── plan •109 1993 | ├── p 1994 | │ ├── ricing •5 1995 | │ └── ulls •24 1996 | ├── readme •19 1997 | ├── s 1998 | │ ├── e 1999 | │ │ ├── arch •23 2000 | │ │ └── ttings •40 2001 | │ │ └── / 2002 | │ │ ├── a 2003 | │ │ │ ├── ccessibility •43 2004 | │ │ │ ├── dmin •41 2005 | │ │ │ └── pp 2006 | │ │ │ ├── earance •42 2007 | │ │ │ └── s •64 2008 | │ │ ├── b 2009 | │ │ │ ├── illing •45 2010 | │ │ │ │ └── /plans •46 2011 | │ │ │ └── locked_users •50 2012 | │ │ ├── co 2013 | │ │ │ ├── de 2014 | │ │ │ │ ├── _review_limits •52 2015 | │ │ │ │ └── spaces •54 2016 | │ │ │ └── pilot •56 2017 | │ │ ├── de 2018 | │ │ │ ├── leted_packages •55 2019 | │ │ │ └── velopers •65 2020 | │ │ ├── in 2021 | │ │ │ ├── stallations •60 2022 | │ │ │ └── teraction_limits •51 2023 | │ │ ├── keys •48 2024 | │ │ ├── notifications •44 2025 | │ │ ├── organizations •49 2026 | │ │ ├── pages •57 2027 | │ │ ├── re 2028 | │ │ │ ├── minders •61 2029 | │ │ │ └── p 2030 | │ │ │ ├── lies •58 2031 | │ │ │ └── ositories •53 2032 | │ │ ├── s 2033 | │ │ │ ├── ecurity •47 2034 | │ │ │ │ ├── -log •62 2035 | │ │ │ │ └── _analysis •59 2036 | │ │ │ └── ponsors-log •63 2037 | │ │ └── tokens •66 2038 | │ ├── ignup •4 2039 | │ └── ponsors •18 2040 | │ └── / 2041 | │ ├── accounts •29 2042 | │ ├── explore •28 2043 | │ └── : •30 2044 | │ └── / 2045 | │ ├── issues/ 2046 | │ │ └── ** •34 2047 | │ ├── : •33 2048 | │ ├── ?? •31 2049 | │ └── + •32 2050 | │ └── / 2051 | │ └── : •35 2052 | │ └── . 2053 | │ └── : •36 2054 | ├── t 2055 | │ ├── eam •16 2056 | │ ├── opics •20 2057 | │ └── rending •21 2058 | └── : •70 2059 | └── / 2060 | └── : •71 2061 | └── / 2062 | ├── actions •80 2063 | │ └── / 2064 | │ ├── runs/ 2065 | │ │ └── : •82 2066 | │ └── workflows/ 2067 | │ └── : •81 2068 | ├── com 2069 | │ ├── m 2070 | │ │ ├── it/ 2071 | │ │ │ └── : •105 2072 | │ │ └── unity •92 2073 | │ └── pare •77 2074 | ├── discussions •78 2075 | │ └── / 2076 | │ └── : •79 2077 | ├── graphs/co 2078 | │ ├── de-frequency •91 2079 | │ ├── mmit-activity •90 2080 | │ └── ntributors •89 2081 | ├── issues •72 2082 | │ └── / 2083 | │ ├── new •74 2084 | │ └── : •73 2085 | ├── network •93 2086 | │ └── / 2087 | │ ├── dependen 2088 | │ │ ├── cies •94 2089 | │ │ └── ts •95 2090 | │ └── members •96 2091 | ├── pul 2092 | │ ├── l 2093 | │ │ ├── s •75 2094 | │ │ └── / 2095 | │ │ └── : •76 2096 | │ └── se •88 2097 | ├── releases •100 2098 | │ └── / 2099 | │ ├── download/ 2100 | │ │ └── : 2101 | │ │ └── / 2102 | │ │ └── : 2103 | │ │ └── . 2104 | │ │ └── : •112 2105 | │ ├── tag/ 2106 | │ │ └── : •101 2107 | │ └── ** •111 2108 | ├── s 2109 | │ ├── ecurity •85 2110 | │ │ └── / 2111 | │ │ ├── advisories •87 2112 | │ │ └── policy •86 2113 | │ └── targazers •97 2114 | │ └── /yoou_know •98 2115 | ├── t 2116 | │ ├── ags •102 2117 | │ │ └── / 2118 | │ │ └── : •103 2119 | │ └── ree/ 2120 | │ └── : •104 2121 | ├── w 2122 | │ ├── atchers •99 2123 | │ └── iki •83 2124 | │ └── / 2125 | │ └── : •84 2126 | └── ** •110 2127 | " 2128 | ); 2129 | 2130 | let (h, p) = tree.find("/rust-lang/rust").unwrap(); 2131 | assert_eq!(h, &2400); 2132 | assert_eq!(p.params(), vec![("org", "rust-lang"), ("repo", "rust")]); 2133 | 2134 | let (h, p) = tree.find("/settings").unwrap(); 2135 | assert_eq!(h, &20); 2136 | assert!(p.params().is_empty()); 2137 | 2138 | let (h, p) = tree.find("/rust-lang/rust/actions/runs/1").unwrap(); 2139 | assert_eq!(h, &2442); 2140 | assert_eq!( 2141 | p.params(), 2142 | vec![("org", "rust-lang"), ("repo", "rust"), ("id", "1")] 2143 | ); 2144 | 2145 | let (h, p) = tree.find("/rust-lang/rust/").unwrap(); 2146 | assert_eq!(h, &3000); 2147 | assert_eq!( 2148 | p.params(), 2149 | vec![("org", "rust-lang"), ("repo", "rust"), ("*1", "")] 2150 | ); 2151 | 2152 | let (h, p) = tree.find("/rust-lang/rust/any").unwrap(); 2153 | assert_eq!(h, &3000); 2154 | assert_eq!( 2155 | p.params(), 2156 | vec![("org", "rust-lang"), ("repo", "rust"), ("*1", "any")] 2157 | ); 2158 | 2159 | let (h, p) = tree.find("/rust-lang/rust/releases/").unwrap(); 2160 | assert_eq!(h, &3001); 2161 | assert_eq!( 2162 | p.params(), 2163 | vec![("org", "rust-lang"), ("repo", "rust"), ("*1", "")] 2164 | ); 2165 | assert_eq!( 2166 | p.pieces, 2167 | &vec![ 2168 | Piece::String(b"/".to_vec()), 2169 | Piece::Parameter(Position::Named(b"org".to_vec()), Kind::Normal), 2170 | Piece::String(b"/".to_vec()), 2171 | Piece::Parameter(Position::Named(b"repo".to_vec()), Kind::Normal), 2172 | Piece::String(b"/releases/".to_vec()), 2173 | Piece::Parameter(Position::Index(1, b"*1".to_vec()), Kind::ZeroOrMoreSegment), 2174 | ] 2175 | ); 2176 | 2177 | let (h, p) = tree.find("/rust-lang/rust-analyzer/releases/download/2022-09-12/rust-analyzer-aarch64-apple-darwin.gz").unwrap(); 2178 | assert_eq!(h, &3002); 2179 | assert_eq!( 2180 | p.params(), 2181 | vec![ 2182 | ("org", "rust-lang"), 2183 | ("repo", "rust-analyzer"), 2184 | ("tag", "2022-09-12"), 2185 | ("filename", "rust-analyzer-aarch64-apple-darwin"), 2186 | ("ext", "gz") 2187 | ] 2188 | ); 2189 | assert_eq!( 2190 | p.pieces, 2191 | &vec![ 2192 | Piece::String(b"/".to_vec()), 2193 | Piece::Parameter(Position::Named(b"org".to_vec()), Kind::Normal), 2194 | Piece::String(b"/".to_vec()), 2195 | Piece::Parameter(Position::Named(b"repo".to_vec()), Kind::Normal), 2196 | Piece::String(b"/releases/download/".to_vec()), 2197 | Piece::Parameter(Position::Named(b"tag".to_vec()), Kind::Normal), 2198 | Piece::String(b"/".to_vec()), 2199 | Piece::Parameter(Position::Named(b"filename".to_vec()), Kind::Normal), 2200 | Piece::String(b".".to_vec()), 2201 | Piece::Parameter(Position::Named(b"ext".to_vec()), Kind::Normal), 2202 | ] 2203 | ); 2204 | assert_eq!( 2205 | p.pattern(), 2206 | "/:org/:repo/releases/download/:tag/:filename.:ext" 2207 | ); 2208 | assert_eq!( 2209 | tree.url_for(*p.id, &["viz-rs", "path-tree", "v0.5.0", "v0.5.0", "gz"]) 2210 | .unwrap(), 2211 | "/viz-rs/path-tree/releases/download/v0.5.0/v0.5.0.gz" 2212 | ); 2213 | } 2214 | 2215 | #[test] 2216 | fn cloneable() { 2217 | let tree = PathTree::::new(); 2218 | assert_eq!( 2219 | ::type_id(&tree), 2220 | ::type_id(&tree.clone()) 2221 | ); 2222 | } 2223 | 2224 | #[test] 2225 | fn test_dots_no_ext() { 2226 | let mut tree = PathTree::new(); 2227 | let _ = tree.insert("/:name", 1); 2228 | 2229 | let result = tree.find("/abc.xyz.123"); 2230 | assert!(result.is_some()); 2231 | 2232 | let (value, params) = result.unwrap(); 2233 | assert_eq!(value, &1); 2234 | 2235 | assert_eq!(params.params(), &[("name", "abc.xyz.123")]); 2236 | } 2237 | 2238 | #[test] 2239 | fn test_dots_ext() { 2240 | let mut tree = PathTree::new(); 2241 | let _ = tree.insert("/:name+.123", 2); 2242 | let _ = tree.insert("/:name*.123.456", 1); 2243 | 2244 | let result = tree.find("/abc.xyz.123"); 2245 | assert!(result.is_some()); 2246 | 2247 | let (value, params) = result.unwrap(); 2248 | assert_eq!(value, &2); 2249 | 2250 | assert_eq!(params.params(), &[("name", "abc.xyz")]); 2251 | 2252 | let result = tree.find("/abc.xyz.123.456"); 2253 | assert!(result.is_some()); 2254 | 2255 | let (value, params) = result.unwrap(); 2256 | assert_eq!(value, &1); 2257 | 2258 | assert_eq!(params.params(), &[("name", "abc.xyz")]); 2259 | } 2260 | 2261 | #[test] 2262 | fn test_dots_ext_no_qualifier() { 2263 | let mut tree = PathTree::new(); 2264 | let _ = tree.insert("/:name.js", 2); 2265 | let _ = tree.insert("/:name.js.gz", 1); 2266 | 2267 | assert_eq!( 2268 | format!("{:?}", &tree.node), 2269 | r" 2270 | / 2271 | └── : 2272 | └── .js •0 2273 | └── .gz •1 2274 | " 2275 | ); 2276 | 2277 | let result = tree.find("/node.js"); 2278 | assert!(result.is_some()); 2279 | 2280 | let (value, params) = result.unwrap(); 2281 | assert_eq!(value, &2); 2282 | 2283 | assert_eq!(params.params(), &[("name", "node")]); 2284 | 2285 | let result = tree.find("/path.lib.js"); 2286 | assert!(result.is_some()); 2287 | 2288 | let (value, params) = result.unwrap(); 2289 | assert_eq!(value, &2); 2290 | 2291 | assert_eq!(params.params(), &[("name", "path.lib")]); 2292 | 2293 | let result = tree.find("/node.js.js"); 2294 | assert!(result.is_some()); 2295 | 2296 | let (value, params) = result.unwrap(); 2297 | assert_eq!(value, &2); 2298 | 2299 | assert_eq!(params.params(), &[("name", "node.js")]); 2300 | 2301 | let result = tree.find("/node.js.gz"); 2302 | assert!(result.is_some()); 2303 | 2304 | let (value, params) = result.unwrap(); 2305 | assert_eq!(value, &1); 2306 | 2307 | assert_eq!(params.params(), &[("name", "node")]); 2308 | 2309 | let result = tree.find("/node.js.gz.js.gz"); 2310 | assert!(result.is_some()); 2311 | 2312 | let (value, params) = result.unwrap(); 2313 | assert_eq!(value, &1); 2314 | 2315 | assert_eq!(params.params(), &[("name", "node.js.gz")]); 2316 | } 2317 | --------------------------------------------------------------------------------