params
140 | table
141 | a table to store the parsed parameters
142 | (optional)
143 |
144 |
matched
145 | table
146 | a table to store the matched conditions, such as path, method and host
147 | (optional)
148 |
149 |
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 |
161 |
local params = {}
162 | local matched = {}
163 | local handler = router:match("/hello-world", { method = "GET" }, params, matched)
164 |
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/README.zh.md:
--------------------------------------------------------------------------------
1 | # Lua-Radix-Router [](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml) [](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml) [](https://coveralls.io/github/vm-001/lua-radix-router) 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Lua-Radix-Router [](https://github.com/vm-001/lua-radix-router/releases) [](https://github.com/vm-001/lua-radix-router/actions/workflows/test.yml) [](https://github.com/vm-001/lua-radix-router/actions/workflows/examples.yml) [](https://coveralls.io/github/vm-001/lua-radix-router) 
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 | { --