├── .busted ├── .github └── workflows │ ├── examples.yml │ ├── luacheck.yml │ └── test.yml ├── .gitignore ├── .luacheckrc ├── .luacov ├── LICENSE ├── Makefile ├── README.md ├── README.zh.md ├── benchmark ├── complex-variable.lua ├── github-apis.txt ├── github-routes.lua ├── simple-prefix.lua ├── simple-regex.lua ├── simple-variable-binding.lua ├── simple-variable.lua ├── static-paths.lua └── utils.lua ├── bin └── resty_busted ├── config.ld ├── docs ├── examples │ ├── custom-matcher.lua.html │ ├── example.lua.html │ └── regular-expression.lua.html ├── index.html └── ldoc.css ├── examples ├── custom-matcher.lua ├── example.lua └── regular-expression.lua ├── lua-radix-router.png ├── radix-router-dev-1.rockspec ├── rockspecs ├── radix-router-0.2.0-1.rockspec ├── radix-router-0.3.0-1.rockspec ├── radix-router-0.4.0-1.rockspec ├── radix-router-0.5.0-1.rockspec ├── radix-router-0.6.0-1.rockspec └── radix-router-0.6.1-1.rockspec ├── spec ├── parser_spec.lua ├── router_spec.lua └── utils_spec.lua └── src ├── constants.lua ├── iterator.lua ├── matcher ├── host.lua ├── matcher.lua └── method.lua ├── options.lua ├── parser ├── parser.lua └── style │ └── default.lua ├── route.lua ├── router.lua ├── trie.lua └── utils.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | verbose = true, 4 | output = "gtest", 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: examples 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | luaVersion: [ "5.2", "5.3", "5.4", "luajit-2.1", "luajit-openresty" ] 18 | 19 | steps: 20 | - name: checkout source code 21 | uses: actions/checkout@v3 22 | 23 | - name: install Lua/LuaJIT 24 | uses: leafo/gh-actions-lua@v11 25 | with: 26 | luaVersion: ${{ matrix.luaVersion }} 27 | 28 | - name: install LuaRocks 29 | uses: leafo/gh-actions-luarocks@v4 30 | 31 | - name: install dependencies 32 | run: | 33 | luarocks install radix-router 34 | 35 | - name: run examples 36 | run: | 37 | lua examples/example.lua 38 | lua examples/custom-matcher.lua 39 | lua examples/regular-expression.lua 40 | -------------------------------------------------------------------------------- /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: Luacheck 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**/*.md' 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - '**/*.md' 12 | 13 | jobs: 14 | luacheck: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Luacheck 20 | uses: lunarmodules/luacheck@v1 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**/*.md' 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - '**/*.md' 12 | 13 | jobs: 14 | lua: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | luaVersion: [ "5.2", "5.3", "5.4", "luajit-2.1", "luajit-openresty" ] 21 | 22 | steps: 23 | - name: checkout source code 24 | uses: actions/checkout@v3 25 | 26 | - name: install Lua/LuaJIT 27 | uses: leafo/gh-actions-lua@v11 28 | with: 29 | luaVersion: ${{ matrix.luaVersion }} 30 | 31 | - name: install LuaRocks 32 | uses: leafo/gh-actions-luarocks@v4 33 | 34 | - name: install dependencies 35 | run: | 36 | luarocks install busted 37 | luarocks install luacov-coveralls 38 | 39 | - name: build 40 | run: | 41 | make install 42 | 43 | - name: run tests 44 | run: | 45 | make test-coverage 46 | 47 | - name: samples 48 | run: | 49 | lua examples/example.lua 50 | lua examples/custom-matcher.lua 51 | lua examples/regular-expression.lua 52 | 53 | - name: report test coverage 54 | if: success() 55 | continue-on-error: true 56 | run: luacov-coveralls 57 | env: 58 | COVERALLS_REPO_TOKEN: ${{ github.token }} 59 | 60 | - name: benchmark 61 | run: | 62 | make bench CMD=lua 63 | 64 | openresty: 65 | runs-on: ubuntu-latest 66 | 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | openrestyVersion: [ "1.19.9.2", "1.21.4.4", "1.25.3.2", "1.27.1.1" ] 71 | 72 | steps: 73 | - name: checkout source code 74 | uses: actions/checkout@v3 75 | 76 | - name: install Lua/LuaJIT 77 | uses: leafo/gh-actions-lua@v11 78 | with: 79 | luaVersion: "luajit-openresty" 80 | 81 | - uses: leafo/gh-actions-openresty@v2 82 | with: 83 | openrestyVersion: ${{ matrix.openrestyVersion }} 84 | 85 | - name: install LuaRocks 86 | uses: leafo/gh-actions-luarocks@v4 87 | 88 | - name: install dependencies 89 | run: | 90 | luarocks install busted 91 | luarocks install luacov-coveralls 92 | 93 | - name: build 94 | run: | 95 | make install 96 | 97 | - name: run tests 98 | run: | 99 | bin/resty_busted --coverage spec/ 100 | 101 | - name: samples 102 | run: | 103 | resty examples/example.lua 104 | resty examples/custom-matcher.lua 105 | resty examples/regular-expression.lua 106 | 107 | - name: report test coverage 108 | if: success() 109 | continue-on-error: true 110 | run: luacov-coveralls 111 | env: 112 | COVERALLS_REPO_TOKEN: ${{ github.token }} 113 | 114 | - name: benchmark 115 | run: | 116 | make bench CMD=resty 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.out 3 | *.rock -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | unused_args = false 2 | max_line_length = false 3 | redefined = false 4 | 5 | globals = { 6 | "ngx", 7 | } 8 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | modules = { 2 | ["radix-router"] = "src/router.lua", 3 | ["radix-router.trie"] = "src/trie.lua", 4 | ["radix-router.iterator"] = "src/iterator.lua", 5 | ["radix-router.constants"] = "src/constants.lua", 6 | ["radix-router.route"] = "src/route.lua", 7 | ["radix-router.utils"] = "src/utils.lua", 8 | ["radix-router.parser"] = "src/parser/parser.lua", 9 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Yusheng Li 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install test test-coverage bench lint 2 | 3 | install: 4 | luarocks make 5 | 6 | test: 7 | busted spec/ 8 | 9 | test-coverage: 10 | busted --coverage spec/ 11 | 12 | lint: 13 | luacheck . 14 | 15 | CMD=luajit 16 | bench: 17 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/static-paths.lua 18 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable.lua 19 | RADIX_ROUTER_ROUTES=1000000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable.lua 20 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-prefix.lua 21 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/simple-regex.lua 22 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/complex-variable.lua 23 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable-binding.lua 24 | RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/github-routes.lua 25 | 26 | ldoc: 27 | @rm -rf docs/* 28 | @ldoc . 29 | 30 | 31 | FILE= 32 | API_KEY= 33 | upload: 34 | luarocks upload $(FILE) --api-key=$(API_KEY) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | special sponsor appwrite 3 |

4 | 5 | # Lua-Radix-Router [![release](https://img.shields.io/github/v/release/vm-001/lua-radix-router?color=green)](https://github.com/vm-001/lua-radix-router/releases) [![Build Status](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml/badge.svg)](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml) [![Build Status](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml/badge.svg)](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml) [![Coverage Status](https://coveralls.io/repos/github/vm-001/lua-radix-router/badge.svg)](https://coveralls.io/github/vm-001/lua-radix-router) ![Lua Versions](https://img.shields.io/badge/Lua-%205.2%20|%205.3%20|%205.4-blue.svg) 6 | 7 | English | [中文](README.zh.md) 8 | 9 | 10 | 11 | Lua-Radix-Router is a lightweight high-performance router library written in pure Lua. It's easy to use with only two exported functions, `Router.new()` and `router:match()`. 12 | 13 | The router is optimized for high performance. It combines HashTable(O(1)) and Compressed Trie(or Radix Tree, O(m) where m is the length of path being searched) for efficient matching. Some of the utility functions have the LuaJIT version for better performance, and will automatically switch when running in LuaJIT. It also scales well even with long paths and a large number of routes. 14 | 15 | The router can be run in different runtimes such as Lua, LuaJIT, or OpenResty. 16 | 17 | This library is considered production ready. 18 | 19 | ## 🔨 Features 20 | 21 | **Patterned path:** You can define named or unnamed patterns in path with pattern syntax "{}" and "{*}" 22 | 23 | - named variables: `/users/{id}/profile-{year}.{format}`, matches with /users/1/profile-2024.html. 24 | - named prefix: `/api/authn/{*path}`, matches with /api/authn/foo and /api/authn/foo/bar. 25 | 26 | **Variable binding:** Stop manually parsing the URL, let the router injects the binding variables for you. 27 | 28 | **Best performance:** The fastest router in Lua/LuaJIT and open-source API Gateways. See [Benchmarks](#-Benchmarks) and [Routing Benchmark](https://github.com/vm-001/gateways-routing-benchmark) in different API Gateways. 29 | 30 | **OpenAPI friendly:** OpenAPI(Swagger) is fully compatible. 31 | 32 | **Trailing slash match:** You can make the Router to ignore the trailing slash by setting `trailing_slash_match` to true. For example, /foo/ to match the existing /foo, /foo to match the existing /foo/. 33 | 34 | **Custom Matcher:** The router has two efficient matchers built in, MethodMatcher(`method`) and HostMatcher(`host`). They can be disabled via `opts.matcher_names`. You can also add your custom matchers via `opts.matchers`. For example, an IpMatcher to evaluate whether the `ctx.ip` is matched with the `ips` of a route. 35 | 36 | **Regex pattern:** You can define regex pattern in variables. a variable without regex pattern is treated as `[^/]+`. 37 | 38 | - `/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}` 39 | - `/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}` 40 | 41 | **Features in the roadmap**: 42 | 43 | - Expression condition: defines custom matching conditions by using expression language. 44 | 45 | ## 📖 Getting started 46 | 47 | Install radix-router via LuaRocks: 48 | 49 | ``` 50 | luarocks install radix-router 51 | ``` 52 | 53 | Or from source 54 | 55 | ``` 56 | make install 57 | ``` 58 | 59 | Get started by an example: 60 | 61 | ```lua 62 | local Router = require "radix-router" 63 | local router, err = Router.new({ 64 | { -- static path 65 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 66 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 67 | }, 68 | { -- variable path 69 | paths = { "/users/{id}/profile-{year}.{format}" }, 70 | handler = "2" 71 | }, 72 | { -- prefix path 73 | paths = { "/api/authn/{*path}" }, 74 | handler = "3" 75 | }, 76 | { -- methods condition 77 | paths = { "/users/{id}" }, 78 | methods = { "POST" }, 79 | handler = "4" 80 | } 81 | }) 82 | if not router then 83 | error("failed to create router: " .. err) 84 | end 85 | 86 | assert("1" == router:match("/html/index.html")) 87 | assert("2" == router:match("/users/100/profile-2023.pdf")) 88 | assert("3" == router:match("/api/authn/token/genreate")) 89 | assert("4" == router:match("/users/100", { method = "POST" })) 90 | 91 | -- variable binding 92 | local params = {} 93 | router:match("/users/100/profile-2023.pdf", nil, params) 94 | assert(params.year == "2023") 95 | assert(params.format == "pdf") 96 | ``` 97 | 98 | For more usage samples, please refer to the [/examples](/examples) directory. For more use cases, please check out [lua-radix-router-use-cases](https://github.com/vm-001/lua-radix-router-use-cases). 99 | 100 | ## 📄 Methods 101 | 102 | ### new 103 | 104 | Creates a radix router instance. 105 | 106 | ```lua 107 | local router, err = Router.new(routes, opts) 108 | ``` 109 | 110 | **Parameters** 111 | 112 | - **routes** (`table|nil`): the array-like Route table. 113 | 114 | - **opts** (`table|nil`): the object-like Options table. 115 | 116 | The available options are as follow 117 | 118 | | NAME | TYPE | DEFAULT | DESCRIPTION | 119 | | -------------------- | ------- | ----------------- | --------------------------------------------------- | 120 | | trailing_slash_match | boolean | false | whether to enable the trailing slash match behavior | 121 | | matcher_names | table | {"method","host"} | enabled built-in macher list | 122 | | matchers | table | { } | custom matcher list | 123 | 124 | 125 | 126 | Route defines the matching conditions for its handler. 127 | 128 | | PROPERTY | DESCRIPTION | 129 | |-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 130 | | `paths`
*required\** | A list of paths that match the Route.
| 131 | | `methods`
*optional* | A list of HTTP methods that match the Route.
| 132 | | `hosts`
*optional* | A list of hostnames that match the Route. Note that the value is case-sensitive. Wildcard hostnames are supported. For example, `*.foo.com` can match with `a.foo.com` or `a.b.foo.com`. | 133 | | `handler`
*required\** | The value of handler will be returned by `router:match()` when the route is matched. | 134 | | `priority`
*optional* | The priority of the route in case of radix tree node conflict. | 135 | 136 | 137 | 138 | ### match 139 | 140 | Return the handler of a matched route that matches the path and condition ctx. 141 | 142 | ```lua 143 | local handler = router:match(path, ctx, params, matched) 144 | ``` 145 | 146 | **Parameters** 147 | 148 | - **path**(`string`): the path to use for matching. 149 | - **ctx**(`table|nil`): the optional condition ctx to use for matching. 150 | - **params**(`table|nil`): the optional table to use for storing the parameters binding result. 151 | - **matched**(`table|nil`): the optional table to use for storing the matched conditions. 152 | 153 | ## 📝 Examples 154 | 155 | #### Regex pattern 156 | 157 | Using regex to define the pattern of a variable. Note that at most one URL segment is evaluated when matching a variable's pattern, which means it's not allowed to define a pattern crossing multiple URL segments, for example, `{var:[/0-9a-z]+}`. 158 | 159 | ```lua 160 | local Router = require "radix-router" 161 | local router = Router.new({ 162 | { 163 | paths = { "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}" }, 164 | handler = "1" 165 | }, 166 | { 167 | paths = { "/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" }, 168 | handler = "2" 169 | }, 170 | }) 171 | assert("1" == router:match("/users/100/profile-2024.pdf")) 172 | assert("2" == router:match("/users/00000000-0000-0000-0000-000000000000")) 173 | ``` 174 | 175 | ## 🧠 Data Structure and Implementation 176 | 177 | Inside the Router, it has a hash-like table to optimize the static path matching. Due to the LuaJIT optimization, static path matching is the fastest and has lower memory usage. (see [Benchmarks](#-Benchmarks)) 178 | 179 | The Router also has a tree structure for patterned path matching. The tree is basically a compact [prefix tree](https://en.wikipedia.org/wiki/Trie) (or [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree)). The primary structure of Router is as follows: 180 | 181 | ``` 182 | { 183 | static = {}, 184 | trie = TrieNode.new(), 185 | ... 186 | } 187 | 188 | +--------+----------+------------------------------------+ 189 | | FIELD | TYPE | DESC | 190 | +--------+----------+------------------------------------+ 191 | | static | table | a hash-like table for static paths | 192 | | trie | TrieNode | a radix tree for pattern paths | 193 | +--------+----------+------------------------------------+ 194 | ``` 195 | 196 | TrieNode is an array-like table. Compared with the hash-like, it reduces memory usage by 20%. The data structure of TrieNode is: 197 | 198 | ``` 199 | { , , , , } 200 | 201 | +-------+----------+------------------+ 202 | | INDEX | NAME | TYPE | 203 | +-------+----------+------------------+ 204 | | 1 | type | integer | 205 | | 2 | path | string | 206 | | 3 | pathn | integer | 207 | | 4 | children | hash-like table | 208 | | 5 | value | array-like table | 209 | +-------+----------+------------------+ 210 | ``` 211 | 212 | Nodes with a common prefix share a common parent. Here is an example of what a Router with three routes could look like: 213 | 214 | ```lua 215 | local router = Router.new({ 216 | { --
217 | paths = { "/api/login" }, 218 | handler = "1", 219 | }, { --
220 | paths = { "/people/{id}/profile" }, 221 | handler = "2", 222 | }, { --
223 | paths = { "/search/{query}", "/src/{*filename}" }, 224 | handler = "3" 225 | } 226 | }) 227 | ``` 228 | 229 | 230 | 231 | 232 | ``` 233 | router.static = { 234 | [/api/login] = { *
} 235 | } 236 | 237 | TrieNode.path TrieNode.value 238 | router.trie = / nil 239 | ├─people/ nil 240 | │ └─{wildcard} nil 241 | │ └─/profile { "/people/{id}/profile", *
} 242 | └─s nil 243 | ├─earch/ nil 244 | │ └─{wildcard} { "/search/{query}", *
} 245 | └─rc/ nil 246 | └─{catchall} { "/src/{*filename}", *
} 247 | ``` 248 | 249 | ## 🔍 Troubleshooting 250 | 251 | 252 | #### Could not find header file for PCRE2 253 | 254 | ``` 255 | Installing https://luarocks.org/lrexlib-pcre2-2.9.2-1.src.rock 256 | 257 | Error: Failed installing dependency: https://luarocks.org/lrexlib-pcre2-2.9.2-1.src.rock - Could not find header file for PCRE2 258 | ``` 259 | 260 | Try manually install `lrexlib-pcre2` (on macOS). 261 | 262 | ``` 263 | $ brew install pcre2 264 | $ ls /opt/homebrew/opt/pcre2/ 265 | $ luarocks install lrexlib-pcre2 PCRE2_DIR=/opt/homebrew/opt/pcre2 266 | ``` 267 | 268 | 269 | ## 🚀 Benchmarks 270 | 271 | #### Usage 272 | 273 | To run the benchmark 274 | 275 | ```$ make bench 276 | $ make install 277 | $ make bench 278 | ``` 279 | 280 | #### Environments 281 | 282 | - MacBook Pro(M4 Pro), 24GB 283 | - LuaJIT 2.1.1731601260 284 | 285 | #### Results 286 | 287 | | test case | route number | ns / op | OPS | RSS | 288 | |-------------------------|------------:|--------:|------------:|-----------:| 289 | | static path | 100000 | 8.70 | 114,984,822 | 48.88 MB | 290 | | simple variable | 100000 | 64.60 | 15,480,954 | 102.55 MB | 291 | | simple variable | 1000000 | 54.78 | 18,257,670 | 976.17 MB | 292 | | simple prefix | 100000 | 50.24 | 19,905,845 | 100.38 MB | 293 | | simple regex | 100000 | 96.72 | 10,338,909 | 111.66 MB | 294 | | complex variable | 100000 | 622.11 | 1,607,443 | 155.05 MB | 295 | | simple variable binding | 100000 | 123.16 | 8,119,565 | 100.36 MB | 296 | | github | 609 | 270.58 | 3,695,778 | 2.88 MB | 297 | 298 |
299 | Expand output 300 | 301 | ``` 302 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/static-paths.lua 303 | ========== static path ========== 304 | routes : 100000 305 | times : 10000000 306 | elapsed : 0.086968 s 307 | ns/op : 8.6968 ns 308 | OPS : 114984822 309 | path : /50000 310 | handler : 50000 311 | Memory : 48.88 MB 312 | 313 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua 314 | ========== variable ========== 315 | routes : 100000 316 | times : 10000000 317 | elapsed : 0.645955 s 318 | ns/op : 64.5955 ns 319 | OPS : 15480954 320 | path : /1/foo 321 | handler : 1 322 | Memory : 102.55 MB 323 | 324 | RADIX_ROUTER_ROUTES=1000000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua 325 | ========== variable ========== 326 | routes : 1000000 327 | times : 10000000 328 | elapsed : 0.547715 s 329 | ns/op : 54.7715 ns 330 | OPS : 18257670 331 | path : /1/foo 332 | handler : 1 333 | Memory : 976.17 MB 334 | 335 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-prefix.lua 336 | ========== prefix ========== 337 | routes : 100000 338 | times : 10000000 339 | elapsed : 0.502365 s 340 | ns/op : 50.2365 ns 341 | OPS : 19905845 342 | path : /1/a 343 | handler : 1 344 | Memory : 100.38 MB 345 | 346 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 luajit benchmark/simple-regex.lua 347 | ========== regex ========== 348 | routes : 100000 349 | times : 1000000 350 | elapsed : 0.096722 s 351 | ns/op : 96.722 ns 352 | OPS : 10338909 353 | path : /1/a 354 | handler : 1 355 | Memory : 111.66 MB 356 | 357 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 luajit benchmark/complex-variable.lua 358 | ========== variable ========== 359 | routes : 100000 360 | times : 1000000 361 | elapsed : 0.622106 s 362 | ns/op : 622.106 ns 363 | OPS : 1607443 364 | path : /aa/bb/cc/dd/ee/ff/gg/hh/ii/jj/kk/ll/mm/nn/oo/pp/qq/rr/ss/tt/uu/vv/ww/xx/yy/zz50000 365 | handler : 50000 366 | Memory : 155.05 MB 367 | 368 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable-binding.lua 369 | ========== variable ========== 370 | routes : 100000 371 | times : 10000000 372 | elapsed : 1.231593 s 373 | ns/op : 123.1593 ns 374 | OPS : 8119565 375 | path : /1/foo 376 | handler : 1 377 | params : name = foo 378 | Memory : 100.36 MB 379 | 380 | RADIX_ROUTER_TIMES=1000000 luajit benchmark/github-routes.lua 381 | ========== github apis ========== 382 | routes : 609 383 | times : 1000000 384 | elapsed : 0.270579 s 385 | ns/op : 270.579 ns 386 | OPS : 3695778 387 | path : /repos/vm-001/lua-radix-router/import 388 | handler : /repos/{owner}/{repo}/import 389 | Memory : 2.88 MB 390 | 391 | ``` 392 | 393 |
394 | 395 | 396 | ## License 397 | 398 | BSD 2-Clause License 399 | 400 | Copyright (c) 2024, Yusheng Li 401 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Lua-Radix-Router [![Build Status](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml/badge.svg)](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml) [![Build Status](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml/badge.svg)](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml) [![Coverage Status](https://coveralls.io/repos/github/vm-001/lua-radix-router/badge.svg)](https://coveralls.io/github/vm-001/lua-radix-router) ![Lua Versions](https://img.shields.io/badge/Lua-%205.2%20|%205.3%20|%205.4-blue.svg) 2 | 3 | [English](README.md) | 中文 (Translated by ChatGPT) 4 | 5 | --- 6 | 7 | Lua-Radix-Router 是一个轻量级高性能的路由器,用纯 Lua 编写。该路由器易于使用,只有两个方法,Router.new() 和 Router:match()。它可以集成到不同的运行时环境,如 Lua 应用程序、LuaJIT 或 OpenResty 中。 8 | 9 | 该路由器专为高性能而设计。采用了压缩动态 Trie(基数树)以实现高效匹配。即使有数百万个包含复杂路径的路由,匹配仍可在1纳秒内完成。 10 | 11 | ## 🔨 特性 12 | 13 | - 变量路径:语法 `{varname}`。 14 | - `/users/{id}/profile-{year}.{format}`:允许在一个路径段中有多个变量 15 | 16 | - 前缀匹配:语法 `{*varname}` 17 | - `/api/authn/{*path}` 18 | 19 | - 变量绑定:路由器在匹配过程中会自动为您注入绑定结果。 20 | 21 | - 最佳性能:Lua/LuaJIT 中最快的路由器。请参阅[性能基准](#-基准测试)。 22 | 23 | - OpenAPI 友好:完全支持 OpenAPI。 24 | 25 | 26 | 27 | **在路线图中的特性**:(start或创建issue来加速优先级) 28 | 29 | - 尾部斜杠匹配:使 URL /foo/ 能够与 /foo 路径匹配。 30 | 31 | - 表达式条件:通过使用表达式语言定义自定义匹配条件。 32 | 33 | - 变量中的正则表达式 34 | 35 | 36 | ## 📖 入门 37 | 38 | 通过 LuaRocks 安装 radix-router: 39 | 40 | ``` 41 | luarocks install radix-router 42 | ``` 43 | 44 | 或者从源码安装 45 | 46 | ``` 47 | make install 48 | ``` 49 | 50 | 通过示例开始: 51 | 52 | ```lua 53 | local Router = require "radix-router" 54 | local router, err = Router.new({ 55 | { -- 静态路径 56 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 57 | handler = "1" -- 处理程序可以是任何非空值(例如布尔值、表、函数) 58 | }, 59 | { -- 变量路径 60 | paths = { "/users/{id}/profile-{year}.{format}" }, 61 | handler = "2" 62 | }, 63 | { -- 前缀路径 64 | paths = { "/api/authn/{*path}" }, 65 | handler = "3" 66 | }, 67 | { -- 方法条件 68 | paths = { "/users/{id}" }, 69 | methods = { "POST" }, 70 | handler = "4" 71 | } 72 | }) 73 | if not router then 74 | error("failed to create router: " .. err) 75 | end 76 | 77 | assert("1" == router:match("/html/index.html")) 78 | assert("2" == router:match("/users/100/profile-2023.pdf")) 79 | assert("3" == router:match("/api/authn/token/genreate")) 80 | assert("4" == router:match("/users/100", { method = "POST" })) 81 | 82 | -- 变量绑定 83 | local params = {} 84 | router:match("/users/100/profile-2023.pdf", nil, params) 85 | assert(params.year == "2023") 86 | assert(params.format == "pdf") 87 | ``` 88 | 89 | 有关更多用法示例,请参阅 `/samples` 目录。 90 | 91 | ## 📄 方法 92 | 93 | 94 | ### new 95 | 96 | 创建一个 radix 路由器实例。 97 | 98 | ```lua 99 | local router, err = Router.new(routes) 100 | ``` 101 | 102 | **参数** 103 | 104 | - **routes**(`table|nil`): the array-like Route table. 105 | 106 | 107 | 108 | 路由定义了其处理程序的匹配条件: 109 | 110 | | 属性 | 描述 | 111 | | ----------------------------- |------------------------------------| 112 | | `paths` *required\** | 匹配条件的路径列表。 | 113 | | `methods` *optional* | 匹配条件的方法列表。 | 114 | | `handler` *required\** | 当路由匹配时,`router:match()` 将返回处理程序的值。 | 115 | | `priority` *optional* | 在 radix 树节点冲突的情况下,路由的优先级。 | 116 | | `expression` *optional* (TDB) | `expression` 使用表达式语言定义的匹配条件 | 117 | 118 | 119 | 120 | ### match 121 | 122 | 返回匹配路径和条件 ctx 的匹配路由的处理程序。 123 | 124 | ```lua 125 | local handler = router:match(path, ctx, params) 126 | ``` 127 | 128 | **参数** 129 | 130 | - **path**(`string`): 用于匹配的路径。 131 | - **ctx**(`table|nil`): 用于匹配的可选条件 ctx。 132 | - **params**(`table|nil`): 用于存储参数绑定结果的可选表。 133 | 134 | ## 🚀 基准测试 135 | 136 | #### 用法 137 | 138 | ``` 139 | $ make install 140 | $ make bench 141 | ``` 142 | 143 | #### 环境 144 | 145 | - Apple MacBook Pro(M1 Pro), 32GB 146 | - LuaJIT 2.1.1700008891 147 | 148 | ``` 149 | $ make bench 150 | ``` 151 | 152 | #### 数据 153 | 154 | | TEST CASE | Router number | nanoseconds / op | QPS | RSS | 155 | | ----------------------- | ------------- |------------------|------------|--------------| 156 | | static path | 100000 | 0.0129826 | 77,026,173 | 65.25 MB | 157 | | simple variable | 100000 | 0.0802077 | 12,467,630 | 147.52 MB | 158 | | simple variable | 1000000 | 0.084604 | 11,819,772 | 1381.47 MB | 159 | | simple prefix | 100000 | 0.0713651 | 14,012,451 | 147.47 MB | 160 | | complex variable | 100000 | 0.914117 | 1,093,951 | 180.30 MB | 161 | | simple variable binding | 100000 | 0.21054 | 4,749,691 | 147.28 MB | 162 | | github | 609 | 0.375829 | 2,660,784 | 2.72 MB | 163 | 164 |
165 | 展开输出 166 | 167 | ``` 168 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/static-paths.lua 169 | ========== static path ========== 170 | routes : 100000 171 | times : 10000000 172 | elapsed : 0.129826 s 173 | QPS : 77026173 174 | ns/op : 0.0129826 ns 175 | path : /50000 176 | handler : 50000 177 | Memory : 65.25 MB 178 | 179 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua 180 | ========== variable ========== 181 | routes : 100000 182 | times : 10000000 183 | elapsed : 0.802077 s 184 | QPS : 12467630 185 | ns/op : 0.0802077 ns 186 | path : /1/foo 187 | handler : 1 188 | Memory : 147.52 MB 189 | 190 | RADIX_ROUTER_ROUTES=1000000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua 191 | ========== variable ========== 192 | routes : 1000000 193 | times : 10000000 194 | elapsed : 0.84604 s 195 | QPS : 11819772 196 | ns/op : 0.084604 ns 197 | path : /1/foo 198 | handler : 1 199 | Memory : 1381.47 MB 200 | 201 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-prefix.lua 202 | ========== prefix ========== 203 | routes : 100000 204 | times : 10000000 205 | elapsed : 0.713651 s 206 | QPS : 14012451 207 | ns/op : 0.0713651 ns 208 | path : /1/a 209 | handler : 1 210 | Memory : 147.47 MB 211 | 212 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 luajit benchmark/complex-variable.lua 213 | ========== variable ========== 214 | routes : 100000 215 | times : 1000000 216 | elapsed : 0.914117 s 217 | QPS : 1093951 218 | ns/op : 0.914117 ns 219 | path : /aa/bb/cc/dd/ee/ff/gg/hh/ii/jj/kk/ll/mm/nn/oo/pp/qq/rr/ss/tt/uu/vv/ww/xx/yy/zz50000 220 | handler : 50000 221 | Memory : 180.30 MB 222 | 223 | RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable-binding.lua 224 | ========== variable ========== 225 | routes : 100000 226 | times : 10000000 227 | elapsed : 2.1054 s 228 | QPS : 4749691 229 | ns/op : 0.21054 ns 230 | path : /1/foo 231 | handler : 1 232 | params : name = foo 233 | Memory : 147.28 MB 234 | 235 | RADIX_ROUTER_TIMES=1000000 luajit benchmark/github-routes.lua 236 | ========== github apis ========== 237 | routes : 609 238 | times : 1000000 239 | elapsed : 0.375829 s 240 | QPS : 2660784 241 | ns/op : 0.375829 ns 242 | path : /repos/vm-001/lua-radix-router/import 243 | handler : /repos/{owner}/{repo}/import 244 | Memory : 2.72 MB 245 | ``` 246 | 247 |
248 | 249 | 250 | -------------------------------------------------------------------------------- /benchmark/complex-variable.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { 12 | paths = { 13 | string.format("/aa/{bb}/cc/{dd}/ee/{ff}/gg/{hh}/ii/{jj}/kk/{ll}/mm/{nn}/oo/{pp}/qq/{rr}/ss/{tt}/uu/{vv}/ww/{xx}/yy/zz%d", i) 14 | }, handler = i } 15 | end 16 | router = Router.new(routes) 17 | end 18 | 19 | local rss_mb = utils.get_rss() 20 | 21 | local path = "/aa/bb/cc/dd/ee/ff/gg/hh/ii/jj/kk/ll/mm/nn/oo/pp/qq/rr/ss/tt/uu/vv/ww/xx/yy/zz" .. route_n / 2 22 | 23 | local elapsed = utils.timing(function() 24 | for _ = 1, times do 25 | router:match(path) 26 | end 27 | end) 28 | 29 | utils.print_result({ 30 | title = "variable", 31 | routes = route_n, 32 | times = times, 33 | elapsed = elapsed, 34 | benchmark_path = path, 35 | benchmark_handler = router:match(path), 36 | rss = rss_mb, 37 | }) 38 | -------------------------------------------------------------------------------- /benchmark/github-apis.txt: -------------------------------------------------------------------------------- 1 | /repos/{owner}/{repo}/check-runs/{check_run_id} 2 | /repos/{owner}/{repo}/actions/caches/{cache_id} 3 | /repositories/{repository_id}/environments/{environment_name}/secrets/public-key 4 | /repos/{owner}/{repo}/collaborators/{username} 5 | /repos/{owner}/{repo}/branches/{branch}/protection 6 | /repos/{owner}/{repo}/actions/jobs/{job_id} 7 | /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name} 8 | /orgs/{org}/organization-roles/{role_id}/teams 9 | /orgs/{org}/teams/{team_slug}/projects 10 | /user/migrations/{migration_id}/archive 11 | /repos/{owner}/{repo}/git/tags/{tag_sha} 12 | /repos/{owner}/{repo}/actions/jobs/{job_id}/logs 13 | /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations 14 | /repos/{owner}/{repo}/codespaces/devcontainers 15 | /orgs/{org}/teams/{team_slug}/projects/{project_id} 16 | /orgs/{org}/organization-roles/{role_id}/users 17 | /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun 18 | /user/migrations/{migration_id}/repos/{repo_name}/lock 19 | /repos/{owner}/{repo}/check-runs/{check_run_id}/rerequest 20 | /repos/{owner}/{repo}/codespaces/machines 21 | /repos/{owner}/{repo}/collaborators/{username}/permission 22 | /orgs/{org}/outside_collaborators 23 | /repos/{owner}/{repo}/actions/oidc/customization/sub 24 | /repositories/{repository_id}/environments/{environment_name}/variables/{name} 25 | /user/orgs 26 | /orgs/{org}/outside_collaborators/{username} 27 | /repos/{owner}/{repo}/comments 28 | /repos/{owner}/{repo}/codespaces/new 29 | /user/packages 30 | /repos/{owner}/{repo}/commits 31 | /orgs/{org}/teams/{team_slug}/repos 32 | /repos/{owner}/{repo}/check-suites/preferences 33 | /repos/{owner}/{repo}/comments/{comment_id} 34 | /gists/{gist_id} 35 | /orgs/{org}/teams/{team_slug}/repos/{owner}/{repo} 36 | /repos/{owner}/{repo}/actions/organization-secrets 37 | /user/packages/{package_type}/{package_name}/restore 38 | /repos/{owner}/{repo}/commits/{commit_sha}/branches-where-head 39 | /repos/{owner}/{repo}/actions/organization-variables 40 | /orgs/{org}/rulesets/{ruleset_id} 41 | /repos/{owner}/{repo}/hooks 42 | /user/packages/{package_type}/{package_name}/versions 43 | /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions 44 | /repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs 45 | /repos/{owner}/{repo}/actions/permissions 46 | /user/packages/{package_type}/{package_name}/versions/{package_version_id} 47 | /orgs/{org}/teams/{team_slug}/teams 48 | /orgs/{org}/packages/{package_type}/{package_name} 49 | /orgs/{org}/secret-scanning/alerts 50 | /repos/{owner}/{repo}/hooks/{hook_id} 51 | /repos/{owner}/{repo}/actions/permissions/access 52 | /repos/{owner}/{repo}/code-scanning/alerts 53 | /gists/{gist_id}/comments 54 | /orgs/{org}/security-advisories 55 | /repos/{owner}/{repo}/commits/{commit_sha}/pulls 56 | /orgs/{org}/packages/{package_type}/{package_name}/restore 57 | /repos/{owner}/{repo}/issues/events/{event_id} 58 | /orgs/{org}/actions/cache/usage-by-repository 59 | /repos/{owner}/{repo}/actions/permissions/selected-actions 60 | /orgs/{org}/packages/{package_type}/{package_name}/versions 61 | /user/public_emails 62 | /orgs/{org}/security-managers 63 | /orgs/{org}/actions/oidc/customization/sub 64 | /app/installations 65 | /repos/{owner}/{repo}/actions/permissions/workflow 66 | /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id} 67 | /orgs/{org}/security-managers/teams/{team_slug} 68 | /repos/{owner}/{repo}/commits/{ref}/check-suites 69 | /repos/{owner}/{repo}/actions/runners 70 | /repos/{owner}/{repo}/commits/{ref}/status 71 | /orgs/{org}/settings/billing/actions 72 | /gists/{gist_id}/commits 73 | /repos/{owner}/{repo}/issues/{issue_number}/assignees 74 | /repos/{owner}/{repo}/actions/runners/downloads 75 | /projects/columns/cards/{card_id}/moves 76 | /orgs/{org}/settings/billing/packages 77 | /gists/{gist_id}/forks 78 | /app/installations/{installation_id}/access_tokens 79 | /repos/{owner}/{repo}/actions/runners/registration-token 80 | /projects/columns/{column_id}/moves 81 | /orgs/{org}/actions/permissions/repositories 82 | /repos/{owner}/{repo}/interaction-limits 83 | /gists/{gist_id}/star 84 | /repos/{owner}/{repo}/actions/runners/remove-token 85 | /repos/{owner}/{repo}/code-scanning/analyses/{analysis_id} 86 | /projects/{project_id} 87 | /repos/{owner}/{repo}/actions/runners/{runner_id} 88 | /orgs/{org}/actions/permissions/repositories/{repository_id} 89 | /gists/{gist_id}/{sha} 90 | /app/installations/{installation_id}/suspended 91 | /repos/{owner}/{repo}/actions/runners/{runner_id}/labels 92 | /repos/{owner}/{repo}/contents/{path} 93 | /gitignore/templates 94 | /repos/{owner}/{repo}/code-scanning/codeql/databases 95 | /orgs/{org}/actions/permissions/selected-actions 96 | /marketplace_listing/plans/{plan_id}/accounts 97 | /applications/{client_id}/grant 98 | /repos/{owner}/{repo}/code-scanning/codeql/databases/{language} 99 | /gitignore/templates/{name} 100 | /projects/{project_id}/collaborators 101 | /repos/{owner}/{repo}/branches/{branch}/protection/enforce_admins 102 | /orgs/{org}/actions/permissions/workflow 103 | /marketplace_listing/stubbed/accounts/{account_id} 104 | /installation/repositories 105 | /marketplace_listing/stubbed/plans 106 | /projects/{project_id}/collaborators/{username} 107 | /marketplace_listing/stubbed/plans/{plan_id}/accounts 108 | /user/ssh_signing_keys 109 | /repos/{owner}/{repo}/branches/{branch}/protection/required_pull_request_reviews 110 | /orgs/{org}/actions/runners 111 | /installation/token 112 | /applications/{client_id}/token/scoped 113 | /projects/{project_id}/collaborators/{username}/permission 114 | /issues 115 | /networks/{owner}/{repo}/events 116 | /orgs/{org}/actions/runners/downloads 117 | /projects/{project_id}/columns 118 | /repos/{owner}/{repo}/branches/{branch}/protection/required_signatures 119 | /notifications 120 | /apps/{app_slug} 121 | /user/starred 122 | /assignments/{assignment_id} 123 | /licenses 124 | /rate_limit 125 | /repos/{owner}/{repo}/contributors 126 | /assignments/{assignment_id}/accepted_assignments 127 | /licenses/{license} 128 | /orgs/{org}/actions/runners/registration-token 129 | /assignments/{assignment_id}/grades 130 | /markdown 131 | /repos/{owner}/{repo}/dependabot/alerts 132 | /orgs/{org}/actions/runners/remove-token 133 | /user/subscriptions 134 | /classrooms 135 | /orgs/{org}/actions/runners/{runner_id} 136 | /repos/{owner}/{repo}/dependabot/alerts/{alert_number} 137 | /classrooms/{classroom_id} 138 | /users 139 | /notifications/threads/{thread_id}/subscription 140 | /orgs/{org}/actions/runners/{runner_id}/labels 141 | /repos/{owner}/{repo}/branches/{branch}/protection/restrictions 142 | /users/{username}/docker/conflicts 143 | /repos/{owner}/{repo}/projects 144 | /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/apps 145 | /users/{username}/events 146 | /users/{username}/events/orgs/{org} 147 | /repos/{owner}/{repo}/properties/values 148 | /users/{username}/events/public 149 | /repos/{owner}/{repo}/pulls 150 | /users/{username}/followers 151 | /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/teams 152 | /users/{username}/following 153 | /users/{username}/following/{target_user} 154 | /orgs/{org}/personal-access-tokens/{pat_id} 155 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number} 156 | /users/{username}/gists 157 | /orgs/{org}/personal-access-tokens/{pat_id}/repositories 158 | /users/{username}/gpg_keys 159 | /orgs/{org}/projects 160 | /users/{username}/hovercard 161 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments 162 | /repos/{owner}/{repo}/pulls/comments 163 | /repos/{owner}/{repo}/pulls/comments/{comment_id} 164 | /orgs/{org}/properties/schema 165 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number} 166 | /teams/{team_id}/projects 167 | /repos/{owner}/{repo}/check-runs 168 | /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions 169 | /teams/{team_id}/projects/{project_id} 170 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number}/reactions 171 | /zen 172 | /orgs/{org}/properties/schema/{custom_property_name} 173 | /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions/{reaction_id} 174 | /teams/{team_id}/repos 175 | /repos/{owner}/{repo}/pulls/{pull_number} 176 | /teams/{team_id}/repos/{owner}/{repo} 177 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number}/reactions/{reaction_id} 178 | /orgs/{org}/properties/values 179 | /repos/{owner}/{repo}/pulls/{pull_number}/codespaces 180 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions 181 | /repos/{owner}/{repo}/pulls/{pull_number}/comments 182 | /teams/{team_id}/teams 183 | /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions/{reaction_id} 184 | /user 185 | /orgs/{org}/public_members 186 | /orgs/{org}/teams/{team_slug}/invitations 187 | /orgs/{org}/public_members/{username} 188 | /orgs/{org}/teams/{team_slug}/members 189 | /orgs/{org}/teams/{team_slug}/memberships/{username} 190 | /orgs/{org}/codespaces/secrets/{secret_name}/repositories 191 | /repos/{owner}/{repo}/actions/artifacts 192 | /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies 193 | /orgs/{org}/codespaces/secrets/{secret_name}/repositories/{repository_id} 194 | /repos/{owner}/{repo}/actions/artifacts/{artifact_id} 195 | /orgs/{org}/copilot/billing 196 | /orgs/{org}/copilot/billing/seats 197 | /repos/{owner}/{repo}/actions/runners/{runner_id}/labels/{name} 198 | /repos/{owner}/{repo}/import/authors 199 | /orgs/{org}/copilot/billing/selected_teams 200 | /repos/{owner}/{repo}/actions/runs 201 | /repos/{owner}/{repo}/import/authors/{author_id} 202 | /repos/{owner}/{repo}/import/large_files 203 | /repos/{owner}/{repo}/actions/runs/{run_id} 204 | /orgs/{org}/copilot/billing/selected_users 205 | /repos/{owner}/{repo}/import/lfs 206 | /repos/{owner}/{repo}/actions/runs/{run_id}/approvals 207 | /repos/{owner}/{repo}/installation 208 | /orgs/{org}/dependabot/alerts 209 | /repos/{owner}/{repo}/actions/runs/{run_id}/approve 210 | /user/blocks 211 | /orgs/{org}/dependabot/secrets 212 | /user/blocks/{username} 213 | /orgs/{org}/dependabot/secrets/public-key 214 | /orgs/{org}/settings/billing/shared-storage 215 | /orgs/{org}/dependabot/secrets/{secret_name} 216 | /orgs/{org}/teams 217 | /users/{username}/installation 218 | /users/{username}/keys 219 | /orgs/{org}/dependabot/secrets/{secret_name}/repositories 220 | /user/codespaces/secrets 221 | /users/{username}/orgs 222 | /orgs/{org}/dependabot/secrets/{secret_name}/repositories/{repository_id} 223 | /user/codespaces/secrets/public-key 224 | /users/{username}/packages 225 | /orgs/{org}/personal-access-token-requests/{pat_request_id}/repositories 226 | /orgs/{org}/docker/conflicts 227 | /orgs/{org}/personal-access-tokens 228 | /user/following/{username} 229 | /orgs/{org}/teams/{team_slug} 230 | /orgs/{org}/events 231 | /users/{username}/packages/{package_type}/{package_name}/restore 232 | /orgs/{org}/codespaces/access/selected_users 233 | /orgs/{org}/failed_invitations 234 | /users/{username}/packages/{package_type}/{package_name}/versions 235 | /user/gpg_keys 236 | /users/{username}/packages/{package_type}/{package_name}/versions/{package_version_id} 237 | /orgs/{org}/hooks 238 | /orgs/{org}/codespaces/secrets 239 | /orgs/{org}/actions/runners/{runner_id}/labels/{name} 240 | /app/hook/deliveries/{delivery_id} 241 | /orgs/{org}/codespaces/secrets/public-key 242 | /orgs/{org}/hooks/{hook_id}/deliveries 243 | /repos/{owner}/{repo}/security-advisories/reports 244 | /app/hook/deliveries/{delivery_id}/attempts 245 | /user/gpg_keys/{gpg_key_id} 246 | /orgs/{org}/codespaces/secrets/{secret_name} 247 | /app/installation-requests 248 | /repos/{owner}/{repo}/security-advisories/{ghsa_id} 249 | /repos/{owner}/{repo}/merges 250 | /user/installations 251 | /orgs/{org}/hooks/{hook_id}/pings 252 | /orgs/{org}/hooks/{hook_id} 253 | /users/{username}/received_events/public 254 | /orgs/{org}/installation 255 | /user/installations/{installation_id}/repositories 256 | /users/{username}/repos 257 | /orgs/{org}/installations 258 | /repos/{owner}/{repo}/dependabot/secrets/public-key 259 | /repos/{owner}/{repo}/milestones 260 | /repos/{owner}/{repo}/environments/{environment_name} 261 | /orgs/{org}/interaction-limits 262 | /users/{username}/settings/billing/packages 263 | /repos/{owner}/{repo}/pulls/{pull_number}/commits 264 | /users/{username}/settings/billing/shared-storage 265 | /repos/{owner}/{repo}/pulls/{pull_number}/files 266 | /repos/{owner}/{repo}/security-advisories/{ghsa_id}/cve 267 | /users/{username}/social_accounts 268 | /repos/{owner}/{repo}/dependency-graph/compare/{basehead} 269 | /repos/{owner}/{repo}/security-advisories/{ghsa_id}/forks 270 | /repos/{owner}/{repo}/pulls/{pull_number}/merge 271 | /repos/{owner}/{repo}/stargazers 272 | /orgs/{org}/invitations 273 | /repos/{owner}/{repo}/milestones/{milestone_number} 274 | /users/{username}/subscriptions 275 | /repos/{owner}/{repo}/stats/code_frequency 276 | /repos/{owner}/{repo}/dependency-graph/snapshots 277 | /repos/{owner}/{repo}/milestones/{milestone_number}/labels 278 | /repos/{owner}/{repo}/stats/commit_activity 279 | /repos/{owner}/{repo}/notifications 280 | /repos/{owner}/{repo}/stats/contributors 281 | /repos/{owner}/{repo}/deployments 282 | /repos/{owner}/{repo}/code-scanning/default-setup 283 | /orgs/{org}/invitations/{invitation_id} 284 | /repos/{owner}/{repo}/pages 285 | /orgs/{org}/invitations/{invitation_id}/teams 286 | /repos/{owner}/{repo}/stats/punch_card 287 | /orgs/{org}/issues 288 | /repos/{owner}/{repo}/code-scanning/sarifs 289 | /repos/{owner}/{repo}/statuses/{sha} 290 | / 291 | /repos/{owner}/{repo}/pulls/{pull_number}/reviews 292 | /orgs/{org}/members/{username} 293 | /user/codespaces/{codespace_name} 294 | /repos/{owner}/{repo}/subscribers 295 | /orgs/{org}/actions/secrets 296 | /repos/{owner}/{repo}/subscription 297 | /orgs/{org}/actions/secrets/public-key 298 | /repos/{owner}/{repo}/pages/builds 299 | /orgs/{org}/members/{username}/codespaces/{codespace_name} 300 | /advisories 301 | /orgs/{org}/actions/secrets/{secret_name} 302 | /repos/{owner}/{repo}/codeowners/errors 303 | /repos/{owner}/{repo}/deployments/{deployment_id}/statuses 304 | /orgs/{org}/personal-access-token-requests 305 | /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id} 306 | /repos/{owner}/{repo}/pages/builds/latest 307 | /orgs/{org}/members/{username}/copilot 308 | /repos/{owner}/{repo}/tags 309 | /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts 310 | /octocat 311 | /user/codespaces/{codespace_name}/publish 312 | /repos/{owner}/{repo}/tags/protection 313 | /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number} 314 | /repos/{owner}/{repo}/pages/deployment 315 | /organizations 316 | /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs 317 | /orgs/{org}/actions/secrets/{secret_name}/repositories 318 | /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals 319 | /repos/{owner}/{repo}/tags/protection/{tag_protection_id} 320 | /orgs/{org}/personal-access-token-requests/{pat_request_id} 321 | /user/codespaces/{codespace_name}/stop 322 | /repos/{owner}/{repo}/tarball/{ref} 323 | /orgs/{org}/actions/secrets/{secret_name}/repositories/{repository_id} 324 | /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events 325 | /repos/{owner}/{repo}/pages/health 326 | /repos/{owner}/{repo}/teams 327 | /orgs/{org}/teams/{team_slug}/discussions 328 | /repos/{owner}/{repo}/topics 329 | /repos/{owner}/{repo}/environments 330 | /repos/{owner}/{repo}/private-vulnerability-reporting 331 | /orgs/{org}/actions/variables 332 | /user/emails 333 | /repos/{owner}/{repo}/readme 334 | /repos/{owner}/{repo}/releases/generate-notes 335 | /repos/{owner}/{repo}/issues/{issue_number}/comments 336 | /advisories/{ghsa_id} 337 | /repos/{owner}/{repo}/releases 338 | /orgs/{org}/actions/variables/{name} 339 | /repos/{owner}/{repo}/issues/{issue_number}/events 340 | /user/followers 341 | /app 342 | /repos/{owner}/{repo}/releases/latest 343 | /repos/{owner}/{repo}/issues/{issue_number}/labels 344 | /repos/{owner}/{repo}/releases/tags/{tag} 345 | /orgs/{org}/actions/variables/{name}/repositories 346 | /repos/{owner}/{repo}/releases/{release_id} 347 | /orgs/{org}/actions/variables/{name}/repositories/{repository_id} 348 | /repos/{owner}/{repo}/releases/assets/{asset_id} 349 | /repos/{owner}/{repo}/issues/{issue_number}/labels/{name} 350 | /orgs/{org}/blocks 351 | /versions 352 | /repos/{owner}/{repo}/issues/{issue_number}/lock 353 | /users/{username}/starred 354 | /users/{username}/ssh_signing_keys 355 | /users/{username}/settings/billing/actions 356 | /users/{username}/received_events 357 | /users/{username}/projects 358 | /orgs/{org}/blocks/{username} 359 | /users/{username}/packages/{package_type}/{package_name}/versions/{package_version_id}/restore 360 | /orgs/{org}/actions/cache/usage 361 | /users/{username}/packages/{package_type}/{package_name} 362 | /users/{username} 363 | /user/teams 364 | /user/starred/{owner}/{repo} 365 | /user/ssh_signing_keys/{ssh_signing_key_id} 366 | /user/social_accounts 367 | /repos/{owner}/{repo}/releases/{release_id}/assets 368 | /repos/{owner}/{repo}/issues/{issue_number}/reactions 369 | /user/repository_invitations/{invitation_id} 370 | /app/hook/deliveries 371 | /user/repository_invitations 372 | /user/repos 373 | /user/projects 374 | /orgs/{org}/code-scanning/alerts 375 | /user/packages/{package_type}/{package_name}/versions/{package_version_id}/restore 376 | /user/packages/{package_type}/{package_name} 377 | /user/migrations/{migration_id}/repositories 378 | /user/migrations/{migration_id} 379 | /repos/{owner}/{repo}/issues/{issue_number}/reactions/{reaction_id} 380 | /user/migrations 381 | /user/memberships/orgs/{org} 382 | /user/memberships/orgs 383 | /repos/{owner}/{repo}/pages/builds/{build_id} 384 | /repos/{owner}/{repo}/issues/{issue_number}/timeline 385 | /repos/{owner}/{repo}/issues/events 386 | /repos/{owner}/{repo}/releases/{release_id}/reactions 387 | /user/keys/{key_id} 388 | /orgs/{org}/codespaces 389 | /repos/{owner}/{repo}/commits/{commit_sha}/comments 390 | /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies 391 | /repos/{owner}/{repo}/codespaces 392 | /repos/{owner}/{repo}/keys 393 | /user/interaction-limits 394 | /user/installations/{installation_id}/repositories/{repository_id} 395 | /user/following 396 | /user/email/visibility 397 | /repos/{owner}/{repo}/releases/{release_id}/reactions/{reaction_id} 398 | /orgs/{org}/codespaces/access 399 | /user/docker/conflicts 400 | /user/codespaces/{codespace_name}/start 401 | /user/codespaces/{codespace_name}/machines 402 | /repos/{owner}/{repo}/rules/branches/{branch} 403 | /user/codespaces/{codespace_name}/exports/{export_id} 404 | /user/codespaces/{codespace_name}/exports 405 | /user/codespaces/secrets/{secret_name}/repositories/{repository_id} 406 | /user/codespaces/secrets/{secret_name}/repositories 407 | /user/codespaces/secrets/{secret_name} 408 | /user/codespaces 409 | /repos/{owner}/{repo}/rulesets 410 | /repos/{owner}/{repo}/branches/{branch} 411 | /repos/{owner}/{repo}/keys/{key_id} 412 | /orgs/{org}/packages 413 | /repos/{owner}/{repo}/deployments/{deployment_id} 414 | /repositories/{repository_id}/environments/{environment_name}/variables 415 | /orgs/{org}/members/{username}/codespaces 416 | /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/logs 417 | /search/code 418 | /repos/{owner}/{repo}/actions/runners/generate-jitconfig 419 | /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id} 420 | /repos/{owner}/{repo}/labels 421 | /gists/{gist_id}/comments/{comment_id} 422 | /repos/{owner}/{repo}/actions/runs/{run_id}/cancel 423 | /repos/{owner}/{repo}/rulesets/rule-suites 424 | /orgs/{org}/actions/permissions 425 | /repos/{owner}/{repo}/actions/secrets/{secret_name} 426 | /repos/{owner}/{repo}/actions/caches 427 | /repos/{owner}/{repo}/actions/runs/{run_id}/deployment_protection_rule 428 | /orgs/{org}/members 429 | /repos/{owner}/{repo}/rulesets/rule-suites/{rule_suite_id} 430 | /repos/{owner}/{repo}/pulls/{pull_number}/update-branch 431 | /search/commits 432 | /repos/{owner}/{repo}/import 433 | /repos/{owner}/{repo}/actions/cache/usage 434 | /repos/{owner}/{repo}/rulesets/{ruleset_id} 435 | /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers 436 | /repos/{owner}/{repo}/labels/{name} 437 | /repos/{owner}/{repo} 438 | /orgs/{org}/rulesets/rule-suites/{rule_suite_id} 439 | /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel 440 | /repos/{owner}/{repo}/actions/variables 441 | /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments 442 | /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format} 443 | /search/issues 444 | /repos/{owner}/{repo}/actions/runs/{run_id}/jobs 445 | /orgs/{org}/rulesets 446 | /repos/{owner}/{repo}/codespaces/secrets 447 | /repos/{owner}/{repo}/secret-scanning/alerts 448 | /repos/{owner}/{repo}/hooks/{hook_id}/deliveries 449 | /repos/{owner}/{repo}/actions/variables/{name} 450 | /orgs/{org}/organization-roles/teams/{team_slug}/{role_id} 451 | /repos/{owner}/{repo}/actions/runs/{run_id}/logs 452 | /app/hook/config 453 | /orgs/{org} 454 | /repos/{owner}/{repo}/languages 455 | /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}/restore 456 | /projects/columns/cards/{card_id} 457 | /repos/{owner}/{repo}/commits/{ref} 458 | /search/labels 459 | /orgs/{org}/organization-roles/teams/{team_slug} 460 | /repos/{owner}/{repo}/actions/runs/{run_id}/pending_deployments 461 | /repos/{owner}/{repo}/license 462 | /repos/{owner}/{repo}/issues/{issue_number}/assignees/{assignee} 463 | /repos/{owner}/{repo}/actions/workflows 464 | /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules/apps 465 | /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions/{reaction_id} 466 | /repos/{owner}/{repo}/issues/comments/{comment_id} 467 | /orgs/{org}/hooks/{hook_id}/deliveries/{delivery_id} 468 | /search/repositories 469 | /markdown/raw 470 | /gists/starred 471 | /repos/{owner}/{repo}/actions/workflows/{workflow_id} 472 | /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts 473 | /repos/{owner}/{repo}/hooks/{hook_id}/config 474 | /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules/{protection_rule_id} 475 | /repos/{owner}/{repo}/git/trees/{tree_sha} 476 | /orgs/{org}/memberships/{username} 477 | /orgs/{org}/migrations 478 | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable 479 | /search/topics 480 | /applications/{client_id}/token 481 | /app/installations/{installation_id} 482 | /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules 483 | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches 484 | /repos/{owner}/{repo}/events 485 | /repos/{owner}/{repo}/dependabot/secrets 486 | /repos/{owner}/{repo}/compare/{basehead} 487 | /search/users 488 | /repos/{owner}/{repo}/commits/{ref}/statuses 489 | /repos/{owner}/{repo}/actions/runs/{run_id}/timing 490 | /repos/{owner}/{repo}/forks 491 | /repos/{owner}/{repo}/comments/{comment_id}/reactions 492 | /marketplace_listing/plans 493 | /notifications/threads/{thread_id} 494 | /repos/{owner}/{repo}/assignees 495 | /orgs/{org}/hooks/{hook_id}/config 496 | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable 497 | /orgs/{org}/{security_product}/{enablement} 498 | /teams/{team_id} 499 | /projects/columns/{column_id} 500 | /repos/{owner}/{repo}/code-scanning/alerts/{alert_number} 501 | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs 502 | /orgs/{org}/repos 503 | /orgs/{org}/migrations/{migration_id} 504 | /repos/{owner}/{repo}/traffic/clones 505 | /repos/{owner}/{repo}/actions/secrets 506 | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/timing 507 | /orgs/{org}/migrations/{migration_id}/repos/{repo_name}/lock 508 | /repos/{owner}/{repo}/git/blobs 509 | /orgs/{org}/migrations/{migration_id}/archive 510 | /repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks/contexts 511 | /repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks 512 | /events 513 | /repos/{owner}/{repo}/activity 514 | /orgs/{org}/migrations/{migration_id}/repositories 515 | /teams/{team_id}/discussions 516 | /repos/{owner}/{repo}/issues 517 | /repos/{owner}/{repo}/automated-security-fixes 518 | /repos/{owner}/{repo}/branches 519 | /orgs/{org}/organization-fine-grained-permissions 520 | /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number} 521 | /repos/{owner}/{repo}/git/blobs/{file_sha} 522 | /repos/{owner}/{repo}/branches/{branch}/protection/restrictions/users 523 | /feeds 524 | /teams/{team_id}/discussions/{discussion_number} 525 | /repos/{owner}/{repo}/branches/{branch}/rename 526 | /repos/{owner}/{repo}/check-suites 527 | /orgs/{org}/organization-roles 528 | /repos/{owner}/{repo}/check-suites/{check_suite_id} 529 | /repos/{owner}/{repo}/git/commits 530 | /repos/{owner}/{repo}/check-suites/{check_suite_id}/rerequest 531 | /gists 532 | /repos/{owner}/{repo}/actions/runs/{run_id}/rerun-failed-jobs 533 | /repos/{owner}/{repo}/traffic/popular/paths 534 | /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/instances 535 | /repos/{owner}/{repo}/code-scanning/analyses 536 | /repos/{owner}/{repo}/actions/runs/{run_id}/rerun 537 | /teams/{team_id}/discussions/{discussion_number}/comments 538 | /repos/{owner}/{repo}/code-scanning/sarifs/{sarif_id} 539 | /user/issues 540 | /repos/{owner}/{repo}/traffic/popular/referrers 541 | /orgs/{org}/members/{username}/codespaces/{codespace_name}/stop 542 | /repos/{owner}/{repo}/invitations/{invitation_id} 543 | /repos/{owner}/{repo}/assignees/{assignee} 544 | /user/keys 545 | /repos/{owner}/{repo}/commits/{ref}/check-runs 546 | /teams/{team_id}/discussions/{discussion_number}/comments/{comment_number} 547 | /repos/{owner}/{repo}/traffic/views 548 | /repos/{owner}/{repo}/security-advisories 549 | /repos/{owner}/{repo}/dependabot/secrets/{secret_name} 550 | /repos/{owner}/{repo}/dependency-graph/sbom 551 | /repos/{owner}/{repo}/autolinks 552 | /repos/{owner}/{repo}/deployments/{deployment_id}/statuses/{status_id} 553 | /repos/{owner}/{repo}/dispatches 554 | /repos/{owner}/{repo}/transfer 555 | /repos/{owner}/{repo}/actions/secrets/public-key 556 | /orgs/{org}/hooks/{hook_id}/deliveries/{delivery_id}/attempts 557 | /repos/{owner}/{repo}/invitations 558 | /repos/{owner}/{repo}/git/trees 559 | /teams/{team_id}/discussions/{discussion_number}/comments/{comment_number}/reactions 560 | /gists/public 561 | /repos/{owner}/{repo}/git/commits/{commit_sha} 562 | /classrooms/{classroom_id}/assignments 563 | /repos/{owner}/{repo}/hooks/{hook_id}/pings 564 | /repos/{owner}/{repo}/hooks/{hook_id}/tests 565 | /repos/{owner}/{repo}/vulnerability-alerts 566 | /marketplace_listing/accounts/{account_id} 567 | /repos/{owner}/{repo}/git/matching-refs/{ref} 568 | /repos/{owner}/{repo}/issues/comments 569 | /codes_of_conduct 570 | /teams/{team_id}/discussions/{discussion_number}/reactions 571 | /user/marketplace_purchases 572 | /repos/{owner}/{repo}/issues/{issue_number} 573 | /repos/{owner}/{repo}/codespaces/permissions_check 574 | /repos/{owner}/{repo}/autolinks/{autolink_id} 575 | /repos/{owner}/{repo}/git/ref/{ref} 576 | /codes_of_conduct/{key} 577 | /repos/{owner}/{repo}/merge-upstream 578 | /user/marketplace_purchases/stubbed 579 | /teams/{team_id}/invitations 580 | /repos/{owner}/{repo}/zipball/{ref} 581 | /repos/{owner}/{repo}/git/refs 582 | /emojis 583 | /repos/{owner}/{repo}/comments/{comment_id}/reactions/{reaction_id} 584 | /teams/{team_id}/members 585 | /repos/{template_owner}/{template_repo}/generate 586 | /app-manifests/{code}/conversions 587 | /repos/{owner}/{repo}/readme/{dir} 588 | /enterprises/{enterprise}/dependabot/alerts 589 | /teams/{team_id}/members/{username} 590 | /repos/{owner}/{repo}/codespaces/secrets/public-key 591 | /repos/{owner}/{repo}/git/refs/{ref} 592 | /orgs/{org}/organization-roles/users/{username} 593 | /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id} 594 | /repos/{owner}/{repo}/codespaces/secrets/{secret_name} 595 | /repos/{owner}/{repo}/community/profile 596 | /orgs/{org}/actions/runners/generate-jitconfig 597 | /orgs/{org}/organization-roles/users/{username}/{role_id} 598 | /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}/locations 599 | /meta 600 | /repos/{owner}/{repo}/stats/participation 601 | /repositories 602 | /projects/columns/{column_id}/cards 603 | /repos/{owner}/{repo}/git/tags 604 | /orgs/{org}/rulesets/rule-suites 605 | /repos/{owner}/{repo}/collaborators 606 | /enterprises/{enterprise}/secret-scanning/alerts 607 | /teams/{team_id}/memberships/{username} 608 | /orgs/{org}/organization-roles/{role_id} 609 | /repositories/{repository_id}/environments/{environment_name}/secrets -------------------------------------------------------------------------------- /benchmark/github-routes.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 1 6 | 7 | local router 8 | local i = 0 9 | do 10 | local file, err = io.open("benchmark/github-apis.txt", "r") 11 | if err then 12 | error(err) 13 | end 14 | local routes = {} 15 | for line in file:lines() do 16 | i = i + 1 17 | routes[i] = { paths = { line }, handler = line } 18 | end 19 | file:close() 20 | router = Router.new(routes) 21 | end 22 | 23 | local rss_mb = utils.get_rss() 24 | 25 | local path = "/repos/vm-001/lua-radix-router/import" 26 | 27 | local elapsed = utils.timing(function() 28 | for _ = 1, times do 29 | router:match(path) 30 | end 31 | end) 32 | 33 | utils.print_result({ 34 | title = "github apis", 35 | routes = i, 36 | times = times, 37 | elapsed = elapsed, 38 | benchmark_path = path, 39 | benchmark_handler = router:match(path), 40 | rss = rss_mb, 41 | }) 42 | -------------------------------------------------------------------------------- /benchmark/simple-prefix.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { paths = { string.format("/%d/{*path}", i) }, handler = i } 12 | end 13 | router = Router.new(routes) 14 | end 15 | 16 | local rss_mb = utils.get_rss() 17 | 18 | local path = "/1/a" 19 | local elapsed = utils.timing(function() 20 | for _ = 1, times do 21 | router:match(path) 22 | end 23 | end) 24 | 25 | utils.print_result({ 26 | title = "prefix", 27 | routes = route_n, 28 | times = times, 29 | elapsed = elapsed, 30 | benchmark_path = path, 31 | benchmark_handler = router:match(path), 32 | rss = rss_mb, 33 | }) 34 | -------------------------------------------------------------------------------- /benchmark/simple-regex.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { paths = { string.format("/%d/{name:[^/]+}", i) }, handler = i } 12 | end 13 | router = Router.new(routes) 14 | end 15 | 16 | local rss_mb = utils.get_rss() 17 | 18 | local path = "/1/a" 19 | local elapsed = utils.timing(function() 20 | for _ = 1, times do 21 | router:match(path) 22 | end 23 | end) 24 | 25 | utils.print_result({ 26 | title = "regex", 27 | routes = route_n, 28 | times = times, 29 | elapsed = elapsed, 30 | benchmark_path = path, 31 | benchmark_handler = router:match(path), 32 | rss = rss_mb, 33 | }) 34 | -------------------------------------------------------------------------------- /benchmark/simple-variable-binding.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { paths = { string.format("/%d/{name}", i) }, handler = i } 12 | end 13 | router = Router.new(routes) 14 | end 15 | 16 | local rss_mb = utils.get_rss() 17 | 18 | local path = "/1/foo" 19 | local params = {} 20 | 21 | local elapsed = utils.timing(function() 22 | for _ = 1, times do 23 | router:match(path, nil, params) 24 | end 25 | end) 26 | 27 | utils.print_result({ 28 | title = "variable", 29 | routes = route_n, 30 | times = times, 31 | elapsed = elapsed, 32 | benchmark_path = path, 33 | benchmark_handler = router:match(path), 34 | rss = rss_mb, 35 | }, { 36 | { name = "params", value = string.format("name = " .. params.name) } 37 | }) 38 | -------------------------------------------------------------------------------- /benchmark/simple-variable.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { paths = { string.format("/%d/{name}", i) }, handler = i } 12 | end 13 | router = Router.new(routes) 14 | end 15 | 16 | local rss_mb = utils.get_rss() 17 | 18 | local path = "/1/foo" 19 | 20 | local elapsed = utils.timing(function() 21 | for _ = 1, times do 22 | router:match(path) 23 | end 24 | end) 25 | 26 | utils.print_result({ 27 | title = "variable", 28 | routes = route_n, 29 | times = times, 30 | elapsed = elapsed, 31 | benchmark_path = path, 32 | benchmark_handler = router:match(path), 33 | rss = rss_mb, 34 | }) 35 | -------------------------------------------------------------------------------- /benchmark/static-paths.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local utils = require "benchmark.utils" 3 | 4 | local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100 5 | local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10 6 | 7 | local router 8 | do 9 | local routes = {} 10 | for i = 1, route_n do 11 | routes[i] = { paths = { string.format("/%d", i) }, handler = i } 12 | end 13 | router = Router.new(routes) 14 | end 15 | 16 | local path = "/" .. route_n / 2 17 | 18 | local rss_mb = utils.get_rss() 19 | 20 | local elapsed = utils.timing(function() 21 | for _ = 1, times do 22 | router:match(path) 23 | end 24 | end) 25 | 26 | utils.print_result({ 27 | title = "static path", 28 | routes = route_n, 29 | times = times, 30 | elapsed = elapsed, 31 | benchmark_path = path, 32 | benchmark_handler = router:match(path), 33 | rss = rss_mb, 34 | }) 35 | -------------------------------------------------------------------------------- /benchmark/utils.lua: -------------------------------------------------------------------------------- 1 | local fmt = string.format 2 | 3 | local function timing(fn) 4 | local start_time = os.clock() 5 | fn() 6 | return os.clock() - start_time 7 | end 8 | 9 | local function print_result(result, items) 10 | print(fmt("========== %s ==========", result.title)) 11 | print("routes :", result.routes) 12 | print("times :", result.times) 13 | print("elapsed :", result.elapsed .. " s") 14 | print("ns/op :", result.elapsed * 1000 * 1000 * 1000 / result.times .. " ns") 15 | print("OPS :", math.floor(result.times / result.elapsed)) 16 | print("path :", result.benchmark_path) 17 | print("handler :", result.benchmark_handler) 18 | for _, item in ipairs(items or {}) do 19 | print(fmt("%s : %s", item.name, item.value)) 20 | end 21 | print("Memory :", result.rss) 22 | print() 23 | end 24 | 25 | local function get_pid() 26 | local ok, ffi = pcall(require, "ffi") 27 | if ok then 28 | ffi.cdef [[ 29 | int getpid(void); 30 | ]] 31 | return ffi.C.getpid() 32 | end 33 | return nil 34 | end 35 | 36 | local function get_rss() 37 | collectgarbage("collect") 38 | 39 | local pid = get_pid() 40 | if not pid then 41 | return "unable to get the pid" 42 | end 43 | 44 | local command = "ps -o rss= -p " .. tostring(pid) 45 | local handle = io.popen(command) 46 | local result = handle:read("*a") 47 | handle:close() 48 | local kbytes = tonumber(result) or 0 49 | return fmt("%.2f MB", kbytes / 1024) 50 | end 51 | 52 | return { 53 | timing = timing, 54 | print_result = print_result, 55 | get_rss = get_rss, 56 | } 57 | -------------------------------------------------------------------------------- /bin/resty_busted: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env resty 2 | 3 | if ngx ~= nil then 4 | ngx.exit = function()end 5 | end 6 | 7 | require 'busted.runner'({ standalone = false }) -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | project = 'Radix-Router' 2 | description = 'A lightweight high-performance and radix tree based router for Lua/LuaJIT.' 3 | full_description = '' 4 | examples = { './examples' } 5 | file='./src/router.lua' 6 | dir = 'docs' 7 | -------------------------------------------------------------------------------- /docs/examples/custom-matcher.lua.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Reference 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 45 | 46 |
47 | 48 |

custom-matcher.lua

49 |
 50 | local Router = require "radix-router"
 51 | 
 52 | local ip_matcher = {
 53 |   process = function(route)
 54 |     -- builds a table for O(1) access
 55 |     if route.ips then
 56 |       local ips = {}
 57 |       for _, ip in ipairs(route.ips) do
 58 |         ips[ip] = true
 59 |       end
 60 |       route.ips = ips
 61 |     end
 62 |   end,
 63 |   match = function(route, ctx, matched)
 64 |     if route.ips then
 65 |       local ip = ctx.ip
 66 |       if not route.ips[ip] then
 67 |         return false
 68 |       end
 69 |       if matched then
 70 |         matched["ip"] = ip
 71 |       end
 72 |     end
 73 |     return true
 74 |   end
 75 | }
 76 | 
 77 | local opts = {
 78 |   matchers = { ip_matcher }, -- register custom ip_matcher
 79 |   matcher_names = { "method" }, -- host is disabled
 80 | }
 81 | 
 82 | local router = Router.new({
 83 |   {
 84 |     paths = { "/" },
 85 |     methods = { "GET", "POST" },
 86 |     ips = { "127.0.0.1", "127.0.0.2" },
 87 |     handler = "1",
 88 |   },
 89 |   {
 90 |     paths = { "/" },
 91 |     methods = { "GET", "POST" },
 92 |     ips = { "192.168.1.1", "192.168.1.2" },
 93 |     handler = "2",
 94 |   }
 95 | }, opts)
 96 | assert("1" == router:match("/", { method = "GET", ip = "127.0.0.2" }))
 97 | local matched = {}
 98 | assert("2" == router:match("/", { method = "GET", ip = "192.168.1.2" }, nil, matched))
 99 | print(matched.method) -- GET
100 | print(matched.ip) -- 192.168.1.2
101 | 102 | 103 |
104 |
105 |
106 | generated by LDoc 1.5.0 107 | Last updated 2024-03-01 01:33:25 108 |
109 |
110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/examples/example.lua.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Reference 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 45 | 46 |
47 | 48 |

example.lua

49 |
50 | local Router = require "radix-router"
51 | local router, err = Router.new({
52 |   {
53 |     paths = { "/foo", "/foo/bar", "/html/index.html" },
54 |     handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function)
55 |   },
56 |   {
57 |     -- variable path
58 |     paths = { "/users/{id}/profile-{year}.{format}" },
59 |     handler = "2"
60 |   },
61 |   {
62 |     -- prefix path
63 |     paths = { "/api/authn/{*path}" },
64 |     handler = "3"
65 |   },
66 |   {
67 |     -- methods
68 |     paths = { "/users/{id}" },
69 |     methods = { "POST" },
70 |     handler = "4"
71 |   }
72 | })
73 | if not router then
74 |   error("failed to create router: " .. err)
75 | end
76 | 
77 | assert("1" == router:match("/html/index.html"))
78 | assert("2" == router:match("/users/100/profile-2023.pdf"))
79 | assert("3" == router:match("/api/authn/token/genreate"))
80 | assert("4" == router:match("/users/100", { method = "POST" }))
81 | 
82 | -- parameter binding
83 | local params = {}
84 | router:match("/users/100/profile-2023.pdf", nil, params)
85 | assert(params.year == "2023")
86 | assert(params.format == "pdf")
87 | 88 | 89 |
90 |
91 |
92 | generated by LDoc 1.5.0 93 | Last updated 2024-03-01 01:33:25 94 |
95 |
96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/examples/regular-expression.lua.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Reference 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 45 | 46 |
47 | 48 |

regular-expression.lua

49 |
50 | local Router = require "radix-router"
51 | local router, err = Router.new({
52 |   {
53 |     paths = { "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}" },
54 |     handler = "1"
55 |   },
56 |   {
57 |     paths = { "/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" },
58 |     handler = "2"
59 |   },
60 | })
61 | if not router then
62 |   error("failed to create router: " .. err)
63 | end
64 | 
65 | assert("1" == router:match("/users/100/profile-2024.pdf"))
66 | assert("2", router:match("/users/00000000-0000-0000-0000-000000000000"))
67 | 68 | 69 |
70 |
71 |
72 | generated by LDoc 1.5.0 73 | Last updated 2024-03-01 01:33:25 74 |
75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Reference 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 49 | 50 |
51 | 52 |

Module radix-router

53 |

Radix-Router is a lightweight high-performance and radix tree based router matching library.

54 |

55 | 56 | 57 |

Functions

58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
Router.new ([routes[, opts]])create a new router.
Router:match (path[, ctx[, params[, matched]]])find a handler of route that matches the path and ctx.
68 | 69 |
70 |
71 | 72 | 73 |

Functions

74 | 75 |
76 |
77 | 78 | Router.new ([routes[, opts]]) 79 |
80 |
81 | create a new router. 82 | 83 | 84 |

Parameters:

85 | 97 | 98 |

Returns:

99 |
    100 |
  1. 101 | a new router, or nil
  2. 102 |
  3. 103 | cannot create router error
  4. 104 |
105 | 106 | 107 | 108 |

Usage:

109 | 118 | 119 |
120 |
121 | 122 | Router:match (path[, ctx[, params[, matched]]]) 123 |
124 |
125 | find a handler of route that matches the path and ctx. 126 | 127 | 128 |

Parameters:

129 | 150 | 151 |

Returns:

152 |
    153 | 154 | the handler of a route matches the path and ctx, or nil if not found 155 |
156 | 157 | 158 | 159 |

Usage:

160 | 165 | 166 |
167 |
168 | 169 | 170 | 171 | 172 |
173 | generated by LDoc 1.5.0 174 | Last updated 2024-03-01 01:33:25 175 |
176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /docs/ldoc.css: -------------------------------------------------------------------------------- 1 | /* BEGIN RESET 2 | 3 | Copyright (c) 2010, Yahoo! Inc. All rights reserved. 4 | Code licensed under the BSD License: 5 | http://developer.yahoo.com/yui/license.html 6 | version: 2.8.2r1 7 | */ 8 | html { 9 | color: #000; 10 | background: #FFF; 11 | } 12 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | } 20 | fieldset,img { 21 | border: 0; 22 | } 23 | address,caption,cite,code,dfn,em,strong,th,var,optgroup { 24 | font-style: inherit; 25 | font-weight: inherit; 26 | } 27 | del,ins { 28 | text-decoration: none; 29 | } 30 | li { 31 | margin-left: 20px; 32 | } 33 | caption,th { 34 | text-align: left; 35 | } 36 | h1,h2,h3,h4,h5,h6 { 37 | font-size: 100%; 38 | font-weight: bold; 39 | } 40 | q:before,q:after { 41 | content: ''; 42 | } 43 | abbr,acronym { 44 | border: 0; 45 | font-variant: normal; 46 | } 47 | sup { 48 | vertical-align: baseline; 49 | } 50 | sub { 51 | vertical-align: baseline; 52 | } 53 | legend { 54 | color: #000; 55 | } 56 | input,button,textarea,select,optgroup,option { 57 | font-family: inherit; 58 | font-size: inherit; 59 | font-style: inherit; 60 | font-weight: inherit; 61 | } 62 | input,button,textarea,select {*font-size:100%; 63 | } 64 | /* END RESET */ 65 | 66 | body { 67 | margin-left: 1em; 68 | margin-right: 1em; 69 | font-family: arial, helvetica, geneva, sans-serif; 70 | background-color: #ffffff; margin: 0px; 71 | } 72 | 73 | code, tt { font-family: monospace; font-size: 1.1em; } 74 | span.parameter { font-family:monospace; } 75 | span.parameter:after { content:":"; } 76 | span.types:before { content:"("; } 77 | span.types:after { content:")"; } 78 | .type { font-weight: bold; font-style:italic } 79 | 80 | body, p, td, th { font-size: .95em; line-height: 1.2em;} 81 | 82 | p, ul { margin: 10px 0 0 0px;} 83 | 84 | strong { font-weight: bold;} 85 | 86 | em { font-style: italic;} 87 | 88 | h1 { 89 | font-size: 1.5em; 90 | margin: 20px 0 20px 0; 91 | } 92 | h2, h3, h4 { margin: 15px 0 10px 0; } 93 | h2 { font-size: 1.25em; } 94 | h3 { font-size: 1.15em; } 95 | h4 { font-size: 1.06em; } 96 | 97 | a:link { font-weight: bold; color: #004080; text-decoration: none; } 98 | a:visited { font-weight: bold; color: #006699; text-decoration: none; } 99 | a:link:hover { text-decoration: underline; } 100 | 101 | hr { 102 | color:#cccccc; 103 | background: #00007f; 104 | height: 1px; 105 | } 106 | 107 | blockquote { margin-left: 3em; } 108 | 109 | ul { list-style-type: disc; } 110 | 111 | p.name { 112 | font-family: "Andale Mono", monospace; 113 | padding-top: 1em; 114 | } 115 | 116 | pre { 117 | background-color: rgb(245, 245, 245); 118 | border: 1px solid #C0C0C0; /* silver */ 119 | padding: 10px; 120 | margin: 10px 0 10px 0; 121 | overflow: auto; 122 | font-family: "Andale Mono", monospace; 123 | } 124 | 125 | pre.example { 126 | font-size: .85em; 127 | } 128 | 129 | table.index { border: 1px #00007f; } 130 | table.index td { text-align: left; vertical-align: top; } 131 | 132 | #container { 133 | margin-left: 1em; 134 | margin-right: 1em; 135 | background-color: #f0f0f0; 136 | } 137 | 138 | #product { 139 | text-align: center; 140 | border-bottom: 1px solid #cccccc; 141 | background-color: #ffffff; 142 | } 143 | 144 | #product big { 145 | font-size: 2em; 146 | } 147 | 148 | #main { 149 | background-color: #f0f0f0; 150 | border-left: 2px solid #cccccc; 151 | } 152 | 153 | #navigation { 154 | float: left; 155 | width: 14em; 156 | vertical-align: top; 157 | background-color: #f0f0f0; 158 | overflow: visible; 159 | } 160 | 161 | #navigation h2 { 162 | background-color:#e7e7e7; 163 | font-size:1.1em; 164 | color:#000000; 165 | text-align: left; 166 | padding:0.2em; 167 | border-top:1px solid #dddddd; 168 | border-bottom:1px solid #dddddd; 169 | } 170 | 171 | #navigation ul 172 | { 173 | font-size:1em; 174 | list-style-type: none; 175 | margin: 1px 1px 10px 1px; 176 | } 177 | 178 | #navigation li { 179 | text-indent: -1em; 180 | display: block; 181 | margin: 3px 0px 0px 22px; 182 | } 183 | 184 | #navigation li li a { 185 | margin: 0px 3px 0px -1em; 186 | } 187 | 188 | #content { 189 | margin-left: 14em; 190 | padding: 1em; 191 | width: 700px; 192 | border-left: 2px solid #cccccc; 193 | border-right: 2px solid #cccccc; 194 | background-color: #ffffff; 195 | } 196 | 197 | #about { 198 | clear: both; 199 | padding: 5px; 200 | border-top: 2px solid #cccccc; 201 | background-color: #ffffff; 202 | } 203 | 204 | @media print { 205 | body { 206 | font: 12pt "Times New Roman", "TimeNR", Times, serif; 207 | } 208 | a { font-weight: bold; color: #004080; text-decoration: underline; } 209 | 210 | #main { 211 | background-color: #ffffff; 212 | border-left: 0px; 213 | } 214 | 215 | #container { 216 | margin-left: 2%; 217 | margin-right: 2%; 218 | background-color: #ffffff; 219 | } 220 | 221 | #content { 222 | padding: 1em; 223 | background-color: #ffffff; 224 | } 225 | 226 | #navigation { 227 | display: none; 228 | } 229 | pre.example { 230 | font-family: "Andale Mono", monospace; 231 | font-size: 10pt; 232 | page-break-inside: avoid; 233 | } 234 | } 235 | 236 | table.module_list { 237 | border-width: 1px; 238 | border-style: solid; 239 | border-color: #cccccc; 240 | border-collapse: collapse; 241 | } 242 | table.module_list td { 243 | border-width: 1px; 244 | padding: 3px; 245 | border-style: solid; 246 | border-color: #cccccc; 247 | } 248 | table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } 249 | table.module_list td.summary { width: 100%; } 250 | 251 | 252 | table.function_list { 253 | border-width: 1px; 254 | border-style: solid; 255 | border-color: #cccccc; 256 | border-collapse: collapse; 257 | } 258 | table.function_list td { 259 | border-width: 1px; 260 | padding: 3px; 261 | border-style: solid; 262 | border-color: #cccccc; 263 | } 264 | table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } 265 | table.function_list td.summary { width: 100%; } 266 | 267 | ul.nowrap { 268 | overflow:auto; 269 | white-space:nowrap; 270 | } 271 | 272 | dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} 273 | dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} 274 | dl.table h3, dl.function h3 {font-size: .95em;} 275 | 276 | /* stop sublists from having initial vertical space */ 277 | ul ul { margin-top: 0px; } 278 | ol ul { margin-top: 0px; } 279 | ol ol { margin-top: 0px; } 280 | ul ol { margin-top: 0px; } 281 | 282 | /* make the target distinct; helps when we're navigating to a function */ 283 | a:target + * { 284 | background-color: #FF9; 285 | } 286 | 287 | 288 | /* styles for prettification of source */ 289 | pre .comment { color: #558817; } 290 | pre .constant { color: #a8660d; } 291 | pre .escape { color: #844631; } 292 | pre .keyword { color: #aa5050; font-weight: bold; } 293 | pre .library { color: #0e7c6b; } 294 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 295 | pre .string { color: #8080ff; } 296 | pre .number { color: #f8660d; } 297 | pre .function-name { color: #60447f; } 298 | pre .operator { color: #2239a8; font-weight: bold; } 299 | pre .preprocessor, pre .prepro { color: #a33243; } 300 | pre .global { color: #800080; } 301 | pre .user-keyword { color: #800080; } 302 | pre .prompt { color: #558817; } 303 | pre .url { color: #272fc2; text-decoration: underline; } 304 | 305 | -------------------------------------------------------------------------------- /examples/custom-matcher.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | 3 | local ip_matcher = { 4 | process = function(route) 5 | -- builds a table for O(1) access 6 | if route.ips then 7 | local ips = {} 8 | for _, ip in ipairs(route.ips) do 9 | ips[ip] = true 10 | end 11 | route.ips = ips 12 | end 13 | end, 14 | match = function(route, ctx, matched) 15 | if route.ips then 16 | local ip = ctx.ip 17 | if not route.ips[ip] then 18 | return false 19 | end 20 | if matched then 21 | matched["ip"] = ip 22 | end 23 | end 24 | return true 25 | end 26 | } 27 | 28 | local opts = { 29 | matchers = { ip_matcher }, -- register custom ip_matcher 30 | matcher_names = { "method" }, -- host is disabled 31 | } 32 | 33 | local router = Router.new({ 34 | { 35 | paths = { "/" }, 36 | methods = { "GET", "POST" }, 37 | ips = { "127.0.0.1", "127.0.0.2" }, 38 | handler = "1", 39 | }, 40 | { 41 | paths = { "/" }, 42 | methods = { "GET", "POST" }, 43 | ips = { "192.168.1.1", "192.168.1.2" }, 44 | handler = "2", 45 | } 46 | }, opts) 47 | assert("1" == router:match("/", { method = "GET", ip = "127.0.0.2" })) 48 | local matched = {} 49 | assert("2" == router:match("/", { method = "GET", ip = "192.168.1.2" }, nil, matched)) 50 | print(matched.method) -- GET 51 | print(matched.ip) -- 192.168.1.2 52 | -------------------------------------------------------------------------------- /examples/example.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local router, err = Router.new({ 3 | { 4 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 5 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 6 | }, 7 | { 8 | -- variable path 9 | paths = { "/users/{id}/profile-{year}.{format}" }, 10 | handler = "2" 11 | }, 12 | { 13 | -- prefix path 14 | paths = { "/api/authn/{*path}" }, 15 | handler = "3" 16 | }, 17 | { 18 | -- methods 19 | paths = { "/users/{id}" }, 20 | methods = { "POST" }, 21 | handler = "4" 22 | } 23 | }) 24 | if not router then 25 | error("failed to create router: " .. err) 26 | end 27 | 28 | assert("1" == router:match("/html/index.html")) 29 | assert("2" == router:match("/users/100/profile-2023.pdf")) 30 | assert("3" == router:match("/api/authn/token/genreate")) 31 | assert("4" == router:match("/users/100", { method = "POST" })) 32 | 33 | -- parameter binding 34 | local params = {} 35 | router:match("/users/100/profile-2023.pdf", nil, params) 36 | assert(params.year == "2023") 37 | assert(params.format == "pdf") 38 | -------------------------------------------------------------------------------- /examples/regular-expression.lua: -------------------------------------------------------------------------------- 1 | local Router = require "radix-router" 2 | local router, err = Router.new({ 3 | { 4 | paths = { "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}" }, 5 | handler = "1" 6 | }, 7 | { 8 | paths = { "/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" }, 9 | handler = "2" 10 | }, 11 | }) 12 | if not router then 13 | error("failed to create router: " .. err) 14 | end 15 | 16 | assert("1" == router:match("/users/100/profile-2024.pdf")) 17 | assert("2", router:match("/users/00000000-0000-0000-0000-000000000000")) -------------------------------------------------------------------------------- /lua-radix-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vm-001/lua-radix-router/37d79f884efa77552e1619fe28bc2668ea083de7/lua-radix-router.png -------------------------------------------------------------------------------- /radix-router-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "dev-1" 3 | 4 | source = { 5 | url = "git+https://github.com/vm-001/lua-radix-router.git", 6 | branch = "main" 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | 53 | dependencies = { 54 | "lrexlib-pcre2", 55 | } 56 | 57 | build = { 58 | type = "builtin", 59 | modules = { 60 | ["radix-router"] = "src/router.lua", 61 | ["radix-router.options"] = "src/options.lua", 62 | ["radix-router.route"] = "src/route.lua", 63 | ["radix-router.trie"] = "src/trie.lua", 64 | ["radix-router.utils"] = "src/utils.lua", 65 | ["radix-router.constants"] = "src/constants.lua", 66 | ["radix-router.iterator"] = "src/iterator.lua", 67 | ["radix-router.parser"] = "src/parser/parser.lua", 68 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 69 | ["radix-router.matcher"] = "src/matcher/matcher.lua", 70 | ["radix-router.matcher.host"] = "src/matcher/host.lua", 71 | ["radix-router.matcher.method"] = "src/matcher/method.lua", 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /rockspecs/radix-router-0.2.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.2.0-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.2.0", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | dependencies = { 53 | "lua >= 5.1, < 5.5" 54 | } 55 | 56 | build = { 57 | type = "builtin", 58 | modules = { 59 | ["radix-router"] = "src/router.lua", 60 | ["radix-router.route"] = "src/route.lua", 61 | ["radix-router.trie"] = "src/trie.lua", 62 | ["radix-router.utils"] = "src/utils.lua", 63 | ["radix-router.constants"] = "src/constants.lua", 64 | ["radix-router.iterator"] = "src/iterator.lua", 65 | ["radix-router.parser"] = "src/parser/parser.lua", 66 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 67 | }, 68 | } -------------------------------------------------------------------------------- /rockspecs/radix-router-0.3.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.3.0-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.3.0", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | dependencies = { 53 | "lua >= 5.1, < 5.5" 54 | } 55 | 56 | build = { 57 | type = "builtin", 58 | modules = { 59 | ["radix-router"] = "src/router.lua", 60 | ["radix-router.route"] = "src/route.lua", 61 | ["radix-router.trie"] = "src/trie.lua", 62 | ["radix-router.utils"] = "src/utils.lua", 63 | ["radix-router.constants"] = "src/constants.lua", 64 | ["radix-router.iterator"] = "src/iterator.lua", 65 | ["radix-router.parser"] = "src/parser/parser.lua", 66 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 67 | }, 68 | } -------------------------------------------------------------------------------- /rockspecs/radix-router-0.4.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.4.0-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.4.0", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | dependencies = { 53 | "lua >= 5.1, < 5.5" 54 | } 55 | 56 | build = { 57 | type = "builtin", 58 | modules = { 59 | ["radix-router"] = "src/router.lua", 60 | ["radix-router.options"] = "src/options.lua", 61 | ["radix-router.route"] = "src/route.lua", 62 | ["radix-router.trie"] = "src/trie.lua", 63 | ["radix-router.utils"] = "src/utils.lua", 64 | ["radix-router.constants"] = "src/constants.lua", 65 | ["radix-router.iterator"] = "src/iterator.lua", 66 | ["radix-router.parser"] = "src/parser/parser.lua", 67 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 68 | }, 69 | } -------------------------------------------------------------------------------- /rockspecs/radix-router-0.5.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.5.0-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.5.0", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | dependencies = { 53 | "lua >= 5.1, < 5.5" 54 | } 55 | 56 | build = { 57 | type = "builtin", 58 | modules = { 59 | ["radix-router"] = "src/router.lua", 60 | ["radix-router.options"] = "src/options.lua", 61 | ["radix-router.route"] = "src/route.lua", 62 | ["radix-router.trie"] = "src/trie.lua", 63 | ["radix-router.utils"] = "src/utils.lua", 64 | ["radix-router.constants"] = "src/constants.lua", 65 | ["radix-router.iterator"] = "src/iterator.lua", 66 | ["radix-router.parser"] = "src/parser/parser.lua", 67 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 68 | ["radix-router.matcher"] = "src/matcher/matcher.lua", 69 | ["radix-router.matcher.host"] = "src/matcher/host.lua", 70 | ["radix-router.matcher.method"] = "src/matcher/method.lua", 71 | }, 72 | } -------------------------------------------------------------------------------- /rockspecs/radix-router-0.6.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.6.0-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.6.0", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | 53 | dependencies = { 54 | "lrexlib-pcre2", 55 | } 56 | 57 | build = { 58 | type = "builtin", 59 | modules = { 60 | ["radix-router"] = "src/router.lua", 61 | ["radix-router.options"] = "src/options.lua", 62 | ["radix-router.route"] = "src/route.lua", 63 | ["radix-router.trie"] = "src/trie.lua", 64 | ["radix-router.utils"] = "src/utils.lua", 65 | ["radix-router.constants"] = "src/constants.lua", 66 | ["radix-router.iterator"] = "src/iterator.lua", 67 | ["radix-router.parser"] = "src/parser/parser.lua", 68 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 69 | ["radix-router.matcher"] = "src/matcher/matcher.lua", 70 | ["radix-router.matcher.host"] = "src/matcher/host.lua", 71 | ["radix-router.matcher.method"] = "src/matcher/method.lua", 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /rockspecs/radix-router-0.6.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "radix-router" 2 | version = "0.6.1-1" 3 | 4 | source = { 5 | url = "git://github.com/vm-001/lua-radix-router", 6 | tag = "v0.6.1", 7 | } 8 | 9 | description = { 10 | summary = "Fast API Router for Lua/LuaJIT", 11 | detailed = [[ 12 | A lightweight high-performance and radix tree based router for Lua/LuaJIT/OpenResty. 13 | 14 | local Router = require "radix-router" 15 | local router, err = Router.new({ 16 | { -- static path 17 | paths = { "/foo", "/foo/bar", "/html/index.html" }, 18 | handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function) 19 | }, 20 | { -- variable path 21 | paths = { "/users/{id}/profile-{year}.{format}" }, 22 | handler = "2" 23 | }, 24 | { -- prefix path 25 | paths = { "/api/authn/{*path}" }, 26 | handler = "3" 27 | }, 28 | { -- methods condition 29 | paths = { "/users/{id}" }, 30 | methods = { "POST" }, 31 | handler = "4" 32 | } 33 | }) 34 | if not router then 35 | error("failed to create router: " .. err) 36 | end 37 | 38 | assert("1" == router:match("/html/index.html")) 39 | assert("2" == router:match("/users/100/profile-2023.pdf")) 40 | assert("3" == router:match("/api/authn/token/genreate")) 41 | assert("4" == router:match("/users/100", { method = "POST" })) 42 | 43 | -- variable binding 44 | local params = {} 45 | router:match("/users/100/profile-2023.pdf", nil, params) 46 | assert(params.year == "2023") 47 | assert(params.format == "pdf") 48 | ]], 49 | homepage = "https://github.com/vm-001/lua-radix-router", 50 | license = "BSD-2-Clause license" 51 | } 52 | 53 | dependencies = { 54 | "lrexlib-pcre2", 55 | } 56 | 57 | build = { 58 | type = "builtin", 59 | modules = { 60 | ["radix-router"] = "src/router.lua", 61 | ["radix-router.options"] = "src/options.lua", 62 | ["radix-router.route"] = "src/route.lua", 63 | ["radix-router.trie"] = "src/trie.lua", 64 | ["radix-router.utils"] = "src/utils.lua", 65 | ["radix-router.constants"] = "src/constants.lua", 66 | ["radix-router.iterator"] = "src/iterator.lua", 67 | ["radix-router.parser"] = "src/parser/parser.lua", 68 | ["radix-router.parser.style.default"] = "src/parser/style/default.lua", 69 | ["radix-router.matcher"] = "src/matcher/matcher.lua", 70 | ["radix-router.matcher.host"] = "src/matcher/host.lua", 71 | ["radix-router.matcher.method"] = "src/matcher/method.lua", 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /spec/parser_spec.lua: -------------------------------------------------------------------------------- 1 | require 'busted.runner'() 2 | 3 | local Parser = require "radix-router.parser" 4 | 5 | describe("parser", function() 6 | describe("default style", function() 7 | it("parse()", function() 8 | local tests = { 9 | [""] = { "" }, 10 | ["{var}"] = { "{var}" }, 11 | ["/{var}/end"] = { "/", "{var}", "/end" }, 12 | ["/aa/{var}"] = { "/aa/", "{var}" }, 13 | ["/aa/{var}/cc"] = { "/aa/", "{var}", "/cc" }, 14 | ["/aa/{var1}/cc/{var2}"] = { "/aa/", "{var1}", "/cc/", "{var2}" }, 15 | ["/user/profile.{format}"] = { "/user/profile.", "{format}" }, 16 | ["/user/{filename}.{format}"] = { "/user/", "{filename}", ".", "{format}" }, 17 | ["/aa/{name:[0-9]+}/{*suffix}"] = { "/aa/", "{name:[0-9]+}", "/", "{*suffix}" }, 18 | ["/user/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}"] = { "/user/", "{id:\\d+}", "/profile-", "{year:\\d{4}}", ".", "{format:(html|pdf)}" }, 19 | } 20 | 21 | for path, expected_tokens in pairs(tests) do 22 | local parser = Parser.new("default") 23 | parser:update(path) 24 | local tokens = parser:parse() 25 | assert.same(expected_tokens, tokens) 26 | end 27 | end) 28 | it("params()", function() 29 | local tests = { 30 | [""] = { }, 31 | ["{var}"] = { "var" }, 32 | ["/aa/{var1}/cc/{var2}/{}/{*}"] = { "var1", "var2" }, 33 | ["/aa/{name:[0-9]+}/{*suffix}"] = { "name", "suffix" } 34 | } 35 | 36 | for path, expected_params in pairs(tests) do 37 | local parser = Parser.new("default") 38 | parser:update(path) 39 | local params = parser:params() 40 | assert.same(expected_params, params) 41 | end 42 | end) 43 | it("bind_params() with trailing_slash_mode", function() 44 | local tests = { 45 | { 46 | path = "/a/var1/", 47 | matched_path = "/a/{var}", 48 | params = { 49 | var = "var1" 50 | }, 51 | }, 52 | { 53 | path = "/a/var1", 54 | matched_path = "/a/{var}/", 55 | params = { 56 | var = "var1" 57 | }, 58 | } 59 | } 60 | for i, test in pairs(tests) do 61 | local parser = Parser.new("default") 62 | local params = {} 63 | parser:update(test.matched_path):bind_params(test.path, #test.path, params, true) 64 | assert.same(test.params, params, "assertion failed: " .. i) 65 | end 66 | end) 67 | it("compile_regex()", function() 68 | local tests = { 69 | { 70 | path = "/a/b/c", 71 | regex = "^\\Q/a/b/c\\E$" 72 | }, 73 | { 74 | path = "/a/{b}/c/{d}", 75 | regex = "^\\Q/a/\\E[^/]+\\Q/c/\\E[^/]+$" 76 | }, 77 | { 78 | path = "/a/{b:\\d+}/c/{d:\\d{3}}", 79 | regex = "^\\Q/a/\\E\\d+\\Q/c/\\E\\d{3}$" 80 | }, 81 | { 82 | path = "/a/{*catchall}", 83 | regex = "^\\Q/a/\\E.*$" 84 | }, 85 | { 86 | path = "/a/{b}/c/{*catchall}", 87 | regex = "^\\Q/a/\\E[^/]+\\Q/c/\\E.*$" 88 | }, 89 | { 90 | path = "/a/{b:[a-z]+}/c/{*catchall}", 91 | regex = "^\\Q/a/\\E[a-z]+\\Q/c/\\E.*$" 92 | }, 93 | { 94 | path = "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}", 95 | regex = "^\\Q/users/\\E\\d+\\Q/profile-\\E\\d{4}\\Q.\\E(html|pdf)$" 96 | } 97 | } 98 | for i, test in pairs(tests) do 99 | local parser = Parser.new("default") 100 | local regex = parser:update(test.path):compile_regex() 101 | assert.same(test.regex, regex, "assertion failed: " .. i) 102 | end 103 | end) 104 | end) 105 | end) 106 | -------------------------------------------------------------------------------- /spec/router_spec.lua: -------------------------------------------------------------------------------- 1 | require 'busted.runner'() 2 | 3 | local Router = require "radix-router" 4 | 5 | describe("Router", function() 6 | describe("new", function() 7 | it("new()", function() 8 | local router, err = Router.new() 9 | assert.is_nil(err) 10 | assert.not_nil(router) 11 | end) 12 | it("new() with routes argument", function() 13 | local router, err = Router.new({}) 14 | assert.is_nil(err) 15 | assert.not_nil(router) 16 | 17 | local router, err = Router.new({ 18 | { 19 | paths = { "/" }, 20 | handler = "/", 21 | } 22 | }) 23 | assert.is_nil(err) 24 | assert.not_nil(router) 25 | end) 26 | it("new() with invalid routes arugment", function() 27 | local router, err 28 | 29 | router, err = Router.new(false) 30 | assert.is_nil(router) 31 | assert.equal("invalid args routes: routes must be table or nil", err) 32 | 33 | router, err = Router.new("") 34 | assert.is_nil(router) 35 | assert.equal("invalid args routes: routes must be table or nil", err) 36 | 37 | router, err = Router.new({ 38 | { 39 | paths = { "/" }, 40 | handler = "/", 41 | }, 42 | { 43 | paths = { "/" }, 44 | handler = nil, 45 | }, 46 | }) 47 | assert.is_nil(router) 48 | assert.equal("invalid route(index 2): handler must not be nil", err) 49 | 50 | router, err = Router.new({ 51 | { 52 | paths = { "/" }, 53 | methods = { "unknown" }, 54 | handler = "/", 55 | }, 56 | }) 57 | assert.is_nil(router) 58 | assert.equal("unable to process route(index 1): invalid methond", err) 59 | end) 60 | it("new() with opts argument", function() 61 | local router, err = Router.new({}, { 62 | trailing_slash_match = true, 63 | matcher_names = { "host" }, 64 | matchers = { 65 | { 66 | match = function(route, ctx, matched) return true end 67 | } 68 | } 69 | }) 70 | assert.is_nil(err) 71 | assert.not_nil(router) 72 | assert.equal(true, router.options.trailing_slash_match) 73 | end) 74 | it("new() with invalid opts argument", function() 75 | local router, err = Router.new({}, { 76 | matchers = { { match = "" } } 77 | }) 78 | assert.is_nil(router) 79 | assert.equal("invalid args opts: invalid type matcher.match", err) 80 | 81 | local router, err = Router.new({}, { trailing_slash_match = "" }) 82 | assert.is_nil(router) 83 | assert.equal("invalid args opts: invalid type trailing_slash_match", err) 84 | 85 | local router, err = Router.new({}, { matcher_names = "" }) 86 | assert.is_nil(router) 87 | assert.equal("invalid args opts: invalid type matcher_names", err) 88 | 89 | local router, err = Router.new({}, { matcher_names = { "inexistent" } }) 90 | assert.is_nil(router) 91 | assert.equal("invalid args opts: invalid matcher name: inexistent", err) 92 | end) 93 | end) 94 | describe("match", function() 95 | it("handler can be an arbitrary value except nil", function() 96 | local tests = { 97 | { path = "/number", value = 1 }, 98 | { path = "/boolean", value = false }, 99 | { path = "/string", value = "string" }, 100 | { path = "/table", value = { key = "value" } }, 101 | { path = "/function", value = function() print("function") end }, 102 | } 103 | local routes = {} 104 | for i, test in ipairs(tests) do 105 | routes[i] = { 106 | paths = { test.path }, 107 | handler = test.value 108 | } 109 | end 110 | local router = assert(Router.new(routes)) 111 | for _, test in ipairs(tests) do 112 | local handler = router:match(test.path) 113 | assert.equal(test.value, handler) 114 | end 115 | end) 116 | describe("paths", function() 117 | describe("static path", function() 118 | it("sanity", function() 119 | local router = Router.new({ 120 | { 121 | paths = { "/a" }, 122 | handler = "1", 123 | }, 124 | { 125 | paths = { "/b" }, 126 | handler = "2", 127 | }, 128 | }) 129 | assert.equal("1", router:match("/a")) 130 | assert.equal("2", router:match("/b")) 131 | end) 132 | end) 133 | describe("variable path", function() 134 | it("sanity", function() 135 | local router = Router.new({ 136 | { 137 | paths = { "/{var1}" }, 138 | handler = "1" 139 | }, 140 | { 141 | paths = { "/aa/{var1}" }, 142 | handler = "2", 143 | }, 144 | { 145 | paths = { "/aa/{var1}/bb/{var2}" }, 146 | handler = "3", 147 | }, 148 | { 149 | paths = { "/bb/{var1}/cc/{var2}" }, 150 | handler = "4", 151 | }, 152 | { 153 | paths = { "/bb/{var1}/cc/dd" }, 154 | handler = "5", 155 | } 156 | }) 157 | 158 | assert.equal("1", router:match("/var1")) 159 | assert.equal("2", router:match("/aa/var1")) 160 | assert.equal("3", router:match("/aa/var1/bb/var2")) 161 | assert.equal("4", router:match("/bb/var1/cc/var2")) 162 | -- path /bb/var1/cc/dd overlap with handle 4 163 | assert.equal("5", router:match("/bb/var1/cc/dd")) 164 | end) 165 | end) 166 | describe("prefix path", function() 167 | it("sanity", function() 168 | local router = Router.new({ 169 | { 170 | paths = { "/{*var1}" }, 171 | handler = "1" 172 | }, 173 | { 174 | paths = { "/aa/{*var1}" }, 175 | handler = "2", 176 | }, 177 | }) 178 | assert.equal("1", router:match("/")) 179 | assert.equal("1", router:match("/aaa")) 180 | assert.equal("1", router:match("/a/b/c")) 181 | assert.equal("2", router:match("/aa/")) 182 | assert.equal("2", router:match("/aa/b")) 183 | assert.equal("2", router:match("/aa/b/c/d")) 184 | end) 185 | end) 186 | describe("mixed path", function() 187 | it("longer path has higher priority", function() 188 | local router = Router.new({ 189 | { 190 | paths = { "/a/{name}/dog{*}" }, 191 | handler = "1", 192 | }, 193 | { 194 | paths = { "/a/{name}/doge" }, 195 | handler = "2", 196 | }, 197 | }) 198 | local handler = router:match("/a/john/doge") 199 | assert.equal("2", handler) 200 | end) 201 | end) 202 | end) 203 | describe("methods", function() 204 | local router = Router.new({ 205 | { 206 | paths = { "/00/11/22" }, 207 | handler = "static", 208 | methods = { "GET" }, 209 | }, 210 | { 211 | paths = { "/00/{}/22" }, 212 | handler = "dynamic", 213 | methods = { "POST" }, 214 | }, 215 | { 216 | paths = { "/aa" }, 217 | handler = "1", 218 | methods = { "GET" } 219 | }, 220 | { 221 | paths = { "/aa" }, 222 | handler = "2", 223 | methods = { "POST" } 224 | }, 225 | { 226 | paths = { "/aa/bb{*}" }, 227 | handler = "3", 228 | methods = { "GET" } 229 | }, 230 | { 231 | paths = { "/aa/{name}" }, 232 | handler = "4", 233 | methods = { "POST" } 234 | }, 235 | { 236 | paths = { "/cc/dd{*}" }, 237 | handler = "5", 238 | methods = { "GET" } 239 | }, 240 | { 241 | paths = { "/cc/{p1}/ee/{p2}" }, 242 | handler = "6", 243 | methods = { "POST" }, 244 | }, 245 | { 246 | paths = { "/cc/{p1}/ee/p2{*}" }, 247 | handler = "7", 248 | methods = { "PUT" }, 249 | }, 250 | { 251 | paths = { "/cc/{p1}/ee/{p2}/ff" }, 252 | handler = "8", 253 | methods = { "PATCH" }, 254 | }, 255 | { 256 | paths = { "/dd/{p1}/ee{*}" }, 257 | handler = "9", 258 | methods = { "GET" }, 259 | }, 260 | { 261 | paths = { "/dd/{p1}/ee" }, 262 | handler = "10", 263 | methods = { "POST" }, 264 | }, 265 | { 266 | paths = { "/ee{*}" }, 267 | methods = { "GET" }, 268 | handler = "11", 269 | }, 270 | { 271 | paths = { "/ee/ff{*}" }, 272 | methods = { "POST" }, 273 | handler = "12", 274 | }, 275 | }) 276 | it("sanity", function() 277 | assert.equal("1", router:match("/aa", { method = "GET" })) 278 | assert.equal("2", router:match("/aa", { method = "POST" })) 279 | assert.equal("3", router:match("/aa/bb", { method = "GET" })) 280 | assert.equal("4", router:match("/aa/name", { method = "POST" })) 281 | assert.equal("5", router:match("/cc/dd", { method = "GET" })) 282 | assert.equal("6", router:match("/cc/p1/ee/p2", { method = "POST" })) 283 | assert.equal("7", router:match("/cc/p1/ee/p2x", { method = "PUT" })) 284 | assert.equal("8", router:match("/cc/p1/ee/p2/ff", { method = "PATCH" })) 285 | end) 286 | it("path matches multiple routes", function() 287 | assert.equal("static", router:match("/00/11/22", { method = "GET" })) 288 | assert.equal("dynamic", router:match("/00/11/22", { method = "POST" })) 289 | -- path matches handler3 and handler4 290 | assert.equal("4", router:match("/aa/bb", { method = "POST" })) 291 | -- path matches handler5 and handler6 and handler7 292 | assert.equal("5", router:match("/cc/dd/ee/p2", { method = "GET" })) 293 | assert.equal("6", router:match("/cc/dd/ee/p2", { method = "POST" })) 294 | assert.equal("7", router:match("/cc/dd/ee/p2", { method = "PUT" })) 295 | -- path matches handler5 and handler7 and handler8 296 | assert.equal("5", router:match("/cc/dd/ee/p2/ff", { method = "GET" })) 297 | assert.equal("7", router:match("/cc/dd/ee/p2/ff", { method = "PUT" })) 298 | assert.equal("8", router:match("/cc/dd/ee/p2/ff", { method = "PATCH" })) 299 | 300 | assert.equal("9", router:match("/dd/p1/ee", { method = "GET" })) 301 | assert.equal("11", router:match("/ee/bb", { method = "GET" })) 302 | end) 303 | end) 304 | describe("priority", function() 305 | it("highest priority first match", function() 306 | local router = Router.new({ 307 | { 308 | paths = { "/static" }, 309 | handler = "1", 310 | priority = 1, 311 | }, 312 | { 313 | paths = { "/static" }, 314 | handler = "2", 315 | priority = 2, 316 | }, 317 | { 318 | paths = { "/param/{name}" }, 319 | handler = "3", 320 | priority = 1, 321 | }, 322 | { 323 | paths = { "/param/{name}" }, 324 | handler = "4", 325 | priority = 2, 326 | }, 327 | { 328 | paths = { "/prefix{*}" }, 329 | handler = "5", 330 | priority = 1, 331 | }, 332 | { 333 | paths = { "/prefix{*}" }, 334 | handler = "6", 335 | priority = 2, 336 | }, 337 | }) 338 | assert.equal("2", router:match("/static")) 339 | assert.equal("4", router:match("/param/name")) 340 | assert.equal("6", router:match("/prefix")) 341 | end) 342 | end) 343 | describe("hosts", function() 344 | it("exact host", function() 345 | local router = Router.new({ 346 | { 347 | paths = { "/single-host" }, 348 | handler = "1", 349 | hosts = { "example.com" } 350 | }, 351 | { 352 | paths = { "/multiple-hosts" }, 353 | handler = "2", 354 | hosts = { "foo.com", "bar.com" } 355 | }, 356 | }) 357 | assert.equal("1", router:match("/single-host", { host = "example.com" })) 358 | assert.equal(nil, router:match("/single-host", { host = "www.example.com" })) 359 | assert.equal(nil, router:match("/single-host", { host = "example1.com" })) 360 | assert.equal(nil, router:match("/single-host", { host = ".com" })) 361 | 362 | assert.equal("2", router:match("/multiple-hosts", { host = "foo.com" })) 363 | assert.equal("2", router:match("/multiple-hosts", { host = "bar.com" })) 364 | assert.equal(nil, router:match("/multiple-hosts", { host = "example.com" })) 365 | end) 366 | it("wildcard host", function() 367 | local router = Router.new({ 368 | { 369 | paths = { "/" }, 370 | handler = "1", 371 | hosts = { "*.example.com" } 372 | }, 373 | { 374 | paths = { "/" }, 375 | handler = "2", 376 | hosts = { "www.example.*" } 377 | }, 378 | { 379 | paths = { "/multiple-hosts" }, 380 | handler = "3", 381 | hosts = { "foo.com", "*.foo.com", "*.bar.com" } 382 | }, 383 | }) 384 | 385 | assert.equal("1", router:match("/", { host = "www.example.com" })) 386 | assert.equal("1", router:match("/", { host = "foo.bar.example.com" })) 387 | assert.equal(nil, router:match("/", { host = ".example.com" })) 388 | 389 | assert.equal("2", router:match("/", { host = "www.example.org" })) 390 | assert.equal("2", router:match("/", { host = "www.example.foo.bar" })) 391 | assert.equal(nil, router:match("/", { host = "www.example." })) 392 | 393 | assert.equal("3", router:match("/multiple-hosts", { host = "foo.com" })) 394 | assert.equal("3", router:match("/multiple-hosts", { host = "www.foo.com" })) 395 | assert.equal("3", router:match("/multiple-hosts", { host = "www.bar.com" })) 396 | end) 397 | it("host value is case-sensitive", function() 398 | local router = Router.new({ 399 | { 400 | paths = { "/" }, 401 | hosts = { "example.com" }, 402 | handler = "1", 403 | } 404 | }) 405 | assert.equal("1", router:match("/", { host = "example.com" })) 406 | assert.equal(nil, router:match("/", { host = "examplE.com" })) 407 | end) 408 | end) 409 | end) 410 | describe("match with params binding", function() 411 | it("sanity", function() 412 | local router = Router.new({ 413 | { 414 | paths = { "/{var}" }, 415 | handler = "0", 416 | }, 417 | { 418 | paths = { "/{var1}/{var2}/" }, 419 | handler = "1", 420 | }, 421 | { 422 | paths = { "/aa/{var1}/cc/{var2}" }, 423 | handler = "2", 424 | }, 425 | { 426 | paths = { "/bb/{*path}" }, 427 | handler = "3", 428 | }, 429 | { 430 | paths = { "/cc/{var}/dd/{*path}" }, 431 | handler = "4", 432 | }, 433 | { 434 | paths = { "/cc/{*path}" }, 435 | handler = "5", 436 | }, 437 | { 438 | paths = { "/dd{*cat}" }, 439 | handler = "6", 440 | }, 441 | { 442 | paths = { "/dd{*dog}" }, 443 | handler = "7", 444 | }, 445 | }) 446 | local ctx = {} 447 | local binding 448 | 449 | binding = {} 450 | assert.equal("0", router:match("/var", ctx, binding)) 451 | assert.same({ var = "var" }, binding) 452 | 453 | binding = {} 454 | assert.equal(nil, router:match("/var1/", ctx, binding)) 455 | assert.equal("1", router:match("/var11111/var222222/", ctx, binding)) 456 | assert.same({ var1 = "var11111", var2 = "var222222" }, binding) 457 | 458 | binding = {} 459 | assert.equal("2", router:match("/aa/var1/cc/var2", ctx, binding)) 460 | assert.same({ var1 = "var1", var2 = "var2" }, binding) 461 | 462 | binding = {} 463 | assert.equal("3", router:match("/bb/", ctx, binding)) 464 | assert.same({ path = "" }, binding) 465 | 466 | binding = {} 467 | assert.equal("3", router:match("/bb/a/b/c/", ctx, binding)) 468 | assert.same({ path = "a/b/c/" }, binding) 469 | 470 | binding = {} 471 | assert.equal("4", router:match("/cc/var/dd/", ctx, binding)) 472 | assert.same({ var = "var", path = "" }, binding) 473 | 474 | binding = {} 475 | assert.equal("4", router:match("/cc/var/dd/a/b/c", ctx, binding)) 476 | assert.same({ var = "var", path = "a/b/c" }, binding) 477 | 478 | binding = {} 479 | assert.equal("6", router:match("/ddsuffix", ctx, binding)) 480 | assert.same({ cat = "suffix" }, binding) 481 | end) 482 | end) 483 | describe("regex", function() 484 | it("sanity", function() 485 | local router = Router.new({ 486 | { 487 | paths = { "/a/{b:\\d{3}}/c" }, 488 | handler = "/a/{b:\\d{3}}/c", 489 | }, 490 | { 491 | paths = { "/a/{b:\\d+}/c" }, 492 | handler = "/a/{b:\\d+}/c", 493 | }, 494 | { 495 | paths = { "/a/{b:[a-z]+}/c" }, 496 | handler = "/a/{b:[a-z]+}/c", 497 | }, 498 | { 499 | paths = { "/a/{b:[^/]+}/c" }, 500 | handler = "/a/{b:[^/]+}/c", 501 | }, 502 | { 503 | paths = { "/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" }, 504 | handler = "1" 505 | }, 506 | { 507 | paths = { "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}" }, 508 | handler = "2", 509 | }, 510 | { 511 | paths = { "/escape/{var}/{var1:[a-z]+}|{var2:[A-Z]+}|{var3:\\d+}|{var4:(html|pdf)}" }, 512 | handler = "3", 513 | }, 514 | }) 515 | assert.equal("/a/{b:\\d+}/c", router:match("/a/2024/c")) 516 | assert.equal("/a/{b:\\d{3}}/c", router:match("/a/123/c")) 517 | assert.equal("/a/{b:[a-z]+}/c", router:match("/a/abc/c")) 518 | assert.equal("/a/{b:[^/]+}/c", router:match("/a/abc0/c")) 519 | 520 | -- /users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}} 521 | assert.equal("1", router:match("/users/48c3d30b-ef4a-46ce-b860-a2c445f27b93")) 522 | assert.equal("1", router:match("/users/00000000-0000-0000-0000-000000000000")) 523 | assert.equal(nil, router:match("/users/0abcdefg")) 524 | assert.equal(nil, router:match("/users/00000000-0000-0000-0000-00000000000")) 525 | 526 | -- /users/{id:\\d+}/profile-{year:\\d{4}}.{format:html|pdf} 527 | assert.equal("2", router:match("/users/123/profile-2024.html")) 528 | assert.equal("2", router:match("/users/123/profile-2024.pdf")) 529 | assert.equal(nil, router:match("/users/abc/profile-2024.html")) 530 | assert.equal(nil, router:match("/users/123/profile-123.html")) 531 | assert.equal(nil, router:match("/users/123/profile-2024.jpg")) 532 | 533 | -- /escape/{var}/{var1:[a-z]+}|{var2:[A-Z]+}|{var3:\\d+}|{var4:(html|pdf)} 534 | assert.equal("3", router:match("/escape/var/aaa|AAA|111|html")) 535 | end) 536 | end) 537 | describe("matching order", function() 538 | it("first registered first match", function() 539 | local router = Router.new({ 540 | { 541 | paths = { "/static" }, 542 | handler = "1", 543 | }, 544 | { 545 | paths = { "/static" }, 546 | handler = "2", 547 | }, 548 | { 549 | paths = { "/param/{name}" }, 550 | handler = "3", 551 | }, 552 | { 553 | paths = { "/param/{name}" }, 554 | handler = "4", 555 | }, 556 | { 557 | paths = { "/prefix{*}" }, 558 | handler = "5", 559 | }, 560 | { 561 | paths = { "/prefix{*}" }, 562 | handler = "6", 563 | }, 564 | }) 565 | assert.equal("1", router:match("/static")) 566 | assert.equal("3", router:match("/param/name")) 567 | assert.equal("5", router:match("/prefix")) 568 | end) 569 | it("static > dynamic", function() 570 | local router = Router.new({ 571 | { 572 | paths = { "/aa/bb" }, 573 | handler = "1", 574 | }, 575 | { 576 | paths = { "/aa/bb{*path}" }, 577 | handler = "2", 578 | }, 579 | { 580 | paths = { "/aa/{name}" }, 581 | handler = "3", 582 | } 583 | }) 584 | assert.equal("1", router:match("/aa/bb")) 585 | end) 586 | end) 587 | describe("match with matched", function() 588 | it("sanity", function() 589 | local router = Router.new({ 590 | { 591 | paths = { "/static" }, 592 | methods = { "GET" }, 593 | hosts = { "example.com" }, 594 | handler = "1", 595 | }, 596 | { 597 | paths = { "/a/{var}", "/b/{var}" }, 598 | methods = { "POST", "PUT" }, 599 | hosts = { "*.example.com" }, 600 | handler = "2", 601 | }, 602 | }) 603 | 604 | local matched = {} 605 | assert.equal("1", router:match("/static", { method = "GET", host = "example.com" }, nil, matched)) 606 | assert.same({ 607 | path = "/static", 608 | host = "example.com", 609 | method = "GET", 610 | }, matched) 611 | 612 | local matched = {} 613 | assert.equal("2", router:match("/b/var", { method = "PUT", host = "www.example.com" }, nil, matched)) 614 | assert.same({ 615 | path = "/b/{var}", 616 | host = "*.example.com", 617 | method = "PUT", 618 | }, matched) 619 | end) 620 | end) 621 | describe("trailing slash match", function() 622 | it("sanity", function() 623 | local options = { 624 | trailing_slash_match = true, 625 | } 626 | 627 | local router = Router.new({ 628 | -- static routes 629 | { 630 | paths = { "/static1" }, 631 | handler = "static1", 632 | }, 633 | { 634 | paths = { "/static2/" }, 635 | handler = "static2", 636 | }, 637 | -- variable in the middle 638 | { 639 | paths = { "/users/{id}/profile" }, 640 | handler = "0", 641 | }, 642 | { 643 | paths = { "/pets/{id}/profile/" }, 644 | handler = "1", 645 | }, 646 | -- variable at the end 647 | { 648 | paths = { "/zz/{id}" }, 649 | handler = "2", 650 | }, 651 | { 652 | paths = { "/ww/{id}/" }, 653 | handler = "3", 654 | }, 655 | { 656 | paths = { "/aa/{var1}" }, 657 | handler = "4", 658 | }, 659 | { 660 | paths = { "/aa/{var1}/bb" }, 661 | handler = "5", 662 | } 663 | }, options) 664 | 665 | assert.equal("static1", router:match("/static1/")) 666 | assert.equal("static2", router:match("/static2")) 667 | 668 | -- matched when path has a extra trailing slash 669 | assert.equal("0", router:match("/users/1/profile/")) 670 | -- matched when path misses trailing slash 671 | assert.equal("1", router:match("/pets/1/profile")) 672 | 673 | -- matched when path has a extra trailing slash 674 | assert.equal("2", router:match("/zz/1/")) 675 | -- matched when path misses trailing slash 676 | 677 | assert.equal("3", router:match("/ww/1")) 678 | 679 | assert.equal("4", router:match("/aa/var1")) 680 | assert.equal("5", router:match("/aa/var1/bb")) 681 | end) 682 | it("with methods condition", function() 683 | local options = { 684 | trailing_slash_match = true, 685 | } 686 | 687 | local router = Router.new({ 688 | { 689 | paths = { "/a/{var}" }, 690 | methods = { "GET" }, 691 | handler = "10", 692 | }, 693 | { 694 | paths = { "/a/{var}/" }, 695 | methods = { "POST" }, 696 | handler = "20", 697 | }, 698 | { 699 | paths = { "/bb/{var}/foo" }, 700 | methods = { "GET" }, 701 | handler = "30", 702 | }, 703 | { 704 | paths = { "/bb/{var}/bar" }, 705 | methods = { "GET" }, 706 | handler = "40", 707 | }, 708 | }, options) 709 | 710 | -- exact match 711 | assert.equal("10", router:match("/a/var", { method = "GET" })) 712 | -- matched when path has a extra trailing slash 713 | assert.equal("10", router:match("/a/var/", { method = "GET" })) 714 | 715 | -- exact match 716 | assert.equal("20", router:match("/a/var/", { method = "POST" })) 717 | -- matched when path misses trailing slash 718 | assert.equal("20", router:match("/a/var", { method = "POST" })) 719 | 720 | -- the prepreerence for path /a/var is handle 10, but the method is not matched 721 | assert.equal("20", router:match("/a/var", { method = "POST" })) 722 | -- the prepreerence for path /a/var/ is handle 20, but the method is not matched 723 | assert.equal("10", router:match("/a/var/", { method = "GET" })) 724 | 725 | assert.equal("30", router:match("/bb/var/foo/", { method = "GET" })) 726 | assert.equal("40", router:match("/bb/var/bar/", { method = "GET" })) 727 | end) 728 | end) 729 | describe("custom matchers", function() 730 | it("disable all of the built-in matcher", function() 731 | local opts = { 732 | matcher_names = {} 733 | } 734 | local router = Router.new({ 735 | { 736 | paths = { "/" }, 737 | methods = { "GET" }, 738 | hosts = { "www.a.com" }, 739 | handler = "1", 740 | }, 741 | }, opts) 742 | assert.equal("1", router:match("/")) 743 | end) 744 | it("disable some of the built-in matcher", function() 745 | local opts = { 746 | matcher_names = { "method" } 747 | } 748 | local router = Router.new({ 749 | { 750 | paths = { "/" }, 751 | methods = { "GET" }, 752 | hosts = { "www.a.com" }, 753 | handler = "1", 754 | }, 755 | }, opts) 756 | assert.equal("1", router:match("/", { method = "GET", host = "no-matter-what" })) 757 | end) 758 | it("custom matcher", function() 759 | local ip_matcher = { 760 | process = function(route) 761 | if route.ips then 762 | local ips = {} 763 | for _, ip in ipairs(route.ips) do 764 | ips[ip] = true 765 | end 766 | route.ips = ips 767 | end 768 | end, 769 | match = function(route, ctx, matched) 770 | if route.ips then 771 | local ip = ctx.ip 772 | if not route.ips[ip] then 773 | return false 774 | end 775 | if matched then 776 | matched["ip"] = ip 777 | end 778 | end 779 | return true 780 | end 781 | } 782 | local opts = { 783 | matchers = { ip_matcher } 784 | } 785 | 786 | local router = Router.new({ 787 | { 788 | paths = { "/" }, 789 | ips = { "127.0.0.1", "127.0.0.2" }, 790 | handler = "1", 791 | }, 792 | { 793 | paths = { "/" }, 794 | ips = { "192.168.1.1", "192.168.1.2" }, 795 | handler = "2", 796 | } 797 | }, opts) 798 | assert.equal("1", router:match("/", { ip = "127.0.0.2" })) 799 | assert.equal("2", router:match("/", { ip = "192.168.1.2" })) 800 | -- with matched 801 | local matched = {} 802 | assert.equal("2", router:match("/", { ip = "192.168.1.2" }, nil, matched)) 803 | assert.equal("192.168.1.2", matched.ip) 804 | end) 805 | end) 806 | end) 807 | -------------------------------------------------------------------------------- /spec/utils_spec.lua: -------------------------------------------------------------------------------- 1 | require 'busted.runner'() 2 | 3 | local utils = require "radix-router.utils" 4 | 5 | describe("utils", function() 6 | it("starts_with()", function() 7 | assert.is_true(utils.starts_with("/abc", "")) 8 | assert.is_true(utils.starts_with("/abc", "/")) 9 | assert.is_true(utils.starts_with("/abc", "/a")) 10 | assert.is_true(utils.starts_with("/abc", "/ab")) 11 | assert.is_true(utils.starts_with("/abc", "/abc")) 12 | 13 | assert.is_false(utils.starts_with("/abc", "/abcd")) 14 | assert.is_false(utils.starts_with("/abc", "/d")) 15 | end) 16 | it("ends_with()", function() 17 | assert.is_true(utils.ends_with("/abc", "")) 18 | assert.is_true(utils.ends_with("/abc", "c")) 19 | assert.is_true(utils.ends_with("/abc", "bc")) 20 | assert.is_true(utils.ends_with("/abc", "abc")) 21 | assert.is_true(utils.ends_with("/abc", "/abc")) 22 | 23 | assert.is_false(utils.ends_with("/abc", "0abc")) 24 | assert.is_false(utils.ends_with("/abc", "d")) 25 | 26 | assert.is_true(utils.ends_with("example.com", ".com")) 27 | assert.is_true(utils.ends_with("example.com", "*.com", nil, nil, 1)) 28 | assert.is_true(utils.ends_with("example.com", "*.com", nil, nil, 5)) 29 | end) 30 | it("lcp()", function() 31 | assert.equal(0, utils.lcp("/abc", "")) 32 | assert.equal(1, utils.lcp("/abc", "/")) 33 | assert.equal(2, utils.lcp("/abc", "/a")) 34 | assert.equal(4, utils.lcp("/abc", "/abc")) 35 | assert.equal(4, utils.lcp("/abc", "/abcd")) 36 | assert.equal(0, utils.lcp("", "/abcd")) 37 | assert.equal(0, utils.lcp("a", "c")) 38 | end) 39 | end) 40 | -------------------------------------------------------------------------------- /src/constants.lua: -------------------------------------------------------------------------------- 1 | --- Constants constants values 2 | -- 3 | -- 4 | 5 | return { 6 | -- The type of TrieNode 7 | node_types = { 8 | literal = 0, 9 | variable = 1, 10 | catchall = 2, 11 | }, 12 | node_indexs = { 13 | type = 1, 14 | path = 2, 15 | pathn = 3, 16 | children = 4, 17 | value = 5, 18 | }, 19 | -- The type of token 20 | token_types = { 21 | literal = 0, 22 | variable = 1, 23 | catchall = 2, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/iterator.lua: -------------------------------------------------------------------------------- 1 | --- Iterator an iterator for iterating radix tree and storing states. 2 | -- 3 | -- 4 | 5 | local utils = require "radix-router.utils" 6 | local constants = require "radix-router.constants" 7 | 8 | local starts_with = utils.starts_with 9 | local str_sub = string.sub 10 | local str_char = string.char 11 | local str_byte = string.byte 12 | 13 | local BYTE_SLASH = str_byte("/") 14 | local TYPE_VARIABLE = constants.node_types.variable 15 | local TYPE_CATCHALL = constants.node_types.catchall 16 | 17 | local _M = {} 18 | local mt = { __index = _M } 19 | 20 | --[[ A copy of node indexs 21 | local i_type = 1 22 | local i_path = 2 23 | local i_pathn = 3 24 | local i_children = 4 25 | local i_value = 5 26 | ]] 27 | 28 | function _M.new(options) 29 | local self = { 30 | trailing_slash_match = options.trailing_slash_match, 31 | stack_node = utils.new_table(4, 0), 32 | stack_paths = utils.new_table(4, 0), 33 | stack_pathns = utils.new_table(4, 0), 34 | stack_n = 0, 35 | values = utils.new_table(4, 0), 36 | } 37 | 38 | return setmetatable(self, mt) 39 | end 40 | 41 | 42 | function _M:push(node, path, path_n) 43 | local stack_n = self.stack_n + 1 44 | self.stack_node[stack_n] = node 45 | self.stack_paths[stack_n] = path 46 | self.stack_pathns[stack_n] = path_n 47 | self.stack_n = stack_n 48 | end 49 | 50 | 51 | function _M:prev() 52 | if self.stack_n == 0 then 53 | return nil 54 | end 55 | -- pop a state from stack 56 | local stack_n = self.stack_n 57 | local path = self.stack_paths[stack_n] 58 | local path_n = self.stack_pathns[stack_n] 59 | local node = self.stack_node[stack_n] 60 | self.stack_n = stack_n - 1 61 | return node, path, path_n 62 | end 63 | 64 | 65 | function _M:reset() 66 | self.stack_n = 0 67 | end 68 | 69 | 70 | function _M:find(node, path, path_n) 71 | local child 72 | local node_path, node_path_n 73 | local first_char 74 | local has_variable 75 | local matched_n = 0 76 | local trailing_slash_match = self.trailing_slash_match 77 | 78 | -- luacheck: ignore 79 | while true do 80 | ::continue:: 81 | if node[1] == TYPE_VARIABLE then 82 | local not_found = true 83 | local i = 0 84 | for n = 1, path_n do 85 | first_char = str_byte(path, n) 86 | if first_char == BYTE_SLASH or 87 | (node[4] and node[4][str_char(first_char)]) then 88 | break 89 | end 90 | i = n 91 | end 92 | if i < path_n then 93 | path = str_sub(path, i + 1) 94 | path_n = path_n - i 95 | if trailing_slash_match and path == "/" and node[5] then 96 | -- matched when path has a extra slash 97 | matched_n = matched_n + 1 98 | self.values[matched_n] = node[5] 99 | end 100 | if node[4] then 101 | first_char = str_sub(path, 1, 1) 102 | child = node[4][first_char] 103 | if child then 104 | -- found static node that matches the path 105 | node = child 106 | not_found = false 107 | end 108 | end 109 | elseif node[5] then 110 | -- the path is variable 111 | matched_n = matched_n + 1 112 | self.values[matched_n] = node[5] 113 | end 114 | 115 | -- case1: the node doesn't contians child to match to the path 116 | -- case2: the path is variable value, but current node doesn't have value 117 | if not_found then 118 | if trailing_slash_match and node[4] then 119 | -- look up the children to see if "/" child with value exists 120 | child = node[4]["/"] 121 | if child and child[2] == "/" and child[5] then 122 | matched_n = matched_n + 1 123 | self.values[matched_n] = child[5] 124 | end 125 | end 126 | 127 | break 128 | end 129 | end 130 | 131 | 132 | -- the node must be a literal node 133 | node_path = node[2] 134 | node_path_n = node[3] 135 | 136 | if path_n > node_path_n then 137 | if starts_with(path, node_path, path_n, node_path_n) then 138 | path = str_sub(path, node_path_n + 1) 139 | path_n = path_n - node_path_n 140 | 141 | child = node[4] and node[4][TYPE_CATCHALL] 142 | if child then 143 | matched_n = matched_n + 1 144 | self.values[matched_n] = child[5] 145 | end 146 | 147 | has_variable = false 148 | child = node[4] and node[4][TYPE_VARIABLE] 149 | if child then 150 | -- node has a variable child, but we don't know whether 151 | -- the path can finally match the path. 152 | -- therefore, record the state(node, path, path_n) to be used later. 153 | self:push(child, path, path_n) 154 | has_variable = true 155 | end 156 | 157 | first_char = str_sub(path, 1, 1) 158 | child = node[4] and node[4][first_char] 159 | if child then 160 | -- found static node that matches the path 161 | node = child 162 | goto continue 163 | end 164 | 165 | if has_variable then 166 | node = self:prev() 167 | goto continue 168 | end 169 | 170 | if trailing_slash_match and path == "/" and node[5] then 171 | matched_n = matched_n + 1 172 | self.values[matched_n] = node[5] 173 | end 174 | end 175 | elseif path == node_path then 176 | -- considers matched if this node has catchall child 177 | child = node[4] and node[4][TYPE_CATCHALL] 178 | if child then 179 | matched_n = matched_n + 1 180 | self.values[matched_n] = child[5] 181 | end 182 | 183 | if node[5] then 184 | matched_n = matched_n + 1 185 | self.values[matched_n] = node[5] 186 | end 187 | else 188 | -- #path < #node_path 189 | if trailing_slash_match and path_n == node_path_n - 1 190 | and str_byte(node_path, node_path_n) == BYTE_SLASH and node[5] then 191 | matched_n = matched_n + 1 192 | self.values[matched_n] = node[5] 193 | end 194 | end 195 | 196 | break 197 | end 198 | 199 | if matched_n > 0 then 200 | return self.values, matched_n 201 | end 202 | 203 | return nil, 0 204 | end 205 | 206 | 207 | return _M 208 | -------------------------------------------------------------------------------- /src/matcher/host.lua: -------------------------------------------------------------------------------- 1 | --- HostMatcher 2 | 3 | local utils = require "radix-router.utils" 4 | 5 | local ipairs = ipairs 6 | local str_byte = string.byte 7 | local starts_with = utils.starts_with 8 | local ends_with = utils.ends_with 9 | 10 | local BYTE_ASTERISK = str_byte("*") 11 | 12 | local _M = {} 13 | 14 | function _M.process(route) 15 | if route.hosts then 16 | local hosts = { [0] = 0 } 17 | for _, host in ipairs(route.hosts) do 18 | local host_n = #host 19 | local wildcard_n = 0 20 | for n = 1, host_n do 21 | if str_byte(host, n) == BYTE_ASTERISK then 22 | wildcard_n = wildcard_n + 1 23 | end 24 | end 25 | if wildcard_n > 1 then 26 | return nil, "invalid host" 27 | elseif wildcard_n == 1 then 28 | local n = hosts[0] + 1 29 | hosts[0] = n 30 | hosts[n] = host -- wildcard host 31 | else 32 | hosts[host] = true 33 | end 34 | end 35 | route.hosts = hosts 36 | end 37 | end 38 | 39 | function _M.match(route, ctx, matched) 40 | if route.hosts then 41 | local host = ctx.host 42 | if not host then 43 | return false 44 | end 45 | if not route.hosts[host] then 46 | if route.hosts[0] == 0 then 47 | return false 48 | end 49 | 50 | local wildcard_match = false 51 | local host_n = #host 52 | local wildcard_host, wildcard_host_n 53 | for i = 1, route.hosts[0] do 54 | wildcard_host = route.hosts[i] 55 | wildcard_host_n = #wildcard_host 56 | if host_n >= wildcard_host_n then 57 | if str_byte(wildcard_host) == BYTE_ASTERISK then 58 | -- case *.example.com 59 | if ends_with(host, wildcard_host, host_n, wildcard_host_n, 1) then 60 | wildcard_match = true 61 | break 62 | end 63 | else 64 | -- case example.* 65 | if starts_with(host, wildcard_host, host_n, wildcard_host_n - 1) then 66 | wildcard_match = true 67 | break 68 | end 69 | end 70 | end 71 | end 72 | if not wildcard_match then 73 | return false 74 | end 75 | if matched then 76 | matched.host = wildcard_host 77 | end 78 | else 79 | if matched then 80 | matched.host = host 81 | end 82 | end 83 | end 84 | 85 | return true 86 | end 87 | 88 | return _M -------------------------------------------------------------------------------- /src/matcher/matcher.lua: -------------------------------------------------------------------------------- 1 | --- Matcher 2 | -- 3 | 4 | local utils = require "radix-router.utils" 5 | 6 | local ipairs = ipairs 7 | local EMPTY = utils.readonly({}) 8 | 9 | local Matcher = {} 10 | local mt = { __index = Matcher } 11 | 12 | local DEFAULTS = { 13 | ["method"] = require("radix-router.matcher.method"), 14 | ["host"] = require("radix-router.matcher.host"), 15 | } 16 | 17 | 18 | function Matcher.new(enabled_names, custom_matchers) 19 | local chain = {} 20 | 21 | for _, matcher in ipairs(custom_matchers or EMPTY) do 22 | table.insert(chain, matcher) 23 | end 24 | 25 | for _, name in ipairs(enabled_names or EMPTY) do 26 | local matcher = DEFAULTS[name] 27 | if not matcher then 28 | return nil, "invalid matcher name: " .. name 29 | end 30 | table.insert(chain, matcher) 31 | end 32 | 33 | return setmetatable({ 34 | chain = chain, 35 | chain_n = #chain, 36 | }, mt) 37 | end 38 | 39 | 40 | function Matcher:process(route) 41 | for _, matcher in ipairs(self.chain) do 42 | if type(matcher.process) == "function" then 43 | local err = matcher.process(route) 44 | if err then 45 | return nil, err 46 | end 47 | end 48 | end 49 | return true 50 | end 51 | 52 | 53 | function Matcher:match(route, ctx, matched) 54 | for i = 1, self.chain_n do 55 | local matcher = self.chain[i] 56 | if not matcher.match(route, ctx, matched) then 57 | return false 58 | end 59 | end 60 | return true 61 | end 62 | 63 | 64 | return Matcher -------------------------------------------------------------------------------- /src/matcher/method.lua: -------------------------------------------------------------------------------- 1 | --- MethodMatcher 2 | 3 | local utils = require "radix-router.utils" 4 | local bit = utils.is_luajit and require "bit" 5 | 6 | local is_luajit = utils.is_luajit 7 | local METHODS = {} 8 | do 9 | local methods = { "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH" } 10 | for i, method in ipairs(methods) do 11 | if is_luajit then 12 | METHODS[method] = bit.lshift(1, i - 1) 13 | else 14 | METHODS[method] = true 15 | end 16 | end 17 | end 18 | 19 | 20 | local _M = {} 21 | 22 | 23 | function _M.process(route) 24 | if route.methods then 25 | local methods_bit = 0 26 | local methods = {} 27 | for _, method in ipairs(route.methods) do 28 | if not METHODS[method] then 29 | return "invalid methond" 30 | end 31 | if is_luajit then 32 | methods_bit = bit.bor(methods_bit, METHODS[method]) 33 | else 34 | methods[method] = true 35 | end 36 | end 37 | route.method = is_luajit and methods_bit or methods 38 | end 39 | end 40 | 41 | 42 | function _M.match(route, ctx, matched) 43 | if route.method then 44 | local method = ctx.method 45 | if not method or METHODS[method] == nil then 46 | return false 47 | end 48 | if is_luajit then 49 | if bit.band(route.method, METHODS[method]) == 0 then 50 | return false 51 | end 52 | else 53 | if not route.method[method] then 54 | return false 55 | end 56 | end 57 | 58 | if matched then 59 | matched.method = method 60 | end 61 | end 62 | 63 | return true 64 | end 65 | 66 | 67 | return _M -------------------------------------------------------------------------------- /src/options.lua: -------------------------------------------------------------------------------- 1 | --- Options Router options 2 | -- 3 | 4 | 5 | local function options(opts) 6 | opts = opts or {} 7 | 8 | local default = { 9 | matcher_names = { "method", "host" }, 10 | matchers = {}, 11 | trailing_slash_match = false, 12 | } 13 | 14 | if opts.trailing_slash_match ~= nil then 15 | if type(opts.trailing_slash_match) ~= "boolean" then 16 | return nil, "invalid type trailing_slash_match" 17 | end 18 | default.trailing_slash_match = opts.trailing_slash_match 19 | end 20 | 21 | if opts.matcher_names ~= nil then 22 | if type(opts.matcher_names) ~= "table" then 23 | return nil, "invalid type matcher_names" 24 | end 25 | default.matcher_names = opts.matcher_names 26 | end 27 | 28 | if opts.matchers ~= nil then 29 | for _, matcher in ipairs(opts.matchers) do 30 | if type(matcher.match) ~= "function" then 31 | return nil, "invalid type matcher.match" 32 | end 33 | end 34 | 35 | default.matchers = opts.matchers 36 | end 37 | 38 | return default 39 | end 40 | 41 | 42 | return { 43 | options = options, 44 | } 45 | -------------------------------------------------------------------------------- /src/parser/parser.lua: -------------------------------------------------------------------------------- 1 | --- Parser path parser that parse a pattern path. 2 | -- 3 | -- 4 | 5 | local Parser = {} 6 | 7 | local parsers = { 8 | ["default"] = require "radix-router.parser.style.default", 9 | } 10 | 11 | -- return a parser instance 12 | function Parser.new(style) 13 | local parser = parsers[style] 14 | if not parser then 15 | return nil, "unknown parser style: " .. style 16 | end 17 | 18 | return parser.new() 19 | end 20 | 21 | 22 | return Parser 23 | -------------------------------------------------------------------------------- /src/parser/style/default.lua: -------------------------------------------------------------------------------- 1 | --- Default style path parser. 2 | -- 3 | -- Parses the path into multiple tokens with patterns. 4 | -- 5 | -- patterns 6 | -- - `{name}`: variable parameter 7 | -- - `{*name}`: catch-all parameter 8 | 9 | local constants = require "radix-router.constants" 10 | 11 | local byte = string.byte 12 | local sub = string.sub 13 | 14 | local BYTE_COLON = byte(":") 15 | local BYTE_ASTERISK = byte("*") 16 | local BYTE_LEFT_BRACKET = byte("{") 17 | local BYTE_RIGHT_BRACKET = byte("}") 18 | local BYTE_SLASH = byte("/") 19 | 20 | local TOKEN_TYPES = constants.token_types 21 | 22 | local _M = {} 23 | local mt = { __index = _M } 24 | 25 | local STATES = { 26 | static = 1, 27 | variable_start = 2, 28 | variable_end = 3, 29 | finish = 4, 30 | } 31 | 32 | 33 | function _M.new() 34 | return setmetatable({}, mt) 35 | end 36 | 37 | 38 | function _M:update(path) 39 | self.path = path 40 | self.path_n = #path 41 | self:reset() 42 | return self 43 | end 44 | 45 | 46 | function _M:reset() 47 | self.anchor = 1 48 | self.pos = 1 49 | self.state = nil 50 | self.bracket_depth = 0 51 | end 52 | 53 | 54 | function _M:next() 55 | if self.state == STATES.finish then 56 | return nil 57 | end 58 | 59 | local char, token, token_type 60 | while self.pos <= self.path_n do 61 | char = byte(self.path, self.pos) 62 | --local char_str = string.char(char) 63 | --print("pos: " .. self.pos .. "(" .. char_str .. ")") 64 | if self.state == nil or self.state == STATES.static then 65 | if char == BYTE_LEFT_BRACKET then 66 | if self.state == STATES.static then 67 | token = sub(self.path, self.anchor, self.pos - 1) 68 | token_type = self.token_type(token) 69 | self.anchor = self.pos 70 | end 71 | self.state = STATES.variable_start 72 | self.bracket_depth = 1 73 | else 74 | self.state = STATES.static 75 | end 76 | elseif self.state == STATES.variable_start then 77 | if char == BYTE_LEFT_BRACKET then 78 | self.bracket_depth = self.bracket_depth + 1 79 | elseif char == BYTE_RIGHT_BRACKET then 80 | self.bracket_depth = self.bracket_depth - 1 81 | if self.bracket_depth == 0 then 82 | self.state = STATES.variable_end 83 | end 84 | end 85 | elseif self.state == STATES.variable_end then 86 | self.state = STATES.static 87 | token = sub(self.path, self.anchor, self.pos - 1) 88 | token_type = self.token_type(token) 89 | self.anchor = self.pos 90 | end 91 | 92 | self.pos = self.pos + 1 93 | 94 | if token then 95 | return token, token_type 96 | end 97 | end 98 | 99 | self.state = STATES.finish 100 | token = sub(self.path, self.anchor, self.pos) 101 | return token, self.token_type(token) 102 | end 103 | 104 | 105 | function _M:parse() 106 | self:reset() 107 | 108 | local tokens = {} 109 | local n = 0 110 | local token = self:next() 111 | while token do 112 | n = n + 1 113 | tokens[n] = token 114 | token = self:next() 115 | end 116 | 117 | return tokens 118 | end 119 | 120 | 121 | function _M.token_type(token) 122 | if byte(token) == BYTE_LEFT_BRACKET and 123 | byte(token, #token) == BYTE_RIGHT_BRACKET then 124 | if byte(token, 2) == BYTE_ASTERISK then 125 | return TOKEN_TYPES.catchall 126 | end 127 | return TOKEN_TYPES.variable 128 | end 129 | 130 | return TOKEN_TYPES.literal 131 | end 132 | 133 | 134 | local function parse_token_regex(token) 135 | for i = 1, #token do 136 | if byte(token, i) == BYTE_COLON then 137 | return sub(token, i + 1, -2) 138 | end 139 | end 140 | return nil 141 | end 142 | 143 | 144 | -- compile path to regex pattern 145 | function _M:compile_regex() 146 | local tokens = { "^" } 147 | 148 | local token, token_type = self:next() 149 | while token do 150 | if token_type == TOKEN_TYPES.variable then 151 | local pattern = parse_token_regex(token) or "[^/]+" 152 | table.insert(tokens, pattern) 153 | elseif token_type == TOKEN_TYPES.catchall then 154 | table.insert(tokens, ".*") 155 | else 156 | -- quote the literal token 157 | table.insert(tokens, "\\Q") 158 | table.insert(tokens, token) 159 | table.insert(tokens, "\\E") 160 | end 161 | token, token_type = self:next() 162 | end 163 | table.insert(tokens, "$") 164 | 165 | return table.concat(tokens) 166 | end 167 | 168 | function _M:params() 169 | local param_names_n = 0 170 | local param_names = {} 171 | local token, token_type = self:next() 172 | while token do 173 | if token_type == TOKEN_TYPES.variable or token_type == TOKEN_TYPES.catchall then 174 | if byte(token) == BYTE_LEFT_BRACKET and byte(token, #token) == BYTE_RIGHT_BRACKET then 175 | local param_name = sub(token, 2, #token - 1) 176 | if byte(param_name) == BYTE_ASTERISK then 177 | param_name = sub(param_name, 2) 178 | end 179 | for i = 1, #param_name do 180 | if byte(param_name, i) == BYTE_COLON then 181 | param_name = sub(param_name, 1, i - 1) 182 | break 183 | end 184 | end 185 | if #param_name > 0 then 186 | param_names_n = param_names_n + 1 187 | param_names[param_names_n] = param_name 188 | end 189 | end 190 | end 191 | 192 | token, token_type = self:next() 193 | end 194 | 195 | return param_names 196 | end 197 | 198 | 199 | function _M:bind_params(req_path, req_path_n, params, trailing_slash_mode) 200 | if not params then 201 | return 202 | end 203 | 204 | local path = self.path 205 | local path_n = self.path_n 206 | local pos, anchor, path_start = 1, 1, 0 207 | local state, char, param_n 208 | while pos <= path_n do 209 | char = byte(path, pos) 210 | -- local debug = string.char(char) 211 | if state == nil or state == STATES.static then 212 | if char == BYTE_LEFT_BRACKET then 213 | if state == STATES.static then 214 | anchor = pos 215 | end 216 | state = STATES.variable_start 217 | else 218 | state = STATES.static 219 | end 220 | path_start = path_start + 1 221 | elseif state == STATES.variable_start then 222 | if char == BYTE_RIGHT_BRACKET then 223 | state = STATES.variable_end 224 | end 225 | elseif state == STATES.variable_end then 226 | state = STATES.static 227 | local param_name = sub(path, anchor + 1, pos - 2) 228 | param_n = pos - anchor 229 | if byte(param_name) == BYTE_ASTERISK then 230 | param_name = sub(param_name, 2) 231 | param_n = param_n - 1 232 | end 233 | for i = 1, param_n do 234 | if byte(param_name, i) == BYTE_COLON then 235 | param_name = sub(param_name, 1, i - 1) 236 | param_n = i - 1 237 | break 238 | end 239 | end 240 | if param_n > 0 then 241 | local i = path_start 242 | while i <= req_path_n and byte(req_path, i) ~= char do 243 | i = i + 1 244 | end 245 | params[param_name] = sub(req_path, path_start, i - 1) 246 | path_start = i 247 | end 248 | end 249 | 250 | pos = pos + 1 251 | end 252 | 253 | if state == STATES.variable_end then 254 | local param_name = sub(path, anchor + 1, pos - 2) 255 | param_n = pos - anchor 256 | if byte(param_name) == BYTE_ASTERISK then 257 | param_name = sub(param_name, 2) 258 | param_n = param_n - 1 259 | end 260 | for i = 1, param_n do 261 | if byte(param_name, i) == BYTE_COLON then 262 | param_name = sub(param_name, 1, i - 1) 263 | param_n = i - 1 264 | break 265 | end 266 | end 267 | if param_n > 0 then 268 | if trailing_slash_mode and byte(req_path, -1) == BYTE_SLASH then 269 | params[param_name] = sub(req_path, path_start, path_n - 1) 270 | else 271 | params[param_name] = sub(req_path, path_start) 272 | end 273 | end 274 | end 275 | end 276 | 277 | 278 | local function contains_regex(path) 279 | local bracket_depth = 0 280 | 281 | for i = 1, #path do 282 | local char = byte(path, i) 283 | if char == BYTE_LEFT_BRACKET then 284 | bracket_depth = bracket_depth + 1 285 | elseif char == BYTE_RIGHT_BRACKET then 286 | bracket_depth = bracket_depth - 1 287 | elseif char == BYTE_COLON and bracket_depth == 1 then 288 | -- regex syntax {var:[^/]+} 289 | -- return true only if the colon is in the first depth 290 | return true 291 | end 292 | end 293 | 294 | return false 295 | end 296 | 297 | 298 | local function is_dynamic(path) 299 | local patn_n = #path 300 | for i = 1, patn_n do 301 | local char = byte(path, i) 302 | if char == BYTE_LEFT_BRACKET or char == BYTE_RIGHT_BRACKET then 303 | return true 304 | end 305 | end 306 | return false 307 | end 308 | 309 | 310 | _M.contains_regex = contains_regex 311 | _M.is_dynamic = is_dynamic 312 | 313 | return _M 314 | -------------------------------------------------------------------------------- /src/route.lua: -------------------------------------------------------------------------------- 1 | --- Route a route defines the matching conditions of its handler. 2 | -- 3 | -- @module radix-router.route 4 | 5 | local ipairs = ipairs 6 | local str_byte = string.byte 7 | local BYTE_SLASH = str_byte("/") 8 | 9 | local Route = {} 10 | local mt = { __index = Route } 11 | 12 | 13 | function Route.new(route) 14 | if route.handler == nil then 15 | return nil, "handler must not be nil" 16 | end 17 | 18 | for _, path in ipairs(route.paths) do 19 | if str_byte(path) ~= BYTE_SLASH then 20 | return nil, "path must start with /" 21 | end 22 | end 23 | 24 | return setmetatable(route, mt) 25 | end 26 | 27 | 28 | function Route:compare(other) 29 | return (self.priority or 0) > (other.priority or 0) 30 | end 31 | 32 | 33 | return Route 34 | -------------------------------------------------------------------------------- /src/router.lua: -------------------------------------------------------------------------------- 1 | --- Radix-Router is a lightweight high-performance and radix tree based router matching library. 2 | -- 3 | -- @module radix-router 4 | 5 | local Trie = require "radix-router.trie" 6 | local Route = require "radix-router.route" 7 | local Parser = require "radix-router.parser" 8 | local Iterator = require "radix-router.iterator" 9 | local Options = require "radix-router.options" 10 | local Matcher = require "radix-router.matcher" 11 | local utils = require "radix-router.utils" 12 | local constants = require "radix-router.constants" 13 | 14 | local ipairs = ipairs 15 | local str_byte = string.byte 16 | local str_sub = string.sub 17 | local idx = constants.node_indexs 18 | local regex_test = utils.regex_test 19 | 20 | local BYTE_SLASH = str_byte("/") 21 | local EMPTY = utils.readonly({}) 22 | 23 | local Router = {} 24 | local mt = { __index = Router } 25 | 26 | 27 | local function add_route(self, path, route) 28 | local path_route = { path, route } 29 | local is_dynamic = self.parser.is_dynamic(path) 30 | if not is_dynamic then 31 | -- static path 32 | local routes = self.static[path] 33 | if not routes then 34 | self.static[path] = { [0] = 1, path_route } 35 | else 36 | routes[0] = routes[0] + 1 37 | routes[routes[0]] = path_route 38 | table.sort(routes, function(o1, o2) 39 | local route1 = o1[2] 40 | local route2 = o2[2] 41 | return route1:compare(route2) 42 | end) 43 | end 44 | return 45 | end 46 | 47 | -- dynamic path 48 | self.trie:add(path, nil, function(node) 49 | local routes = node[idx.value] 50 | if not routes then 51 | node[idx.value] = { [0] = 1, path_route } 52 | return 53 | end 54 | routes[0] = routes[0] + 1 55 | routes[routes[0]] = path_route 56 | table.sort(routes, function(o1, o2) 57 | local route1 = o1[2] 58 | local route2 = o2[2] 59 | return route1:compare(route2) 60 | end) 61 | end, self.parser) 62 | 63 | if self.parser.contains_regex(path) then 64 | self.regexs[path] = self.parser:update(path):compile_regex() 65 | end 66 | end 67 | 68 | 69 | --- create a new router. 70 | -- @tab[opt] routes a list-like table of routes 71 | -- @tab[opt] opts options table 72 | -- @return a new router, or nil 73 | -- @return cannot create router error 74 | -- @usage 75 | -- local router, err = router.new({ 76 | -- { 77 | -- paths = { "/hello-{word}" }, 78 | -- methods = { "GET" }, 79 | -- handler = "hello handler", 80 | -- }, 81 | -- }) 82 | function Router.new(routes, opts) 83 | if routes ~= nil and type(routes) ~= "table" then 84 | return nil, "invalid args routes: routes must be table or nil" 85 | end 86 | 87 | local options, err = Options.options(opts) 88 | if not options then 89 | return nil, "invalid args opts: " .. err 90 | end 91 | 92 | local matcher, err = Matcher.new(options.matcher_names, options.matchers) 93 | if err then 94 | return nil, "invalid args opts: " .. err 95 | end 96 | 97 | local self = { 98 | options = options, 99 | parser = Parser.new("default"), 100 | static = {}, 101 | regexs = {}, 102 | regexs_cache = {}, 103 | trie = Trie.new(), 104 | iterator = Iterator.new(options), 105 | matcher = matcher, 106 | } 107 | 108 | for i, route in ipairs(routes or EMPTY) do 109 | local ok, err = self.matcher:process(route) 110 | if not ok then 111 | return nil, "unable to process route(index " .. i .. "): " .. err 112 | end 113 | 114 | local route_t, err = Route.new(route) 115 | if err then 116 | return nil, "invalid route(index " .. i .. "): " .. err 117 | end 118 | 119 | for _, path in ipairs(route.paths) do 120 | add_route(self, path, route_t) 121 | end 122 | end 123 | 124 | return setmetatable(self, mt) 125 | end 126 | 127 | 128 | local function find_route(self, path, routes, ctx, matched, evaluate_regex) 129 | for n = 1, routes[0] do 130 | local route_path = routes[n][1] 131 | local route = routes[n][2] 132 | local regex_matched = true 133 | if evaluate_regex then 134 | local regex = self.regexs[route_path] 135 | if regex then 136 | regex_matched = regex_test(path, regex, self.regexs_cache) 137 | end 138 | end 139 | if regex_matched and self.matcher:match(route, ctx, matched) then 140 | if matched then 141 | matched.path = route_path 142 | end 143 | return route, route_path 144 | end 145 | end 146 | 147 | return nil 148 | end 149 | 150 | 151 | --- find a handler of route that matches the path and ctx. 152 | -- @string path the request path 153 | -- @tab[opt] ctx the request context 154 | -- @tab[opt] params a table to store the parsed parameters 155 | -- @tab[opt] matched a table to store the matched conditions, such as path, method and host 156 | -- @return the handler of a route matches the path and ctx, or nil if not found 157 | -- @usage 158 | -- local params = {} 159 | -- local matched = {} 160 | -- local handler = router:match("/hello-world", { method = "GET" }, params, matched) 161 | function Router:match(path, ctx, params, matched) 162 | ctx = ctx or EMPTY 163 | 164 | local trailing_slash_match = self.options.trailing_slash_match 165 | local matched_route, matched_path 166 | 167 | local routes = self.static[path] 168 | if routes then 169 | matched_route, matched_path = find_route(self, path, routes, ctx, matched) 170 | if matched_route then 171 | return matched_route.handler 172 | end 173 | end 174 | 175 | if trailing_slash_match then 176 | if str_byte(path, -1) == BYTE_SLASH then 177 | routes = self.static[str_sub(path, 1, -2)] 178 | else 179 | routes = self.static[path .. "/"] 180 | end 181 | if routes then 182 | matched_route, matched_path = find_route(self, path, routes, ctx, matched) 183 | if matched_route then 184 | return matched_route.handler 185 | end 186 | end 187 | end 188 | 189 | local path_n = #path 190 | local node = self.trie 191 | local state_path = path 192 | local state_path_n = path_n 193 | repeat 194 | local values, count = self.iterator:find(node, state_path, state_path_n) 195 | if values then 196 | for n = count, 1, -1 do 197 | matched_route, matched_path = find_route(self, path, values[n], ctx, matched, true) 198 | if matched_route then 199 | break 200 | end 201 | end 202 | if matched_route then 203 | break 204 | end 205 | end 206 | node, state_path, state_path_n = self.iterator:prev() 207 | until node == nil 208 | 209 | if matched_route then 210 | if params then 211 | self.parser:update(matched_path):bind_params(path, path_n, params, trailing_slash_match) 212 | end 213 | return matched_route.handler 214 | end 215 | 216 | return nil 217 | end 218 | 219 | 220 | return Router 221 | -------------------------------------------------------------------------------- /src/trie.lua: -------------------------------------------------------------------------------- 1 | --- Trie 2 | -- 3 | -- 4 | 5 | local utils = require "radix-router.utils" 6 | local constants = require "radix-router.constants" 7 | 8 | local str_sub = string.sub 9 | local lcp = utils.lcp 10 | local type = type 11 | 12 | local TOKEN_TYPES = constants.token_types 13 | local TYPES = constants.node_types 14 | local idx = constants.node_indexs 15 | 16 | local TrieNode = {} 17 | local mt = { __index = TrieNode } 18 | 19 | 20 | function TrieNode.new(node_type, path, children, value) 21 | local pathn = path and #path or 0 22 | local self = { node_type, path, pathn, children, value } 23 | return setmetatable(self, mt) 24 | end 25 | 26 | 27 | function TrieNode:set(value, fn) 28 | if type(fn) == "function" then 29 | fn(self) 30 | return 31 | end 32 | self[idx.value] = value 33 | end 34 | 35 | 36 | local function insert(node, path, value, fn, parser) 37 | parser:update(path) 38 | local token, token_type = parser:next() 39 | while token do 40 | if token_type == TOKEN_TYPES.variable then 41 | node[idx.type] = TYPES.variable 42 | node[idx.pathn] = 0 43 | elseif token_type == TOKEN_TYPES.catchall then 44 | node[idx.type] = TYPES.catchall 45 | node[idx.pathn] = 0 46 | else 47 | node[idx.type] = TYPES.literal 48 | node[idx.path] = token 49 | node[idx.pathn] = #token 50 | end 51 | 52 | token, token_type = parser:next() 53 | if token then 54 | local child = TrieNode.new() 55 | if token_type == TOKEN_TYPES.literal then 56 | local char = str_sub(token, 1, 1) 57 | node[idx.children] = { [char] = child } 58 | else 59 | node[idx.children] = { [token_type] = child } 60 | end 61 | node = child 62 | end 63 | end 64 | 65 | node:set(value, fn) 66 | end 67 | 68 | 69 | local function split(node, path, prefix_n) 70 | local child = TrieNode.new( 71 | TYPES.literal, 72 | str_sub(node[idx.path], prefix_n + 1), 73 | node[idx.children], 74 | node[idx.value] 75 | ) 76 | 77 | -- update current node 78 | node[idx.type] = TYPES.literal 79 | node[idx.path] = str_sub(path, 1, prefix_n) 80 | node[idx.pathn] = #node[idx.path] 81 | node[idx.value] = nil 82 | node[idx.children] = { [str_sub(child[idx.path], 1, 1)] = child } 83 | end 84 | 85 | 86 | function TrieNode:add(path, value, fn, parser) 87 | if not self[idx.path] and not self[idx.type] then 88 | -- insert to current empty node 89 | insert(self, path, value, fn, parser) 90 | return 91 | end 92 | 93 | local node = self 94 | local token, token_type 95 | while true do 96 | local common_prefix_n = lcp(node[idx.path], path) 97 | 98 | if common_prefix_n < node[idx.pathn] then 99 | split(node, path, common_prefix_n) 100 | end 101 | 102 | if common_prefix_n < #path then 103 | if node[idx.type] == TYPES.variable then 104 | -- token must a variable 105 | path = str_sub(path, #token + 1) 106 | if #path == 0 then 107 | break 108 | end 109 | elseif node[idx.type] == TYPES.catchall then 110 | -- token must a catchall 111 | -- catchall node matches entire path 112 | break 113 | else 114 | path = str_sub(path, common_prefix_n + 1) 115 | end 116 | 117 | local child 118 | if node[idx.children] then 119 | local first_char = str_sub(path, 1, 1) 120 | if node[idx.children][first_char] then 121 | -- found literal child 122 | child = node[idx.children][first_char] 123 | else 124 | parser:update(path) 125 | token, token_type = parser:next() -- store the next token of path 126 | if node[idx.children][token_type] then 127 | -- found either variable or catchall child 128 | child = node[idx.children][token_type] 129 | end 130 | end 131 | end 132 | 133 | if child then 134 | node = child 135 | else 136 | child = TrieNode.new() 137 | insert(child, path, value, fn, parser) 138 | node[idx.children] = node[idx.children] or {} 139 | if child[idx.type] == TYPES.literal then 140 | local first_char = str_sub(path, 1, 1) 141 | node[idx.children][first_char] = child 142 | else 143 | node[idx.children][token_type] = child 144 | end 145 | return 146 | end 147 | else 148 | break 149 | end 150 | end 151 | 152 | node:set(value, fn) 153 | end 154 | 155 | 156 | return TrieNode 157 | -------------------------------------------------------------------------------- /src/utils.lua: -------------------------------------------------------------------------------- 1 | --- Utility functions. 2 | -- Some of the functions have jit implementation for better performance. 3 | -- 4 | -- @module radix-router.utils 5 | local str_byte = string.byte 6 | local math_min = math.min 7 | local type = type 8 | 9 | local is_luajit = type(_G.jit) == "table" 10 | -- print("luajit enabled: " .. tostring(is_luajit)) 11 | 12 | --- clear a table 13 | local clear_table 14 | do 15 | local ok 16 | ok, clear_table = pcall(require, "table.clear") 17 | if not ok then 18 | local pairs = pairs 19 | clear_table = function (tab) 20 | for k, _ in pairs(tab) do 21 | tab[k] = nil 22 | end 23 | end 24 | end 25 | end 26 | 27 | 28 | --- allocate a pre-sized table 29 | local new_table 30 | do 31 | local ok 32 | ok, new_table = pcall(require, "table.new") 33 | if not ok then 34 | new_table = function(narr, nrec) 35 | return {} 36 | end 37 | end 38 | end 39 | 40 | 41 | local starts_with 42 | local ends_with 43 | do 44 | if is_luajit then 45 | local ffi = require "ffi" 46 | local C = ffi.C 47 | ffi.cdef[[ 48 | int memcmp(const void *s1, const void *s2, size_t n); 49 | ]] 50 | starts_with = function(str, prefix, strn, prefixn) 51 | strn = strn or #str 52 | prefixn = prefixn or #prefix 53 | 54 | if prefixn == 0 then 55 | return true 56 | end 57 | 58 | if strn < prefixn then 59 | return false 60 | end 61 | 62 | local rc = C.memcmp(str, prefix, prefixn) 63 | return rc == 0 64 | end 65 | ends_with = function(str, suffix, strn, suffixn, suffix_skip) 66 | strn = strn or #str 67 | suffix_skip = suffix_skip or 0 68 | suffixn = (suffixn or #suffix) - suffix_skip 69 | 70 | if suffixn == 0 then 71 | return true 72 | end 73 | 74 | if strn < suffixn then 75 | return false 76 | end 77 | 78 | local rc = C.memcmp(ffi.cast("char *", str) + strn - suffixn, ffi.cast("char *", suffix) + suffix_skip, suffixn) 79 | return rc == 0 80 | end 81 | else 82 | local str_sub = string.sub 83 | starts_with = function(str, prefix, strn, prefixn) 84 | strn = strn or #str 85 | prefixn = prefixn or #prefix 86 | 87 | if prefixn == 0 then 88 | return true 89 | end 90 | 91 | if strn < prefixn then 92 | return false 93 | end 94 | 95 | for i = 1, prefixn do 96 | if str_byte(str, i) ~= str_byte(prefix, i) then 97 | return false 98 | end 99 | end 100 | 101 | return true 102 | end 103 | ends_with = function(str, suffix, strn, suffixn, suffix_skip) 104 | strn = strn or #str 105 | suffix_skip = suffix_skip or 0 106 | suffixn = (suffixn or #suffix) - suffix_skip 107 | 108 | if suffixn == 0 then 109 | return true 110 | end 111 | 112 | if strn < suffixn then 113 | return false 114 | end 115 | 116 | return str_sub(str, -suffixn) == str_sub(suffix, 1 + suffix_skip) 117 | end 118 | end 119 | end 120 | 121 | 122 | local function lcp(str1, str2) 123 | if str1 == nil or str2 == nil then 124 | return 0 125 | end 126 | local min_len = math_min(#str1, #str2) 127 | local n = 0 128 | for i = 1, min_len do 129 | if str_byte(str1, i) == str_byte(str2, i) then 130 | n = n + 1 131 | else 132 | break 133 | end 134 | end 135 | return n 136 | end 137 | 138 | 139 | local function readonly(t) 140 | return setmetatable(t, { 141 | __newindex = function() error("attempt to modify a read-only table") end 142 | }) 143 | end 144 | 145 | --- test a string whether matches a regex pattern. 146 | local regex_test 147 | do 148 | if ngx and ngx.re then 149 | -- print("regex_test(ngx.re.find)") 150 | local ngx_re_find = ngx.re.find 151 | regex_test = function(str, regex) 152 | local from, to = ngx_re_find(str, regex, "jo") 153 | return from == 1 and to == #str 154 | end 155 | else 156 | -- print("regex_test(rex_pcre2)") 157 | local lrex = require "rex_pcre2" 158 | regex_test = function(str, regex, cache) 159 | local compiled = cache[regex] 160 | if not compiled then 161 | compiled = lrex.new(regex) 162 | compiled:jit_compile() 163 | cache[regex] = compiled 164 | end 165 | local from, to = compiled:find(str) 166 | return from == 1 and to == #str 167 | end 168 | end 169 | end 170 | 171 | 172 | return { 173 | lcp = lcp, 174 | starts_with = starts_with, 175 | ends_with = ends_with, 176 | clear_table = clear_table, 177 | new_table = new_table, 178 | is_luajit = is_luajit, 179 | readonly = readonly, 180 | regex_test = regex_test, 181 | } 182 | --------------------------------------------------------------------------------