diff --git a/.github/workflows/sample.yml b/.github/workflows/examples.yml similarity index 85% rename from .github/workflows/sample.yml rename to .github/workflows/examples.yml index 15784c9..ebcf1c1 100644 --- a/.github/workflows/sample.yml +++ b/.github/workflows/examples.yml @@ -1,4 +1,4 @@ -name: sample +name: examples on: pull_request: @@ -35,6 +35,7 @@ jobs: run: | luarocks install radix-router - - name: samples + - name: run examples run: | - lua samples/1-sample.lua \ No newline at end of file + lua examples/example.lua + lua examples/custom-matcher.lua diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f960fe3..70fc41d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,10 +44,6 @@ jobs: run: | make test-coverage - - name: samples - run: | - lua samples/1-sample.lua - - name: report test coverage if: success() continue-on-error: true diff --git a/Makefile b/Makefile index a3dba7e..fb247d4 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,7 @@ bench: 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 + +ldoc: + @rm -rf docs/* + @ldoc . diff --git a/README.md b/README.md index 9c2e396..dabb1e0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# 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/sample.yml/badge.svg)](https://github.com/vm-001/lua-radix-router/actions/workflows/sample.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) +# 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) English | [中文](README.zh.md) -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()`. +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()`. 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. @@ -86,7 +86,7 @@ assert(params.year == "2023") assert(params.format == "pdf") ``` -For more usage samples, please refer to the [/samples](/samples) directory. For more use cases, please check out https://github.com/vm-001/lua-radix-router-use-cases. +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). ## 📄 Methods @@ -111,8 +111,8 @@ local router, err = Router.new(routes, opts) | 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 | - - + + Route defines the matching conditions for its handler. @@ -203,7 +203,7 @@ router.static = { [/api/login] = { * } } - TrieNode.path TrieNode.value + TrieNode.path TrieNode.value router.trie = / nil ├─people/ nil │ └─{wildcard} nil @@ -229,7 +229,7 @@ $ make bench #### Environments -- Apple MacBook Pro(M1 Pro), 32GB +- Apple MacBook Pro(M1 Pro), 32GB - LuaJIT 2.1.1700008891 #### Results diff --git a/README.zh.md b/README.zh.md index 626953a..aa3557d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1,4 +1,4 @@ -# 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) [![Coverage Status](https://coveralls.io/repos/github/vm-001/lua-radix-router/badge.svg)](https://coveralls.io/github/vm-001/lua-radix-router) +# 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) [English](README.md) | 中文 (Translated by ChatGPT) diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..44bee71 --- /dev/null +++ b/config.ld @@ -0,0 +1,6 @@ +project = 'Radix-Router' +description = 'A lightweight high-performance and radix tree based router for Lua/LuaJIT.' +full_description = '' +examples = { './examples' } +file='./src/router.lua' +dir = 'docs' diff --git a/docs/examples/custom-matcher.lua.html b/docs/examples/custom-matcher.lua.html new file mode 100644 index 0000000..56ca1ce --- /dev/null +++ b/docs/examples/custom-matcher.lua.html @@ -0,0 +1,110 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

custom-matcher.lua

+
+local Router = require "radix-router"
+
+local ip_matcher = {
+  process = function(route)
+    -- builds a table for O(1) access
+    if route.ips then
+      local ips = {}
+      for _, ip in ipairs(route.ips) do
+        ips[ip] = true
+      end
+      route.ips = ips
+    end
+  end,
+  match = function(route, ctx, matched)
+    if route.ips then
+      local ip = ctx.ip
+      if not route.ips[ip] then
+        return false
+      end
+      if matched then
+        matched["ip"] = ip
+      end
+    end
+    return true
+  end
+}
+
+local opts = {
+  matchers = { ip_matcher }, -- register custom ip_matcher
+  matcher_names = { "method" }, -- host is disabled
+}
+
+local router = Router.new({
+  {
+    paths = { "/" },
+    methods = { "GET", "POST" },
+    ips = { "127.0.0.1", "127.0.0.2" },
+    handler = "1",
+  },
+  {
+    paths = { "/" },
+    methods = { "GET", "POST" },
+    ips = { "192.168.1.1", "192.168.1.2" },
+    handler = "2",
+  }
+}, opts)
+assert("1" == router:match("/", { method = "GET", ip = "127.0.0.2" }))
+local matched = {}
+assert("2" == router:match("/", { method = "GET", ip = "192.168.1.2" }, nil, matched))
+print(matched.method) -- GET
+print(matched.ip) -- 192.168.1.2
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-02-05 16:00:23 +
+
+ + diff --git a/docs/examples/example.lua.html b/docs/examples/example.lua.html new file mode 100644 index 0000000..8fdea21 --- /dev/null +++ b/docs/examples/example.lua.html @@ -0,0 +1,96 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

example.lua

+
+local Router = require "radix-router"
+local router, err = Router.new({
+  {
+    paths = { "/foo", "/foo/bar", "/html/index.html" },
+    handler = "1" -- handler can be any non-nil value. (e.g. boolean, table, function)
+  },
+  {
+    -- variable path
+    paths = { "/users/{id}/profile-{year}.{format}" },
+    handler = "2"
+  },
+  {
+    -- prefix path
+    paths = { "/api/authn/{*path}" },
+    handler = "3"
+  },
+  {
+    -- methods
+    paths = { "/users/{id}" },
+    methods = { "POST" },
+    handler = "4"
+  }
+})
+if not router then
+  error("failed to create router: " .. err)
+end
+
+assert("1" == router:match("/html/index.html"))
+assert("2" == router:match("/users/100/profile-2023.pdf"))
+assert("3" == router:match("/api/authn/token/genreate"))
+assert("4" == router:match("/users/100", { method = "POST" }))
+
+-- parameter binding
+local params = {}
+router:match("/users/100/profile-2023.pdf", nil, params)
+assert(params.year == "2023")
+assert(params.format == "pdf")
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-02-05 16:00:23 +
+
+ + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..8587640 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,177 @@ + + + + + Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module radix-router

+

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

+

+ + +

Functions

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

Functions

+ +
+
+ + Router.new ([routes[, opts]]) +
+
+ create a new router. + + +

Parameters:

+ + +

Returns:

+
    +
  1. + a new router, or nil
  2. +
  3. + cannot create router error
  4. +
+ + + +

Usage:

+ + +
+
+ + Router:match (path[, ctx[, params[, matched]]]) +
+
+ find a handler of route that matches the path and ctx. + + +

Parameters:

+ + +

Returns:

+
    + + the handler of a route matches the path and ctx, or nil if not found +
+ + + +

Usage:

+ + +
+
+ + + + +
+generated by LDoc 1.5.0 +Last updated 2024-02-05 16:00:23 +
+ + + diff --git a/docs/ldoc.css b/docs/ldoc.css new file mode 100644 index 0000000..f945ae7 --- /dev/null +++ b/docs/ldoc.css @@ -0,0 +1,304 @@ +/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #000; + background: #FFF; +} +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 { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #ffffff; margin: 0px; +} + +code, tt { font-family: monospace; font-size: 1.1em; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 20px 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #004080; text-decoration: none; } +a:visited { font-weight: bold; color: #006699; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre { + background-color: rgb(245, 245, 245); + border: 1px solid #C0C0C0; /* silver */ + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + +pre.example { + font-size: .85em; +} + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #f0f0f0; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #ffffff; +} + +#product big { + font-size: 2em; +} + +#main { + background-color: #f0f0f0; + border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 14em; + vertical-align: top; + background-color: #f0f0f0; + overflow: visible; +} + +#navigation h2 { + background-color:#e7e7e7; + font-size:1.1em; + color:#000000; + text-align: left; + padding:0.2em; + border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; + padding: 1em; + width: 700px; + border-left: 2px solid #cccccc; + border-right: 2px solid #cccccc; + background-color: #ffffff; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #ffffff; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.module_list td.summary { width: 100%; } + + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #FF9; +} + + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #aa5050; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #8080ff; } +pre .number { color: #f8660d; } +pre .function-name { color: #60447f; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .user-keyword { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + diff --git a/samples/2.custom-matcher.lua b/examples/custom-matcher.lua similarity index 100% rename from samples/2.custom-matcher.lua rename to examples/custom-matcher.lua diff --git a/samples/1-sample.lua b/examples/example.lua similarity index 100% rename from samples/1-sample.lua rename to examples/example.lua diff --git a/src/router.lua b/src/router.lua index 81e8612..9054bbc 100644 --- a/src/router.lua +++ b/src/router.lua @@ -1,6 +1,6 @@ ---- Router the router engine --- +--- Radix-Router is a lightweight high-performance and radix tree based router matching library. -- +-- @module radix-router local Trie = require "radix-router.trie" local Route = require "radix-router.route" @@ -60,9 +60,19 @@ local function add_route(self, path, route) end ---- new a Router instance --- @tab routes routes table --- @tab otps options table +--- create a new router. +-- @tab[opt] routes a list-like table of routes +-- @tab[opt] opts options table +-- @return a new router, or nil +-- @return cannot create router error +-- @usage +-- local router, err = router.new({ +-- { +-- paths = { "/hello-{word}" }, +-- methods = { "GET" }, +-- handler = "hello handler", +-- }, +-- }) function Router.new(routes, opts) if routes ~= nil and type(routes) ~= "table" then return nil, "invalid args routes: routes must be table or nil" @@ -130,11 +140,16 @@ local function find_route(matcher, routes, ctx, matched) end ---- return the handler of a Route that matches the path and ctx --- @string path the path --- @tab ctx the condition ctx --- @tab params table to store the parameters --- @tab matched table to store the matched condition +--- find a handler of route that matches the path and ctx. +-- @string path the request path +-- @tab[opt] ctx the request context +-- @tab[opt] params a table to store the parsed parameters +-- @tab[opt] matched a table to store the matched conditions, such as path, method and host +-- @return the handler of a route matches the path and ctx, or nil if not found +-- @usage +-- local params = {} +-- local matched = {} +-- local handler = router:match("/hello-world", { method = "GET" }, params, matched) function Router:match(path, ctx, params, matched) ctx = ctx or EMPTY