Skip to content

Commit

Permalink
feat: regex pattern in variables (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
vm-001 authored Mar 1, 2024
1 parent f1ca79a commit 06a5e1c
Show file tree
Hide file tree
Showing 21 changed files with 538 additions and 108 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: examples

on:
pull_request:
paths-ignore:
- '**/*.md'
push:
branches:
- main
Expand Down Expand Up @@ -39,3 +36,4 @@ jobs:
run: |
lua examples/example.lua
lua examples/custom-matcher.lua
lua examples/regular-expression.lua
59 changes: 57 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- '**/*.md'

jobs:
test:
lua:
runs-on: ubuntu-latest

strategy:
Expand Down Expand Up @@ -48,6 +48,7 @@ jobs:
run: |
lua examples/example.lua
lua examples/custom-matcher.lua
lua examples/regular-expression.lua
- name: report test coverage
if: success()
Expand All @@ -58,4 +59,58 @@ jobs:

- name: benchmark
run: |
make bench CMD=lua
make bench CMD=lua
openresty:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
openrestyVersion: [ "1.19.9.1", "1.21.4.3", "1.25.3.1" ]

steps:
- name: checkout source code
uses: actions/checkout@v3

- name: install Lua/LuaJIT
uses: leafo/gh-actions-lua@v8
with:
luaVersion: "luajit-openresty"

- uses: leafo/gh-actions-openresty@v1
with:
openrestyVersion: ${{ matrix.openrestyVersion }}

- name: install LuaRocks
uses: leafo/gh-actions-luarocks@v4

- name: install dependencies
run: |
luarocks install busted
luarocks install luacov-coveralls
- name: build
run: |
make install
- name: run tests
run: |
bin/resty_busted --coverage spec/
- name: samples
run: |
resty examples/example.lua
resty examples/custom-matcher.lua
resty examples/regular-expression.lua
- name: report test coverage
if: success()
continue-on-error: true
run: luacov-coveralls
env:
COVERALLS_REPO_TOKEN: ${{ github.token }}

- name: benchmark
run: |
make bench CMD=resty
4 changes: 4 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
unused_args = false
max_line_length = false
redefined = false

globals = {
"ngx",
}
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bench:
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable.lua
RADIX_ROUTER_ROUTES=1000000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable.lua
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-prefix.lua
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/simple-regex.lua
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/complex-variable.lua
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 $(CMD) benchmark/simple-variable-binding.lua
RADIX_ROUTER_TIMES=1000000 $(CMD) benchmark/github-routes.lua
Expand Down
136 changes: 86 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ The router can be run in different runtimes such as Lua, LuaJIT, or OpenResty.

**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/.

**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.
**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.

**Regex pattern:** You can define regex pattern in variables. a variable without regex pattern is treated as `[^/]+`.

- `/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}`
- `/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}`

**Features in the roadmap**:

- Expression condition: defines custom matching conditions by using expression language.
- Regex in variable


## 📖 Getting started

Expand Down Expand Up @@ -106,11 +109,11 @@ local router, err = Router.new(routes, opts)

The available options are as follow

| NAME | TYPE | DEFAULT | DESCRIPTION |
| -------------------- | ------- | ------------------ | --------------------------------------------------- |
| trailing_slash_match | boolean | false | whether to enable the trailing slash match behavior |
| matcher_names | table | {"method", "host"} | enabled built-in macher list |
| matchers | table | { } | custom matcher list |
| NAME | TYPE | DEFAULT | DESCRIPTION |
| -------------------- | ------- | ----------------- | --------------------------------------------------- |
| trailing_slash_match | boolean | false | whether to enable the trailing slash match behavior |
| matcher_names | table | {"method","host"} | enabled built-in macher list |
| matchers | table | { } | custom matcher list |



Expand Down Expand Up @@ -141,6 +144,28 @@ local handler = router:match(path, ctx, params, matched)
- **params**(`table|nil`): the optional table to use for storing the parameters binding result.
- **matched**(`table|nil`): the optional table to use for storing the matched conditions.

## 📝 Examples

#### Regex pattern

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]+}`.

```lua
local Router = require "radix-router"
local router = Router.new({
{
paths = { "/users/{id:\\d+}/profile-{year:\\d{4}}.{format:(html|pdf)}" },
handler = "1"
},
{
paths = { "/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}" },
handler = "2"
},
})
assert("1" == router:match("/users/100/profile-2024.pdf"))
assert("2", router:match("/users/00000000-0000-0000-0000-000000000000"))
```

## 🧠 Data Structure and Implementation

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))
Expand Down Expand Up @@ -215,7 +240,6 @@ router.trie = / nil
└─{catchall} { "/src/{*filename}", *<table 3> }
```


## 🚀 Benchmarks

#### Usage
Expand All @@ -234,15 +258,16 @@ $ make bench

#### Results

| TEST CASE | Router number | nanoseconds / op | QPS | RSS |
| ----------------------- |---------------|------------------|------------|--------------|
| static path | 100000 | 0.0129826 | 77,026,173 | 65.25 MB |
| simple variable | 100000 | 0.0802077 | 12,467,630 | 147.52 MB |
| simple variable | 1000000 | 0.084604 | 11,819,772 | 1381.47 MB |
| simple prefix | 100000 | 0.0713651 | 14,012,451 | 147.47 MB |
| complex variable | 100000 | 0.914117 | 1,093,951 | 180.30 MB |
| simple variable binding | 100000 | 0.21054 | 4,749,691 | 147.28 MB |
| github | 609 | 0.375829 | 2,660,784 | 2.72 MB |
| test case | route number | ns/op | OPS | RSS |
|-------------------------|--------------|-----------|------------|------------|
| static path | 100000 | 0.0171333 | 58,365,872 | 48.69 MB |
| simple variable | 100000 | 0.0844033 | 11,847,877 | 99.97 MB |
| simple variable | 1000000 | 0.087095 | 11,481,675 | 1000.41 MB |
| simple prefix | 100000 | 0.0730344 | 13,692,177 | 99.92 MB |
| simple regex | 100000 | 0.14444 | 6,923,289 | 126.64 MB |
| complex variable | 100000 | 0.858975 | 1,164,178 | 140.08 MB |
| simple variable binding | 100000 | 0.1843245 | 5,425,214 | 99.94 MB |
| github | 609 | 0.38436 | 2,601,727 | 2.69 MB |

<details>
<summary>Expand output</summary>
Expand All @@ -252,79 +277,90 @@ RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/static-p
========== static path ==========
routes : 100000
times : 10000000
elapsed : 0.129826 s
QPS : 77026173
ns/op : 0.0129826 ns
elapsed : 0.171333 s
QPS : 58365872
ns/op : 0.0171333 ns
path : /50000
handler : 50000
Memory : 65.25 MB
Memory : 48.69 MB
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua
========== variable ==========
routes : 100000
times : 10000000
elapsed : 0.802077 s
QPS : 12467630
ns/op : 0.0802077 ns
elapsed : 0.844033 s
QPS : 11847877
ns/op : 0.0844033 ns
path : /1/foo
handler : 1
Memory : 147.52 MB
Memory : 99.97 MB
RADIX_ROUTER_ROUTES=1000000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable.lua
========== variable ==========
routes : 1000000
times : 10000000
elapsed : 0.84604 s
QPS : 11819772
ns/op : 0.084604 ns
path : /1/foo
handler : 1
Memory : 1381.47 MB
routes : 1000000
times : 10000000
elapsed : 0.870953 s
QPS : 11481675
ns/op : 0.0870953 ns
path : /1/foo
handler : 1
Memory : 1000.41 MB
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-prefix.lua
========== prefix ==========
routes : 100000
times : 10000000
elapsed : 0.713651 s
QPS : 14012451
ns/op : 0.0713651 ns
elapsed : 0.730344 s
QPS : 13692177
ns/op : 0.0730344 ns
path : /1/a
handler : 1
Memory : 99.92 MB
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 luajit benchmark/simple-regex.lua
========== regex ==========
routes : 100000
times : 1000000
elapsed : 0.14444 s
QPS : 6923289
ns/op : 0.14444 ns
path : /1/a
handler : 1
Memory : 147.47 MB
Memory : 126.64 MB
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=1000000 luajit benchmark/complex-variable.lua
========== variable ==========
routes : 100000
times : 1000000
elapsed : 0.914117 s
QPS : 1093951
ns/op : 0.914117 ns
elapsed : 0.858975 s
QPS : 1164178
ns/op : 0.858975 ns
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
handler : 50000
Memory : 180.30 MB
Memory : 140.08 MB
RADIX_ROUTER_ROUTES=100000 RADIX_ROUTER_TIMES=10000000 luajit benchmark/simple-variable-binding.lua
========== variable ==========
routes : 100000
times : 10000000
elapsed : 2.1054 s
QPS : 4749691
ns/op : 0.21054 ns
elapsed : 1.843245 s
QPS : 5425214
ns/op : 0.1843245 ns
path : /1/foo
handler : 1
params : name = foo
Memory : 147.28 MB
Memory : 99.94 MB
RADIX_ROUTER_TIMES=1000000 luajit benchmark/github-routes.lua
========== github apis ==========
routes : 609
times : 1000000
elapsed : 0.375829 s
QPS : 2660784
ns/op : 0.375829 ns
elapsed : 0.38436 s
QPS : 2601727
ns/op : 0.38436 ns
path : /repos/vm-001/lua-radix-router/import
handler : /repos/{owner}/{repo}/import
Memory : 2.72 MB
Memory : 2.69 MB
```

</details>
Expand Down
33 changes: 33 additions & 0 deletions benchmark/simple-regex.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
local Router = require "radix-router"
local utils = require "benchmark.utils"

local route_n = os.getenv("RADIX_ROUTER_ROUTES") or 1000 * 100
local times = os.getenv("RADIX_ROUTER_TIMES") or 1000 * 1000 * 10

local router
do
local routes = {}
for i = 1, route_n do
routes[i] = { paths = { string.format("/%d/{name:[^/]+}", i) }, handler = i }
end
router = Router.new(routes)
end

local rss_mb = utils.get_rss()

local path = "/1/a"
local elapsed = utils.timing(function()
for _ = 1, times do
router:match(path)
end
end)

utils.print_result({
title = "regex",
routes = route_n,
times = times,
elapsed = elapsed,
benchmark_path = path,
benchmark_handler = router:match(path),
rss = rss_mb,
})
2 changes: 1 addition & 1 deletion benchmark/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ local function print_result(result, items)
print("routes :", result.routes)
print("times :", result.times)
print("elapsed :", result.elapsed .. " s")
print("QPS :", math.floor(result.times / result.elapsed))
print("ns/op :", result.elapsed * 1000 * 1000 / result.times .. " ns")
print("OPS :", math.floor(result.times / result.elapsed))
print("path :", result.benchmark_path)
print("handler :", result.benchmark_handler)
for _, item in ipairs(items or {}) do
Expand Down
7 changes: 7 additions & 0 deletions bin/resty_busted
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env resty

if ngx ~= nil then
ngx.exit = function()end
end

require 'busted.runner'({ standalone = false })
Loading

0 comments on commit 06a5e1c

Please sign in to comment.