├── .gitignore
├── .luacheckrc
├── .sublimelinterrc
├── .travis.yml
├── .travis
├── platform.sh
├── setenv_lua.sh
├── setup_lua.sh
└── setup_servers.sh
├── Changes.md
├── LICENSE
├── Makefile
├── README.md
├── README_zh.md
├── bin
├── lord.lua
└── scaffold
│ ├── generator.lua
│ ├── launcher.lua
│ ├── nginx
│ ├── conf_template.lua
│ ├── config.lua
│ ├── directive.lua
│ └── handle.lua
│ └── utils.lua
├── dist.ini
├── lib
└── lor
│ ├── index.lua
│ ├── lib
│ ├── application.lua
│ ├── debug.lua
│ ├── holder.lua
│ ├── methods.lua
│ ├── middleware
│ │ ├── cookie.lua
│ │ ├── init.lua
│ │ └── session.lua
│ ├── node.lua
│ ├── request.lua
│ ├── response.lua
│ ├── router
│ │ ├── group.lua
│ │ └── router.lua
│ ├── trie.lua
│ ├── utils
│ │ ├── aes.lua
│ │ ├── base64.lua
│ │ └── utils.lua
│ ├── view.lua
│ └── wrap.lua
│ └── version.lua
├── resty
├── cookie.lua
├── template.lua
└── template
│ ├── html.lua
│ └── microbenchmark.lua
└── spec
├── cases
├── basic_spec.lua
├── common_spec.lua
├── error_middleware_spec.lua
├── final_handler_spec.lua
├── group_index_route_spec.lua
├── group_router_spec.lua
├── mock_request.lua
├── mock_response.lua
├── multi_route_spec.lua
├── node_id_spec.lua
├── not_found_spec.lua
├── path_params_spec.lua
├── path_pattern_1_spec.lua
├── path_pattern_2_spec.lua
├── path_pattern_3_spec.lua
└── uri_char_spec.lua
└── trie
├── basic_spec.lua
├── complex_cases_spec.lua
├── debug_cases.lua
├── define_node_spec.lua
├── find_node_spec.lua
├── handle_spec.lua
└── strict_route_spec.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | ###test
2 | local_install.sh
3 | gen_rockspec.sh
4 | test.sh
5 | test.lua
6 | snippets.lua
7 | .idea
8 | *.iml
9 | *.rock
10 | *.rockspec
11 | lor-*
12 | .DS_Store
13 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | std = "ngx_lua"
2 | globals = {"LOR_FRAMEWORK_DEBUG"}
3 |
4 | exclude_files = {"test/*", "resty", "bin"}
5 |
--------------------------------------------------------------------------------
/.sublimelinterrc:
--------------------------------------------------------------------------------
1 | {
2 | "linters": {
3 | "luacheck": {
4 | "@disable": false,
5 | "args": [],
6 | "std": "ngx_lua",
7 | "excludes": ["test/*", "resty", "bin", "*_spec.lua", "*.test.lua"],
8 | "globals": ["LOR_FRAMEWORK_DEBUG"],
9 | "ignore": [
10 | "channel"
11 | ],
12 | "limit": null,
13 | "only": []
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: c
2 |
3 | sudo: false
4 |
5 | env:
6 | global:
7 | - LUAROCKS=2.2.2
8 | matrix:
9 | # Lua 5.1
10 | - LUA=lua5.1
11 | # LuaJIT latest stable version (2.0.4)
12 | - LUA=luajit
13 | # Openresty + LuaJIT + mysql
14 | - LUA=luajit SERVER=openresty
15 | # - LUA=luajit2.0 # current head of 2.0 branch
16 | # - LUA=luajit2.1 # current head of 2.1 branch
17 | # Not supported
18 | # - LUA=lua5.2
19 | # - LUA=lua5.3
20 |
21 | branches:
22 | - master
23 | - v0.3.0
24 |
25 |
26 | before_install:
27 | - source .travis/setenv_lua.sh
28 |
29 | install:
30 | - luarocks install https://luarocks.org/manifests/olivine-labs/busted-2.0.rc12-1.rockspec
31 | - luarocks install lrexlib-pcre 2.7.2-1
32 | - luarocks install luaposix
33 | - luarocks install lua-cjson
34 | #- luarocks make
35 |
36 | script:
37 | - busted spec/*
38 |
39 | notifications:
40 | email:
41 | on_success: change
42 | on_failure: always
43 |
--------------------------------------------------------------------------------
/.travis/platform.sh:
--------------------------------------------------------------------------------
1 | if [ -z "${PLATFORM:-}" ]; then
2 | PLATFORM=$TRAVIS_OS_NAME;
3 | fi
4 |
5 | if [ "$PLATFORM" == "osx" ]; then
6 | PLATFORM="macosx";
7 | fi
8 |
9 | if [ -z "$PLATFORM" ]; then
10 | if [ "$(uname)" == "Linux" ]; then
11 | PLATFORM="linux";
12 | else
13 | PLATFORM="macosx";
14 | fi;
15 | fi
16 |
--------------------------------------------------------------------------------
/.travis/setenv_lua.sh:
--------------------------------------------------------------------------------
1 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/luarocks/bin
2 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/openresty/nginx/sbin
3 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/openresty/bin
4 | bash .travis/setup_lua.sh
5 | if [ "$SERVER" == "openresty" ]; then
6 | bash .travis/setup_servers.sh
7 | fi
8 |
9 | eval `$HOME/.lua/luarocks path`
10 |
--------------------------------------------------------------------------------
/.travis/setup_lua.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # A script for setting up environment for travis-ci testing.
4 | # Sets up Lua and Luarocks.
5 | # LUA must be "lua5.1", "lua5.2" or "luajit".
6 | # luajit2.0 - master v2.0
7 | # luajit2.1 - master v2.1
8 |
9 | set -eufo pipefail
10 |
11 | LUAJIT_BASE="LuaJIT-2.0.4"
12 |
13 | source .travis/platform.sh
14 |
15 | LUA_HOME_DIR=$TRAVIS_BUILD_DIR/install/lua
16 |
17 | LR_HOME_DIR=$TRAVIS_BUILD_DIR/install/luarocks
18 |
19 | mkdir $HOME/.lua
20 |
21 | LUAJIT="no"
22 |
23 | if [ "$PLATFORM" == "macosx" ]; then
24 | if [ "$LUA" == "luajit" ]; then
25 | LUAJIT="yes";
26 | fi
27 | if [ "$LUA" == "luajit2.0" ]; then
28 | LUAJIT="yes";
29 | fi
30 | if [ "$LUA" == "luajit2.1" ]; then
31 | LUAJIT="yes";
32 | fi;
33 | elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then
34 | LUAJIT="yes";
35 | fi
36 |
37 | mkdir -p "$LUA_HOME_DIR"
38 |
39 | if [ "$LUAJIT" == "yes" ]; then
40 |
41 | git clone https://github.com/LuaJIT/LuaJIT $LUAJIT_BASE;
42 |
43 | cd $LUAJIT_BASE
44 |
45 | if [ "$LUA" == "luajit2.1" ]; then
46 | git checkout v2.1;
47 | # force the INSTALL_TNAME to be luajit
48 | perl -i -pe 's/INSTALL_TNAME=.+/INSTALL_TNAME= luajit/' Makefile
49 | else
50 | git checkout v2.0.4;
51 | fi
52 |
53 | make && make install PREFIX="$LUA_HOME_DIR"
54 |
55 | ln -s $LUA_HOME_DIR/bin/luajit $HOME/.lua/luajit
56 | ln -s $LUA_HOME_DIR/bin/luajit $HOME/.lua/lua;
57 |
58 | else
59 |
60 | if [ "$LUA" == "lua5.1" ]; then
61 | curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz
62 | cd lua-5.1.5;
63 | fi
64 |
65 | # Build Lua without backwards compatibility for testing
66 | perl -i -pe 's/-DLUA_COMPAT_(ALL|5_2)//' src/Makefile
67 | make $PLATFORM
68 | make INSTALL_TOP="$LUA_HOME_DIR" install;
69 |
70 | ln -s $LUA_HOME_DIR/bin/lua $HOME/.lua/lua
71 | ln -s $LUA_HOME_DIR/bin/luac $HOME/.lua/luac;
72 |
73 | fi
74 |
75 | cd $TRAVIS_BUILD_DIR
76 |
77 | lua -v
78 |
79 | LUAROCKS_BASE=luarocks-$LUAROCKS
80 |
81 | curl --location http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz
82 |
83 | cd $LUAROCKS_BASE
84 |
85 | if [ "$LUA" == "luajit" ]; then
86 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR";
87 | elif [ "$LUA" == "luajit2.0" ]; then
88 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR";
89 | elif [ "$LUA" == "luajit2.1" ]; then
90 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.1" --prefix="$LR_HOME_DIR";
91 | else
92 | ./configure --with-lua="$LUA_HOME_DIR" --prefix="$LR_HOME_DIR"
93 | fi
94 |
95 | make build && make install
96 |
97 | ln -s $LR_HOME_DIR/bin/luarocks $HOME/.lua/luarocks
98 |
99 | cd $TRAVIS_BUILD_DIR
100 |
101 | luarocks --version
102 |
103 | rm -rf $LUAROCKS_BASE
104 |
105 | if [ "$LUAJIT" == "yes" ]; then
106 | rm -rf $LUAJIT_BASE;
107 | elif [ "$LUA" == "lua5.1" ]; then
108 | rm -rf lua-5.1.5;
109 | fi
110 |
--------------------------------------------------------------------------------
/.travis/setup_servers.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # A script for setting up environment for travis-ci testing.
4 | # Sets up openresty.
5 | OPENRESTY_VERSION="1.9.3.1"
6 | OPENRESTY_DIR=$TRAVIS_BUILD_DIR/install/openresty
7 |
8 | #if [ "$LUA" == "lua5.1" ]; then
9 | # luarocks install LuaBitOp
10 | #fi
11 |
12 | wget https://openresty.org/download/ngx_openresty-$OPENRESTY_VERSION.tar.gz
13 | tar xzvf ngx_openresty-$OPENRESTY_VERSION.tar.gz
14 | cd ngx_openresty-$OPENRESTY_VERSION/
15 |
16 | ./configure --prefix="$OPENRESTY_DIR" --with-luajit
17 |
18 | make
19 | make install
20 |
21 | ln -s $OPENRESTY_DIR/bin/resty $HOME/.lua/resty
22 | ln -s $OPENRESTY_DIR/nginx/sbin/nginx $HOME/.lua/nginx
23 |
24 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/openresty/nginx/sbin
25 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/openresty/bin
26 |
27 | nginx -v
28 | resty -V
29 |
30 | cd ../
31 | rm -rf ngx_openresty-$OPENRESTY_VERSION
32 | cd $TRAVIS_BUILD_DIR
33 |
34 |
--------------------------------------------------------------------------------
/Changes.md:
--------------------------------------------------------------------------------
1 | ### v0.3.4 2017.08.30
2 |
3 | - 修复默认session插件的`session_aes_secret`长度问题
4 | - 此问题存在于OpenResty v1.11.2.5版本及可能之后的版本中
5 | - lua-resty-string v0.10开始AES salt必须是[8个字符](https://github.com/openresty/lua-resty-string/commit/69df3dcc2230364a54761a0d5a65327c6a4e256a)
6 | - 使用内置的session插件时`session_aes_secret`不再是必须配置
7 | - 若不填则默认为`12345678`
8 | - 若不足8个字符则以`0`补足
9 | - 若超过8个字符则只使用前8个
10 |
11 | ### v0.3.3 2017.08.05
12 |
13 | - 使用严格的路由节点id策略,避免潜在冲突
14 |
15 |
16 | ### v0.3.2 2017.06.10
17 |
18 | - 关于内置session插件的更改
19 | - 修复session过期时间bug
20 | - 移除lua-resty-session依赖
21 | - 内置session插件替换为基于cookie的简单实现
22 | - 接口仍然保持与之前版本兼容
23 | - 关于session处理,仍然建议根据具体业务需求和安全考量自行实现
24 | - 支持URI中含有字符'-'
25 |
26 | ### v0.3.1 2017.04.16
27 |
28 | - 支持路由中包含`~`字符(from [@XadillaX](https://github.com/XadillaX))
29 | - 支持组路由(group router)的多级路由写法
30 | - 支持组路由下直接挂载中间件(see [issues#40](https://github.com/sumory/lor/issues/40))
31 |
32 | ### v0.3.0 2017.02.11
33 |
34 | 此版本为性能优化和内部实现重构版本,API使用上保持与之前版本兼容,详细描述如下:
35 |
36 | **特性**
37 |
38 | - 中间件(middlewares)重构,支持任意级别、多种方式挂载中间件,这些中间件包括
39 | - 预处理中间件(use)
40 | - 错误处理中间件(erroruse)
41 | - 业务处理中间件(get/post/put/delete...)
42 | - 提高路由性能
43 | - 路由匹配次数不再随路由数增多而正比例增长
44 | - 全面支持正则路由和通配符路由
45 | - use、get/put/delete/post等API优化,如支持数组参数、支持单独挂载中间件等改进
46 | - 路由匹配更加灵活: 优先匹配精确路由,其次再匹配正则路由或通配符路由
47 |
48 | **Break Changes**
49 |
50 | 与之前版本相比,break changes主要有以下几点(基本都是一些比较少用到的特性)
51 |
52 | - 路由执行顺序不再与路由定义顺序相关, 如错误路由不用必须定义在最下方
53 | - 如果一个请求最终匹配不到已定义的任何路由,则不会执行任何中间件代码(之前的版本会执行,这浪费了一些性能)
54 |
55 |
56 | ### v0.2.6 2016.11.26
57 |
58 | - 升级内部集成的session中间件
59 | - lua-resty-session升级到2.13版本
60 | - 添加一个session过期参数timeout,默认为3600秒
61 | - 添加一个refresh_cookie参数,用于控制否在有新请求时刷新session和cookie过期时间,默认“是”
62 | - 更新`lord new`项目模板
63 | - 缓存`app`对象,提高性能
64 | - 调整CRUD示例, 详细请参看脚手架代码中的app/routes/user.lua
65 | - 删除默认响应头X-Powered-By
66 |
67 | ### v0.2.4 2016.11.16
68 |
69 | - 支持"application/json"类型请求
70 |
71 |
72 | ### v0.2.2 2016.10.15
73 |
74 | - 支持opm, 可通过`opm install sumory/lor`安装
75 | - 注意opm暂不支持命令安装, 所以这种方式无法安装`lord`命令
76 | - 若仍想使用`lord`命令,建议使用`sh install.sh`方式安装
77 |
78 | ### v0.1.6 2016.10.14
79 |
80 | - `lord`工具改为使用resty-cli实现,不再依赖luajit
81 |
82 | ### v0.1.5 2016.10.01
83 |
84 | - Path URI支持"."
85 | - 使用xpcall替换pcall以记录更多出错日志
86 | - 更新了测试用例
87 |
88 | ### v0.1.4 2016.07.30
89 |
90 | - 删除一些无用代码和引用
91 | - 升级测试依赖库
92 | - 修改文档和注释
93 | - 修改一些小bug
94 |
95 | ### v0.1.0 2016.03.15
96 |
97 | - 增加一配置项,是否启用模板功能:app:conf("view enable", true), 默认为关闭
98 | - view.lua中ngx.var.template_root存在性判断
99 | - 增加docker支持
100 | - 命令`lord --path`变更为`lord path`,用于查看当前lor的安装路径
101 | - 官网文档更新[http://lor.sumory.com](http://lor.sumory.com)
102 |
103 | ### v0.0.9 2016.03.02
104 |
105 | - 使用install.sh安装lor时如果有指定安装目录,则在指定的目录后面拼上"lor",避免文件误删的问题
106 | - TODO: debug时列出整个路由表供参考
107 |
108 | ### v0.0.8 2016.02.26
109 |
110 | - 支持multipart/form文件上传
111 | - 修复了一个group router被多次app:use时出现404的bug
112 | - 支持Response:json(data, flag)方法传入第二个bool类型参数flag,指明序列化json时默认的空table是否编码为{}
113 | - true 作为{}处理
114 | - false 作为[]处理
115 | - 不传入第二个参数则当作[]处理
116 |
117 |
118 | ### v0.0.7 2016.02.02
119 |
120 | - 统一代码风格
121 | - 优化部分代码,比如使用ngx.re代替string对应方法、尽量使用local等
122 | - Break API: req:isFound() -> req:is_found()
123 | - Fix bug: 修复了在lua_code_cache on时的一些404问题
124 |
125 |
126 | ### v0.0.6 2016.01.30
127 |
128 | - 修改了lor的默认安装路径到/usr/local/lor
129 | - 命令行工具`lord`生成的项目模板更改
130 | - 加入了nginx.conf配置,方便之后维护自定义的nginx配置
131 | - 加入start/stop/restart脚本,方便之后项目的灵活部署
132 | - 改善了路由pattern,支持path variable含有"-"字符
133 | - 增加了几个测试用例
134 | - 修复了上一个请求的path variable会污染之后请求的bug
135 | - 完善了res:redirect API
136 | - 修复了请求体为空时解析的bug
137 | - 给lor对象添加了版本号
138 | - 添加了静态文件支持(通过在nginx.conf里配置)
139 | - 编写了lor框架示例项目[lor-example](https://github.com/lorlabs/lor-example)
140 |
141 |
142 | ### v0.0.5 2016.01.28
143 |
144 | - 完善了Documents和API文档,详见[lor官网](http://lor.sumory.com)
145 | - `lor new`命令生成的项目模板增加了一个middleware目录,用于存放自定义插件
146 | - 该目录的命名和位置都是非强制的,用户可按需要将自定义的插件放在任何地方
147 | - 修改了lor new产生的项目模板,增加了几个基本API的使用方式
148 |
149 |
150 | ### v0.0.4 2016.01.25
151 |
152 | - 以默认插件的形式添加cookie支持(lua-resty-cookie)
153 | - 以默认插件的形式添加session支持(lua-resty-session)
154 |
155 |
156 | ### v0.0.3 2016.01.23
157 |
158 | - 修复上版本路由bug
159 | - 添加模板支持(lua-resty-template)
160 | - 完善了40余个常规测试用例
161 | - 完善了命令行工具`lord`
162 | - 常规API使用方法添加到默认项目模板
163 |
164 |
165 | ### v0.0.2 2016.01.21
166 |
167 | - 完全重构v0.0.1路由
168 | - Sinatra风格路由
169 | - 主要API设计完成并实现
170 |
171 |
172 | ### v0.0.1 2016.01.15
173 |
174 | - 原型设计和实验
175 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 - 2017 sumory.wu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TO_INSTALL = lib/* resty spec bin
2 | LOR_HOME ?= /usr/local
3 | LORD_BIN ?= /usr/local/bin
4 |
5 | .PHONY: test install
6 |
7 | test:
8 | busted spec/*
9 |
10 | install_lor:
11 | @mkdir -p ${LOR_HOME}/lor
12 | @mkdir -p ${LOR_HOME}
13 | @rm -rf ${LOR_HOME}/lor/*
14 |
15 | @echo "install lor runtime files to "${LOR_HOME}/lor
16 |
17 | @for item in $(TO_INSTALL) ; do \
18 | cp -a $$item ${LOR_HOME}/lor/; \
19 | done;
20 |
21 | @echo "lor runtime files installed."
22 |
23 |
24 | install_lord: install_lor
25 | @mkdir -p ${LORD_BIN}
26 | @echo "install lord cli to "${LORD_BIN}"/"
27 |
28 | @echo "#!/usr/bin/env resty" > tmp_lor_bin
29 | @echo "package.path=\""${LOR_HOME}/lor"/?.lua;;\"" >> tmp_lor_bin
30 | @echo "if arg[1] and arg[1] == \"path\" then" >> tmp_lor_bin
31 | @echo " print(\"${LOR_HOME}/lor\")" >> tmp_lor_bin
32 | @echo " return" >> tmp_lor_bin
33 | @echo "end" >> tmp_lor_bin
34 | @echo "require('bin.lord')(arg)" >> tmp_lor_bin
35 |
36 | @mv tmp_lor_bin ${LORD_BIN}/lord
37 | @chmod +x ${LORD_BIN}/lord
38 |
39 | @echo "lord cli installed."
40 |
41 | install: install_lord
42 | @echo "lor framework installed successfully."
43 |
44 | version:
45 | @lord -v
46 |
47 | help:
48 | @lord -h
49 |
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lor
2 |
3 | [](https://travis-ci.org/sumory/lor) [](https://github.com/sumory/lor/releases/latest) [](https://github.com/sumory/lor/blob/master/LICENSE)
4 |
5 | 中文 English
6 |
7 | A fast and minimalist web framework based on [OpenResty](http://openresty.org).
8 |
9 |
10 |
11 | ```lua
12 | local lor = require("lor.index")
13 | local app = lor()
14 |
15 | app:get("/", function(req, res, next)
16 | res:send("hello world!")
17 | end)
18 |
19 | app:run()
20 | ```
21 |
22 | ## Examples
23 |
24 | - [lor-example](https://github.com/lorlabs/lor-example)
25 | - [openresty-china](https://github.com/sumory/openresty-china)
26 | - [lua-redis-admin](https://github.com/lifeblood/lua-redis-admin)
27 |
28 |
29 | ## Installation
30 |
31 | 1) shell
32 |
33 | ```shell
34 | git clone https://github.com/sumory/lor
35 | cd lor
36 | make install
37 | ```
38 |
39 | `LOR_HOME` and `LORD_BIN` are supported by `Makefile`, so the following command could be used to customize installation:
40 |
41 | ```
42 | make install LOR_HOME=/path/to/lor LORD_BIN=/path/to/lord
43 | ```
44 |
45 | 2) opm
46 |
47 | `opm install` is supported from v0.2.2.
48 |
49 | ```
50 | opm install sumory/lor
51 | ```
52 |
53 | `lord` cli is not supported with this installation.
54 |
55 | 3) homebrew
56 |
57 | you can use [homebrew-lor](https://github.com/syhily/homebrew-lor) on Mac OSX.
58 |
59 | ```
60 | $ brew tap syhily/lor
61 | $ brew install lor
62 | ```
63 |
64 |
65 | ## Features
66 |
67 | - Routing like [Sinatra](http://www.sinatrarb.com/) which is a famous Ruby framework
68 | - Similar API with [Express](http://expressjs.com), good experience for Node.js or Javascript developers
69 | - Middleware support
70 | - Group router support
71 | - Session/Cookie/Views supported and could be redefined with `Middleware`
72 | - Easy to build HTTP APIs, web site, or single page applications
73 |
74 |
75 | ## Docs & Community
76 |
77 | - [Website and Documentation](http://lor.sumory.com).
78 | - [Github Organization](https://github.com/lorlabs) for Official Middleware & Modules.
79 |
80 |
81 | ## Quick Start
82 |
83 | A quick way to get started with lor is to utilize the executable cli tool `lord` to generate an scaffold application.
84 |
85 | `lord` is installed with `lor` framework. it looks like:
86 |
87 | ```bash
88 | $ lord -h
89 | lor ${version}, a Lua web framework based on OpenResty.
90 |
91 | Usage: lord COMMAND [OPTIONS]
92 |
93 | Commands:
94 | new [name] Create a new application
95 | start Starts the server
96 | stop Stops the server
97 | restart Restart the server
98 | version Show version of lor
99 | help Show help tips
100 | ```
101 |
102 | Create app:
103 |
104 | ```
105 | $ lord new lor_demo
106 | ```
107 |
108 | Start server:
109 |
110 | ```
111 | $ cd lor_demo && lord start
112 | ```
113 |
114 | Visit [http://localhost:8888](http://localhost:8888).
115 |
116 |
117 | ## Tests
118 |
119 | Install [busted](http://olivinelabs.com/busted/), then run test
120 |
121 | ```
122 | busted spec/*
123 | ```
124 |
125 | ## Homebrew
126 |
127 | [https://github.com/syhily/homebrew-lor](https://github.com/syhily/homebrew-lor) maintained by [@syhily](https://github.com/syhily)
128 |
129 | ## Contributors
130 |
131 | - [@ms2008](https://github.com/ms2008)
132 | - [@wanghaisheng](https://github.com/wanghaisheng)
133 | - [@lihuibin](https://github.com/lihuibin)
134 | - [@syhily](https://github.com/syhily)
135 | - [@vinsonzou](https://github.com/vinsonzou)
136 | - [@lhmwzy](https://github.com/lhmwzy)
137 | - [@hanxi](https://github.com/hanxi)
138 | - [@诗兄](https://github.com/269724033)
139 | - [@hetz](https://github.com/hetz)
140 | - [@XadillaX](https://github.com/XadillaX)
141 |
142 | ## License
143 |
144 | [MIT](./LICENSE)
145 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # Lor
2 |
3 | [](https://travis-ci.org/sumory/lor) [](https://github.com/sumory/lor/releases/latest) [](https://github.com/sumory/lor/blob/master/LICENSE)
4 |
5 | 中文 English
6 |
7 | **Lor**是一个运行在[OpenResty](http://openresty.org)上的基于Lua编写的Web框架.
8 |
9 | - 路由采用[Sinatra](http://www.sinatrarb.com/)风格,结构清晰,易于编码和维护.
10 | - API借鉴了[Express](http://expressjs.com)的思路和设计,Node.js跨界开发者可以很快上手.
11 | - 支持多种路由,路由可分组,路由匹配支持正则模式.
12 | - 支持middleware机制,可在任意路由上挂载中间件.
13 | - 可作为HTTP API Server,也可用于构建传统的Web应用.
14 |
15 |
16 | ### 文档
17 |
18 | [http://lor.sumory.com](http://lor.sumory.com)
19 |
20 | #### 示例项目
21 |
22 | - 简单示例项目[lor-example](https://github.com/lorlabs/lor-example)
23 | - 全站示例项目[openresty-china](https://github.com/sumory/openresty-china)
24 |
25 |
26 | ### 快速开始
27 |
28 | **特别注意:** 在使用lor之前请首先确保OpenResty已安装,并将`nginx`/`resty`命令配置到环境变量中。即在命令行直接输入`nginx -v`、`resty -v`能正确执行。
29 |
30 | 一个简单示例(更复杂的示例或项目模板请使用`lord`命令生成):
31 |
32 | ```lua
33 | local lor = require("lor.index")
34 | local app = lor()
35 |
36 | app:get("/", function(req, res, next)
37 | res:send("hello world!")
38 | end)
39 |
40 | -- 路由示例: 匹配/query/123?foo=bar
41 | app:get("/query/:id", function(req, res, next)
42 | local foo = req.query.foo
43 | local path_id = req.params.id
44 | res:json({
45 | foo = foo,
46 | id = path_id
47 | })
48 | end)
49 |
50 | -- 错误处理插件,可根据需要定义多个
51 | app:erroruse(function(err, req, res, next)
52 | -- err是错误对象
53 | ngx.log(ngx.ERR, err)
54 | if req:is_found() ~= true then
55 | return res:status(404):send("sorry, not found.")
56 | end
57 | res:status(500):send("server error")
58 | end)
59 |
60 | app:run()
61 | ```
62 |
63 | ### 安装
64 |
65 |
66 | #### 1)使用脚本安装(推荐)
67 |
68 | 使用Makefile安装lor框架:
69 |
70 | ```shell
71 | git clone https://github.com/sumory/lor
72 | cd lor
73 | make install
74 | ```
75 |
76 | 默认`lor`的运行时lua文件会被安装到`/usr/local/lor`下, 命令行工具`lord`被安装在`/usr/local/bin`下。
77 |
78 | 如果希望自定义安装目录, 可参考如下命令自定义路径:
79 |
80 | ```shell
81 | make install LOR_HOME=/path/to/lor LORD_BIN=/path/to/lord
82 | ```
83 |
84 | 执行**默认安装**后, lor的命令行工具`lord`就被安装在了`/usr/local/bin`下, 通过`which lord`查看:
85 |
86 | ```
87 | $ which lord
88 | /usr/local/bin/lord
89 | ```
90 |
91 | `lor`的运行时包安装在了指定目录下, 可通过`lord path`命令查看。
92 |
93 |
94 | #### 2)使用opm安装
95 |
96 | `opm`是OpenResty即将推出的官方包管理器,从v0.2.2开始lor支持通过opm安装:
97 |
98 | ```
99 | opm install sumory/lor
100 | ```
101 |
102 | 注意: 目前opm不支持安装命令行工具,所以此种方式安装后不能使用`lord`命令。
103 |
104 |
105 | #### 3)使用homebrew安装
106 |
107 | 除使用以上方式安装外, Mac用户还可使用homebrew来安装lor, 该方式由[@syhily](https://github.com/syhily)提供, 更详尽的使用方法请参见[这里](https://github.com/syhily/homebrew-lor)。
108 |
109 | ```
110 | $ brew tap syhily/lor
111 | $ brew install lor
112 | ```
113 |
114 | 至此, `lor`框架已经安装完毕,接下来使用`lord`命令行工具快速开始一个项目骨架.
115 |
116 |
117 | ### 使用
118 |
119 | ```
120 | $ lord -h
121 | lor ${version}, a Lua web framework based on OpenResty.
122 |
123 | Usage: lord COMMAND [OPTIONS]
124 |
125 | Commands:
126 | new [name] Create a new application
127 | start Starts the server
128 | stop Stops the server
129 | restart Restart the server
130 | version Show version of lor
131 | help Show help tips
132 | ```
133 |
134 | 执行`lord new lor_demo`,则会生成一个名为lor_demo的示例项目,然后执行:
135 |
136 | ```
137 | cd lor_demo
138 | lord start
139 | ```
140 |
141 | 之后访问[http://localhost:8888/](http://localhost:8888/), 即可。
142 |
143 | 更多使用方法,请参考[use cases](./spec/cases)测试用例。
144 |
145 | ### Homebrew
146 |
147 | [https://github.com/syhily/homebrew-lor](https://github.com/syhily/homebrew-lor)由[@syhily](https://github.com/syhily)维护。
148 |
149 | ### 贡献者
150 |
151 | - [@ms2008](https://github.com/ms2008)
152 | - [@wanghaisheng](https://github.com/wanghaisheng)
153 | - [@lihuibin](https://github.com/lihuibin)
154 | - [@syhily](https://github.com/syhily)
155 | - [@vinsonzou](https://github.com/vinsonzou)
156 | - [@lhmwzy](https://github.com/lhmwzy)
157 | - [@hanxi](https://github.com/hanxi)
158 | - [@诗兄](https://github.com/269724033)
159 | - [@hetz](https://github.com/hetz)
160 | - [@XadillaX](https://github.com/XadillaX)
161 |
162 | ### 讨论交流
163 |
164 | 有一个QQ群用于在线讨论: 522410959
165 |
166 | ### License
167 |
168 | [MIT](./LICENSE)
169 |
--------------------------------------------------------------------------------
/bin/lord.lua:
--------------------------------------------------------------------------------
1 | package.path = './?.lua;' .. package.path
2 |
3 | local generator = require("bin.scaffold.generator")
4 | local lor = require("bin.scaffold.launcher")
5 | local version = require("lor.version")
6 |
7 | local usages = [[lor v]] .. version .. [[, a Lua web framework based on OpenResty.
8 |
9 | Usage: lord COMMAND [OPTIONS]
10 |
11 | Commands:
12 | new [name] Create a new application
13 | start Start running app server
14 | stop Stop the server
15 | restart Restart the server
16 | version Show version of lor
17 | help Show help tips
18 | path Show install path
19 | ]]
20 |
21 | local function exec(args)
22 | local arg = table.remove(args, 1)
23 |
24 | -- parse commands and options
25 | if arg == 'new' and args[1] then
26 | generator.new(args[1]) -- generate example code
27 | elseif arg == 'start' then
28 | lor.start() -- start application
29 | elseif arg == 'stop' then
30 | lor.stop() -- stop application
31 | elseif arg == 'restart' then
32 | lor.stop()
33 | lor.start()
34 | elseif arg == 'reload' then
35 | lor.reload()
36 | elseif arg == 'help' or arg == '-h' then
37 | print(usages)
38 | elseif arg == 'version' or arg == '-v' then
39 | print(version) -- show lor framework version
40 | elseif arg == nil then
41 | print(usages)
42 | else
43 | print("[lord] unsupported commands or options, `lord -h` to check usages.")
44 | end
45 | end
46 |
47 | return exec
48 |
--------------------------------------------------------------------------------
/bin/scaffold/launcher.lua:
--------------------------------------------------------------------------------
1 | local sgmatch = string.gmatch
2 | local ogetenv = os.getenv
3 |
4 | local ngx_handle = require 'bin.scaffold.nginx.handle'
5 | local ngx_config = require 'bin.scaffold.nginx.config'
6 | local ngx_conf_template = require 'bin.scaffold.nginx.conf_template'
7 |
8 | local Lor = {}
9 |
10 | function Lor.nginx_conf_content()
11 | -- read nginx.conf file
12 | local nginx_conf_template = ngx_conf_template.get_ngx_conf_template()
13 |
14 | -- append notice
15 | nginx_conf_template = [[
16 | #generated by `lor framework`
17 | ]] .. nginx_conf_template
18 |
19 | local match = {}
20 | local tmp = 1
21 | for v in sgmatch(nginx_conf_template , '{{(.-)}}') do
22 | match[tmp] = v
23 | tmp = tmp + 1
24 | end
25 |
26 | for _, directive in ipairs(match) do
27 | if ngx_config[directive] ~= nil then
28 | nginx_conf_template = string.gsub(nginx_conf_template, '{{' .. directive .. '}}', ngx_config[directive])
29 | else
30 | nginx_conf_template = string.gsub(nginx_conf_template, '{{' .. directive .. '}}', '#' .. directive)
31 | end
32 | end
33 |
34 | return nginx_conf_template
35 | end
36 |
37 | local function new_handler()
38 | local necessary_dirs ={ -- runtime nginx conf/pid/logs dir
39 | tmp = 'tmp',
40 | logs = 'logs'
41 | }
42 | local env = ogetenv("LOR_ENV") or 'dev'
43 | return ngx_handle.new(
44 | necessary_dirs,
45 | Lor.nginx_conf_content(),
46 | "conf/nginx-" .. env .. ".conf"
47 | )
48 | end
49 |
50 | function Lor.start()
51 | local env = ogetenv("LOR_ENV") or 'dev'
52 |
53 | local ok, handler = pcall(function() return new_handler() end)
54 | if ok == false then
55 | print("ERROR:Cannot initialize handler: " .. handler)
56 | return
57 | end
58 |
59 | local result = handler:start(env)
60 | if result == 0 or result == true or result == "true" then
61 | if env ~= 'test' then
62 | print("app in " .. env .. " was succesfully started on port " .. ngx_config.PORT)
63 | end
64 | else
65 | print("ERROR: Could not start app on port " .. ngx_config.PORT)
66 | end
67 | end
68 |
69 | function Lor.stop()
70 | local env = ogetenv("LOR_ENV") or 'dev'
71 |
72 | local handler = new_handler()
73 | local result = handler:stop(env)
74 |
75 | if env ~= 'test' then
76 | if result == 0 or result == true or result == "true" then
77 | print("app in " .. env .. " was succesfully stopped.")
78 | else
79 | print("ERROR: Could not stop app (are you sure it is running?).")
80 | end
81 | end
82 | end
83 |
84 | function Lor.reload()
85 | local env = ogetenv("LOR_ENV") or 'dev'
86 |
87 | local handler = new_handler()
88 | local result = handler:reload(env)
89 |
90 | if env ~= 'test' then
91 | if result == 0 or result == true or result == "true" then
92 | print("app in " .. env .. " was succesfully reloaded.")
93 | else
94 | print("ERROR: Could not reloaded app.")
95 | end
96 | end
97 | end
98 |
99 | return Lor
100 |
--------------------------------------------------------------------------------
/bin/scaffold/nginx/conf_template.lua:
--------------------------------------------------------------------------------
1 | local _M = {}
2 |
3 | function _M:get_ngx_conf_template()
4 | return [[
5 | # user www www;
6 | pid tmp/{{LOR_ENV}}-nginx.pid;
7 |
8 | # This number should be at maxium the number of CPU on the server
9 | worker_processes 4;
10 |
11 | events {
12 | # Number of connections per worker
13 | worker_connections 4096;
14 | }
15 |
16 | http {
17 | sendfile on;
18 | include ./mime.types;
19 |
20 | {{LUA_PACKAGE_PATH}}
21 | lua_code_cache on;
22 |
23 | server {
24 | # List port
25 | listen {{PORT}};
26 |
27 | # Access log
28 | access_log logs/{{LOR_ENV}}-access.log;
29 |
30 | # Error log
31 | error_log logs/{{LOR_ENV}}-error.log;
32 |
33 | # this variable is for view render(lua-resty-template)
34 | set $template_root '';
35 |
36 | location /static {
37 | alias {{STATIC_FILE_DIRECTORY}}; #app/static;
38 | }
39 |
40 | # lor runtime
41 | {{CONTENT_BY_LUA_FILE}}
42 | }
43 | }
44 | ]]
45 | end
46 |
47 | return _M
48 |
--------------------------------------------------------------------------------
/bin/scaffold/nginx/config.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local ogetenv = os.getenv
3 | local utils = require 'bin.scaffold.utils'
4 | local app_run_env = ogetenv("LOR_ENV") or 'dev'
5 |
6 | local lor_ngx_conf = {}
7 | lor_ngx_conf.common = { -- directives
8 | LOR_ENV = app_run_env,
9 | -- INIT_BY_LUA_FILE = './app/nginx/init.lua',
10 | -- LUA_PACKAGE_PATH = '',
11 | -- LUA_PACKAGE_CPATH = '',
12 | CONTENT_BY_LUA_FILE = './app/main.lua',
13 | STATIC_FILE_DIRECTORY = './app/static'
14 | }
15 |
16 | lor_ngx_conf.env = {}
17 | lor_ngx_conf.env.dev = {
18 | LUA_CODE_CACHE = false,
19 | PORT = 8888
20 | }
21 |
22 | lor_ngx_conf.env.test = {
23 | LUA_CODE_CACHE = true,
24 | PORT = 9999
25 | }
26 |
27 | lor_ngx_conf.env.prod = {
28 | LUA_CODE_CACHE = true,
29 | PORT = 80
30 | }
31 |
32 | local function getNgxConf(conf_arr)
33 | if conf_arr['common'] ~= nil then
34 | local common_conf = conf_arr['common']
35 | local env_conf = conf_arr['env'][app_run_env]
36 | for directive, info in pairs(common_conf) do
37 | env_conf[directive] = info
38 | end
39 | return env_conf
40 | elseif conf_arr['env'] ~= nil then
41 | return conf_arr['env'][app_run_env]
42 | end
43 | return {}
44 | end
45 |
46 | local function buildConf()
47 | local sys_ngx_conf = getNgxConf(lor_ngx_conf)
48 | return sys_ngx_conf
49 | end
50 |
51 | local ngx_directive_handle = require('bin.scaffold.nginx.directive'):new(app_run_env)
52 | local ngx_directives = ngx_directive_handle:directiveSets()
53 | local ngx_run_conf = buildConf()
54 |
55 | local LorNgxConf = {}
56 | for directive, func in pairs(ngx_directives) do
57 | if type(func) == 'function' then
58 | local func_rs = func(ngx_directive_handle, ngx_run_conf[directive])
59 | if func_rs ~= false then
60 | LorNgxConf[directive] = func_rs
61 | end
62 | else
63 | LorNgxConf[directive] = ngx_run_conf[directive]
64 | end
65 | end
66 |
67 | return LorNgxConf
68 |
--------------------------------------------------------------------------------
/bin/scaffold/nginx/directive.lua:
--------------------------------------------------------------------------------
1 | -- most code is from https://github.com/idevz/vanilla/blob/master/vanilla/sys/nginx/directive.lua
2 |
3 | package.path = './app/?.lua;' .. package.path
4 | package.cpath = './app/library/?.so;' .. package.cpath
5 |
6 | local Directive = {}
7 |
8 | function Directive:new(env)
9 | local run_env = 'prod'
10 | if env ~= nil then run_env = env end
11 | local instance = {
12 | run_env = run_env,
13 | directiveSets = self.directiveSets
14 | }
15 | setmetatable(instance, Directive)
16 | return instance
17 | end
18 |
19 | function Directive:luaPackagePath(lua_path)
20 | local path = package.path
21 | if lua_path ~= nil then path = lua_path .. path end
22 | local res = [[lua_package_path "]] .. path .. [[;;";]]
23 | return res
24 | end
25 |
26 | function Directive:luaPackageCpath(lua_cpath)
27 | local path = package.cpath
28 | if lua_cpath ~= nil then path = lua_cpath .. path end
29 | local res = [[lua_package_cpath "]] .. path .. [[";]]
30 | return res
31 | end
32 |
33 | function Directive:codeCache(bool_var)
34 | if bool_var == true then bool_var = 'on' else bool_var = 'off' end
35 | local res = [[lua_code_cache ]] .. bool_var.. [[;]]
36 | return res
37 | end
38 |
39 | function Directive:luaSharedDict( lua_lib )
40 | local ok, sh_dict_conf_or_error = pcall(function() return require(lua_lib) end)
41 | if ok == false then
42 | return false
43 | end
44 | local res = ''
45 | if sh_dict_conf_or_error ~= nil then
46 | for name,size in pairs(sh_dict_conf_or_error) do
47 | res = res .. [[lua_shared_dict ]] .. name .. ' ' .. size .. ';'
48 | end
49 | end
50 | return res
51 | end
52 |
53 | function Directive:initByLua(lua_lib)
54 | if lua_lib == nil then return '' end
55 | local res = [[init_by_lua require(']] .. lua_lib .. [['):run();]]
56 | return res
57 | end
58 |
59 | function Directive:initByLuaFile(lua_file)
60 | if lua_file == nil then return '' end
61 | local res = [[init_by_lua_file ]] .. lua_file .. [[;]]
62 | return res
63 | end
64 |
65 | function Directive:initWorkerByLua(lua_lib)
66 | if lua_lib == nil then return '' end
67 | local res = [[init_worker_by_lua require(']] .. lua_lib .. [['):run();]]
68 | return res
69 | end
70 |
71 | function Directive:initWorkerByLuaFile(lua_file)
72 | if lua_file == nil then return '' end
73 | local res = [[init_worker_by_lua_file ]] .. lua_file .. [[;]]
74 | return res
75 | end
76 |
77 | function Directive:setByLua(lua_lib)
78 | if lua_lib == nil then return '' end
79 | local res = [[set_by_lua require(']] .. lua_lib .. [[');]]
80 | return res
81 | end
82 |
83 | function Directive:setByLuaFile(lua_file)
84 | if lua_file == nil then return '' end
85 | local res = [[set_by_lua_file ]] .. lua_file .. [[;]]
86 | return res
87 | end
88 |
89 | function Directive:rewriteByLua(lua_lib)
90 | if lua_lib == nil then return '' end
91 | local res = [[rewrite_by_lua require(']] .. lua_lib .. [['):run();]]
92 | return res
93 | end
94 |
95 | function Directive:rewriteByLuaFile(lua_file)
96 | if lua_file == nil then return '' end
97 | local res = [[rewrite_by_lua_file ]] .. lua_file .. [[;]]
98 | return res
99 | end
100 |
101 | function Directive:accessByLua(lua_lib)
102 | if lua_lib == nil then return '' end
103 | local res = [[access_by_lua require(']] .. lua_lib .. [['):run();]]
104 | return res
105 | end
106 |
107 | function Directive:accessByLuaFile(lua_file)
108 | if lua_file == nil then return '' end
109 | local res = [[access_by_lua_file ]] .. lua_file .. [[;]]
110 | return res
111 | end
112 |
113 | function Directive:contentByLua(lua_lib)
114 | if lua_lib == nil then return '' end
115 | -- local res = [[content_by_lua require(']] .. lua_lib .. [['):run();]]
116 | local res = [[location / {
117 | content_by_lua require(']] .. lua_lib .. [['):run();
118 | }]]
119 | return res
120 | end
121 |
122 | function Directive:contentByLuaFile(lua_file)
123 | if lua_file == nil then return '' end
124 | local res = [[location / {
125 | content_by_lua_file ]] .. lua_file .. [[;
126 | }]]
127 | return res
128 | end
129 |
130 | function Directive:headerFilterByLua(lua_lib)
131 | if lua_lib == nil then return '' end
132 | local res = [[header_filter_by_lua require(']] .. lua_lib .. [['):run();]]
133 | return res
134 | end
135 |
136 | function Directive:headerFilterByLuaFile(lua_file)
137 | if lua_file == nil then return '' end
138 | local res = [[header_filter_by_lua_file ]] .. lua_file .. [[;]]
139 | return res
140 | end
141 |
142 | function Directive:bodyFilterByLua(lua_lib)
143 | if lua_lib == nil then return '' end
144 | local res = [[body_filter_by_lua require(']] .. lua_lib .. [['):run();]]
145 | return res
146 | end
147 |
148 | function Directive:bodyFilterByLuaFile(lua_file)
149 | if lua_file == nil then return '' end
150 | local res = [[body_filter_by_lua_file ]] .. lua_file .. [[;]]
151 | return res
152 | end
153 |
154 | function Directive:logByLua(lua_lib)
155 | if lua_lib == nil then return '' end
156 | local res = [[log_by_lua require(']] .. lua_lib .. [['):run();]]
157 | return res
158 | end
159 |
160 | function Directive:logByLuaFile(lua_file)
161 | if lua_file == nil then return '' end
162 | local res = [[log_by_lua_file ]] .. lua_file .. [[;]]
163 | return res
164 | end
165 |
166 |
167 | function Directive:staticFileDirectory(static_file_directory)
168 | if static_file_directory == nil then return '' end
169 | return static_file_directory
170 | end
171 |
172 |
173 | function Directive:directiveSets()
174 | return {
175 | ['LOR_ENV'] = self.run_env,
176 | ['PORT'] = 80,
177 | ['NGX_PATH'] = '',
178 | ['LUA_PACKAGE_PATH'] = Directive.luaPackagePath,
179 | ['LUA_PACKAGE_CPATH'] = Directive.luaPackageCpath,
180 | ['LUA_CODE_CACHE'] = Directive.codeCache,
181 | ['LUA_SHARED_DICT'] = Directive.luaSharedDict,
182 | ['INIT_BY_LUA'] = Directive.initByLua,
183 | ['INIT_BY_LUA_FILE'] = Directive.initByLuaFile,
184 | ['INIT_WORKER_BY_LUA'] = Directive.initWorkerByLua,
185 | ['INIT_WORKER_BY_LUA_FILE'] = Directive.initWorkerByLuaFile,
186 | ['SET_BY_LUA'] = Directive.setByLua,
187 | ['SET_BY_LUA_FILE'] = Directive.setByLuaFile,
188 | ['REWRITE_BY_LUA'] = Directive.rewriteByLua,
189 | ['REWRITE_BY_LUA_FILE'] = Directive.rewriteByLuaFile,
190 | ['ACCESS_BY_LUA'] = Directive.accessByLua,
191 | ['ACCESS_BY_LUA_FILE'] = Directive.accessByLuaFile,
192 | ['CONTENT_BY_LUA'] = Directive.contentByLua,
193 | ['CONTENT_BY_LUA_FILE'] = Directive.contentByLuaFile,
194 | ['HEADER_FILTER_BY_LUA'] = Directive.headerFilterByLua,
195 | ['HEADER_FILTER_BY_LUA_FILE'] = Directive.headerFilterByLuaFile,
196 | ['BODY_FILTER_BY_LUA'] = Directive.bodyFilterByLua,
197 | ['BODY_FILTER_BY_LUA_FILE'] = Directive.bodyFilterByLuaFile,
198 | ['LOG_BY_LUA'] = Directive.logByLua,
199 | ['LOG_BY_LUA_FILE'] = Directive.logByLuaFile,
200 | ['STATIC_FILE_DIRECTORY'] = Directive.staticFileDirectory
201 | }
202 | end
203 |
204 | return Directive
205 |
--------------------------------------------------------------------------------
/bin/scaffold/nginx/handle.lua:
--------------------------------------------------------------------------------
1 | -- most code is from https://github.com/ostinelli/gin/blob/master/gin/cli/base_launcher.lua
2 | local function create_dirs(necessary_dirs)
3 | for _, dir in pairs(necessary_dirs) do
4 | os.execute("mkdir -p " .. dir .. " > /dev/null")
5 | end
6 | end
7 |
8 | local function create_nginx_conf(nginx_conf_file_path, nginx_conf_content)
9 | local fw = io.open(nginx_conf_file_path, "w")
10 | fw:write(nginx_conf_content)
11 | fw:close()
12 | end
13 |
14 | local function remove_nginx_conf(nginx_conf_file_path)
15 | os.remove(nginx_conf_file_path)
16 | end
17 |
18 | local function nginx_command(env, nginx_conf_file_path, nginx_signal)
19 | local env_cmd = ""
20 |
21 | if env ~= nil then env_cmd = "-g \"env LOR_ENV=" .. env .. ";\"" end
22 | local cmd = "nginx " .. nginx_signal .. " " .. env_cmd .. " -p `pwd`/ -c " .. nginx_conf_file_path
23 | print("execute: " .. cmd)
24 | return os.execute(cmd)
25 | end
26 |
27 | local function start_nginx(env, nginx_conf_file_path)
28 | return nginx_command(env, nginx_conf_file_path, '')
29 | end
30 |
31 | local function stop_nginx(env, nginx_conf_file_path)
32 | return nginx_command(env, nginx_conf_file_path, '-s stop')
33 | end
34 |
35 | local function reload_nginx(env, nginx_conf_file_path)
36 | return nginx_command(env, nginx_conf_file_path, '-s reload')
37 | end
38 |
39 |
40 | local NginxHandle = {}
41 | NginxHandle.__index = NginxHandle
42 |
43 | function NginxHandle.new(necessary_dirs, nginx_conf_content, nginx_conf_file_path)
44 | local instance = {
45 | nginx_conf_content = nginx_conf_content,
46 | nginx_conf_file_path = nginx_conf_file_path,
47 | necessary_dirs = necessary_dirs
48 | }
49 | setmetatable(instance, NginxHandle)
50 | return instance
51 | end
52 |
53 | function NginxHandle:start(env)
54 | create_dirs(self.necessary_dirs)
55 | -- create_nginx_conf(self.nginx_conf_file_path, self.nginx_conf_content)
56 |
57 | return start_nginx(env, self.nginx_conf_file_path)
58 | end
59 |
60 | function NginxHandle:stop(env)
61 | local result = stop_nginx(env, self.nginx_conf_file_path)
62 | -- remove_nginx_conf(self.nginx_conf_file_path)
63 |
64 | return result
65 | end
66 |
67 | function NginxHandle:reload(env)
68 | -- remove_nginx_conf(self.nginx_conf_file_path)
69 | create_dirs(self.necessary_dirs)
70 | -- create_nginx_conf(self.nginx_conf_file_path, self.nginx_conf_content)
71 |
72 | return reload_nginx(env, self.nginx_conf_file_path)
73 | end
74 |
75 | return NginxHandle
76 |
--------------------------------------------------------------------------------
/bin/scaffold/utils.lua:
--------------------------------------------------------------------------------
1 | local pcall = pcall
2 | local require = require
3 | local iopen = io.open
4 | local smatch = string.match
5 |
6 | local Utils = {}
7 |
8 | -- read file
9 | function Utils.read_file(file_path)
10 | local f = iopen(file_path, "rb")
11 | local content = f:read("*a")
12 | f:close()
13 | return content
14 | end
15 |
16 | local function require_module(module_name)
17 | return require(module_name)
18 | end
19 |
20 | -- try to require
21 | function Utils.try_require(module_name, default)
22 | local ok, module_or_err = pcall(require_module, module_name)
23 |
24 | if ok == true then return module_or_err end
25 |
26 | if ok == false and smatch(module_or_err, "'" .. module_name .. "' not found") then
27 | return default
28 | else
29 | error(module_or_err)
30 | end
31 | end
32 |
33 | function Utils.dirname(str)
34 | if str:match(".-/.-") then
35 | local name = string.gsub(str, "(.*/)(.*)", "%1")
36 | return name
37 | else
38 | return ''
39 | end
40 | end
41 |
42 | return Utils
43 |
--------------------------------------------------------------------------------
/dist.ini:
--------------------------------------------------------------------------------
1 | name = lor
2 | abstract = A fast and minimalist web framework based on OpenResty.
3 | version = 0.3.4
4 | author = Sumory Wu (@sumory)
5 | is_original = yes
6 | license = mit
7 | repo_link = https://github.com/sumory/lor
8 | main_module = lib/lor/index.lua
9 | exclude_files = .travis, docker, docs, .travis.yml
10 | requires = bungle/lua-resty-template >= 1.9, p0pr0ck5/lua-resty-cookie >= 0.01
11 |
--------------------------------------------------------------------------------
/lib/lor/index.lua:
--------------------------------------------------------------------------------
1 | local type = type
2 |
3 | local version = require("lor.version")
4 | local Group = require("lor.lib.router.group")
5 | local Router = require("lor.lib.router.router")
6 | local Request = require("lor.lib.request")
7 | local Response = require("lor.lib.response")
8 | local Application = require("lor.lib.application")
9 | local Wrap = require("lor.lib.wrap")
10 |
11 | LOR_FRAMEWORK_DEBUG = false
12 |
13 | local createApplication = function(options)
14 | if options and options.debug and type(options.debug) == 'boolean' then
15 | LOR_FRAMEWORK_DEBUG = options.debug
16 | end
17 |
18 | local app = Application:new()
19 | app:init(options)
20 |
21 | return app
22 | end
23 |
24 | local lor = Wrap:new(createApplication, Router, Group, Request, Response)
25 | lor.version = version
26 |
27 | return lor
28 |
--------------------------------------------------------------------------------
/lib/lor/lib/application.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local type = type
3 | local xpcall = xpcall
4 | local setmetatable = setmetatable
5 |
6 | local Router = require("lor.lib.router.router")
7 | local Request = require("lor.lib.request")
8 | local Response = require("lor.lib.response")
9 | local View = require("lor.lib.view")
10 | local supported_http_methods = require("lor.lib.methods")
11 |
12 | local router_conf = {
13 | strict_route = true,
14 | ignore_case = true,
15 | max_uri_segments = true,
16 | max_fallback_depth = true
17 | }
18 |
19 | local App = {}
20 |
21 | function App:new()
22 | local instance = {}
23 | instance.cache = {}
24 | instance.settings = {}
25 | instance.router = Router:new()
26 |
27 | setmetatable(instance, {
28 | __index = self,
29 | __call = self.handle
30 | })
31 |
32 | instance:init_method()
33 | return instance
34 | end
35 |
36 | function App:run(final_handler)
37 | local request = Request:new()
38 | local response = Response:new()
39 |
40 | local enable_view = self:getconf("view enable")
41 | if enable_view then
42 | local view_config = {
43 | view_enable = enable_view,
44 | view_engine = self:getconf("view engine"), -- view engine: resty-template or others...
45 | view_ext = self:getconf("view ext"), -- defautl is "html"
46 | view_layout = self:getconf("view layout"), -- defautl is ""
47 | views = self:getconf("views") -- template files directory
48 | }
49 |
50 | local view = View:new(view_config)
51 | response.view = view
52 | end
53 |
54 | self:handle(request, response, final_handler)
55 | end
56 |
57 | function App:init(options)
58 | self:default_configuration(options)
59 | end
60 |
61 | function App:default_configuration(options)
62 | options = options or {}
63 |
64 | -- view and template configuration
65 | if options["view enable"] ~= nil and options["view enable"] == true then
66 | self:conf("view enable", true)
67 | else
68 | self:conf("view enable", false)
69 | end
70 | self:conf("view engine", options["view engine"] or "tmpl")
71 | self:conf("view ext", options["view ext"] or "html")
72 | self:conf("view layout", options["view layout"] or "")
73 | self:conf("views", options["views"] or "./app/views/")
74 |
75 | self.locals = {}
76 | self.locals.settings = self.setttings
77 | end
78 |
79 | -- dispatch `req, res` into the pipeline.
80 | function App:handle(req, res, callback)
81 | local router = self.router
82 | local done = callback or function(err)
83 | if err then
84 | if ngx then ngx.log(ngx.ERR, err) end
85 | res:status(500):send("internal error! please check log.")
86 | end
87 | end
88 |
89 | if not router then
90 | return done()
91 | end
92 |
93 | local err_msg
94 | local ok, e = xpcall(function()
95 | router:handle(req, res, done)
96 | end, function(msg)
97 | err_msg = msg
98 | end)
99 |
100 | if not ok then
101 | done(err_msg)
102 | end
103 | end
104 |
105 | function App:use(path, fn)
106 | self:inner_use(3, path, fn)
107 | end
108 |
109 | -- just a mirror for `erroruse`
110 | function App:erruse(path, fn)
111 | self:erroruse(path, fn)
112 | end
113 |
114 | function App:erroruse(path, fn)
115 | self:inner_use(4, path, fn)
116 | end
117 |
118 | -- should be private
119 | function App:inner_use(fn_args_length, path, fn)
120 | local router = self.router
121 |
122 | if path and fn and type(path) == "string" then
123 | router:use(path, fn, fn_args_length)
124 | elseif path and not fn then
125 | fn = path
126 | path = nil
127 | router:use(path, fn, fn_args_length)
128 | else
129 | error("error usage for `middleware`")
130 | end
131 |
132 | return self
133 | end
134 |
135 | function App:init_method()
136 | for http_method, _ in pairs(supported_http_methods) do
137 | self[http_method] = function(_self, path, ...) -- funcs...
138 | _self.router:app_route(http_method, path, ...)
139 | return _self
140 | end
141 | end
142 | end
143 |
144 | function App:all(path, ...)
145 | for http_method, _ in pairs(supported_http_methods) do
146 | self.router:app_route(http_method, path, ...)
147 | end
148 |
149 | return self
150 | end
151 |
152 | function App:conf(setting, val)
153 | self.settings[setting] = val
154 |
155 | if router_conf[setting] == true then
156 | self.router:conf(setting, val)
157 | end
158 |
159 | return self
160 | end
161 |
162 | function App:getconf(setting)
163 | return self.settings[setting]
164 | end
165 |
166 | function App:enable(setting)
167 | self.settings[setting] = true
168 | return self
169 | end
170 |
171 | function App:disable(setting)
172 | self.settings[setting] = false
173 | return self
174 | end
175 |
176 | --- only for dev
177 | function App:gen_graph()
178 | return self.router.trie:gen_graph()
179 | end
180 |
181 | return App
182 |
--------------------------------------------------------------------------------
/lib/lor/lib/debug.lua:
--------------------------------------------------------------------------------
1 | local pcall = pcall
2 | local type = type
3 | local pairs = pairs
4 |
5 |
6 | local function debug(...)
7 | if not LOR_FRAMEWORK_DEBUG then
8 | return
9 | end
10 |
11 | local info = { ... }
12 | if info and type(info[1]) == 'function' then
13 | pcall(function() info[1]() end)
14 | elseif info and type(info[1]) == 'table' then
15 | for i, v in pairs(info[1]) do
16 | print(i, v)
17 | end
18 | elseif ... ~= nil then
19 | print(...)
20 | else
21 | print("debug not works...")
22 | end
23 | end
24 |
25 | return debug
26 |
--------------------------------------------------------------------------------
/lib/lor/lib/holder.lua:
--------------------------------------------------------------------------------
1 | local utils = require("lor.lib.utils.utils")
2 | local ActionHolder = {}
3 |
4 | function ActionHolder:new(func, node, action_type)
5 | local instance = {
6 | id = "action-" .. utils.random(),
7 | node = node,
8 | action_type = action_type,
9 | func = func,
10 | }
11 |
12 | setmetatable(instance, {
13 | __index = self,
14 | __call = self.func
15 | })
16 | return instance
17 | end
18 |
19 |
20 | local NodeHolder = {}
21 |
22 | function NodeHolder:new()
23 | local instance = {
24 | key = "",
25 | val = nil, -- Node
26 | }
27 | setmetatable(instance, { __index = self })
28 | return instance
29 | end
30 |
31 | local Matched = {}
32 |
33 | function Matched:new()
34 | local instance = {
35 | node = nil,
36 | params = {},
37 | pipeline = {},
38 | }
39 | setmetatable(instance, { __index = self })
40 | return instance
41 | end
42 |
43 |
44 | return {
45 | ActionHolder = ActionHolder,
46 | NodeHolder = NodeHolder,
47 | Matched = Matched
48 | }
49 |
--------------------------------------------------------------------------------
/lib/lor/lib/methods.lua:
--------------------------------------------------------------------------------
1 | -- get and post methods is guaranteed, the others is still in process
2 | -- but all these methods shoule work at most cases by default
3 | local supported_http_methods = {
4 | get = true, -- work well
5 | post = true, -- work well
6 | head = true, -- no test
7 | options = true, -- no test
8 | put = true, -- work well
9 | patch = true, -- no test
10 | delete = true, -- work well
11 | trace = true, -- no test
12 | all = true -- todo:
13 | }
14 |
15 | return supported_http_methods
--------------------------------------------------------------------------------
/lib/lor/lib/middleware/cookie.lua:
--------------------------------------------------------------------------------
1 | local ck = require("resty.cookie")
2 |
3 | -- Mind:
4 | -- base on 'lua-resty-cookie', https://github.com/cloudflare/lua-resty-cookie
5 | -- this is the default `cookie` middleware
6 | -- you're recommended to define your own `cookie` middleware.
7 |
8 | -- usage example:
9 | -- app:get("/user", function(req, res, next)
10 | -- local ok, err = req.cookie.set({
11 | -- key = "qq",
12 | -- value = '4==||==hello zhang==||==123456',
13 | -- path = "/",
14 | -- domain = "new.cn",
15 | -- secure = false, --设置后浏览器只有访问https才会把cookie带过来,否则浏览器请求时不带cookie参数
16 | -- httponly = true, --设置后js 无法读取
17 | -- --expires = ngx.cookie_time(os.time() + 3600),
18 | -- max_age = 3600, --用秒来设置cookie的生存期。
19 | -- samesite = "Strict", --或者 Lax 指a域名下收到的cookie 不能通过b域名的表单带过来
20 | -- extension = "a4334aebaece"
21 | -- })
22 | -- end)
23 |
24 | local cookie_middleware = function(cookieConfig)
25 | return function(req, res, next)
26 | local COOKIE, err = ck:new()
27 |
28 | if not COOKIE then
29 | req.cookie = {} -- all cookies
30 | res._cookie = nil
31 | else
32 | req.cookie = {
33 | set = function(...)
34 | local _cookie = COOKIE
35 | if not _cookie then
36 | return ngx.log(ngx.ERR, "response#none _cookie found to write")
37 | end
38 |
39 | local p = ...
40 | if type(p) == "table" then
41 | local ok, err = _cookie:set(p)
42 | if not ok then
43 | return ngx.log(ngx.ERR, err)
44 | end
45 | else
46 | local params = { ... }
47 | local ok, err = _cookie:set({
48 | key = params[1],
49 | value = params[2] or "",
50 | })
51 | if not ok then
52 | return ngx.log(ngx.ERR, err)
53 | end
54 | end
55 | end,
56 |
57 | get = function (name)
58 | local _cookie = COOKIE
59 | local field, err = _cookie:get(name)
60 |
61 | if not field then
62 | return nil
63 | else
64 | return field
65 | end
66 | end,
67 |
68 | get_all = function ()
69 | local _cookie = COOKIE
70 | local fields, err = _cookie:get_all()
71 |
72 | local t = {}
73 | if not fields then
74 | return nil
75 | else
76 | for k, v in pairs(fields) do
77 | if k and v then
78 | t[k] = v
79 | end
80 | end
81 | return t
82 | end
83 | end
84 | }
85 | end
86 |
87 | next()
88 | end
89 | end
90 |
91 | return cookie_middleware
92 |
--------------------------------------------------------------------------------
/lib/lor/lib/middleware/init.lua:
--------------------------------------------------------------------------------
1 | local init_middleware = function(req, res, next)
2 | req.res = res
3 | req.next = next
4 | res.req = req
5 | -- res:set_header('X-Powered-By', 'Lor Framework')
6 | res.locals = res.locals or {}
7 | next()
8 | end
9 |
10 | return init_middleware
11 |
--------------------------------------------------------------------------------
/lib/lor/lib/middleware/session.lua:
--------------------------------------------------------------------------------
1 | local type, xpcall = type, xpcall
2 | local traceback = debug.traceback
3 | local string_sub = string.sub
4 | local string_len = string.len
5 | local http_time = ngx.http_time
6 | local ngx_time = ngx.time
7 | local ck = require("resty.cookie")
8 | local utils = require("lor.lib.utils.utils")
9 | local aes = require("lor.lib.utils.aes")
10 | local base64 = require("lor.lib.utils.base64")
11 |
12 |
13 | local function decode_data(field, aes_key, ase_secret)
14 | if not field or field == "" then return {} end
15 | local payload = base64.decode(field)
16 | local data = {}
17 | local cipher = aes.new()
18 | local decrypt_str = cipher:decrypt(payload, aes_key, ase_secret)
19 | local decode_obj = utils.json_decode(decrypt_str)
20 | return decode_obj or data
21 | end
22 |
23 | local function encode_data(obj, aes_key, ase_secret)
24 | local default = "{}"
25 | local str = utils.json_encode(obj) or default
26 | local cipher = aes.new()
27 | local encrypt_str = cipher:encrypt(str, aes_key, ase_secret)
28 | local encode_encrypt_str = base64.encode(encrypt_str)
29 | return encode_encrypt_str
30 | end
31 |
32 | local function parse_session(field, aes_key, ase_secret)
33 | if not field then return end
34 | return decode_data(field, aes_key, ase_secret)
35 | end
36 |
37 | --- no much secure & performance consideration
38 | --- TODO: optimization & security issues
39 | local session_middleware = function(config)
40 | config = config or {}
41 | config.session_key = config.session_key or "_app_"
42 | if config.refresh_cookie ~= false then
43 | config.refresh_cookie = true
44 | end
45 | if not config.timeout or type(config.timeout) ~= "number" then
46 | config.timeout = 3600 -- default session timeout is 3600 seconds
47 | end
48 |
49 |
50 | local err_tip = "session_aes_key should be set for session middleware"
51 | -- backward compatibility for lor < v0.3.2
52 | config.session_aes_key = config.session_aes_key or "custom_session_aes_key"
53 | if not config.session_aes_key then
54 | ngx.log(ngx.ERR, err_tip)
55 | end
56 |
57 | local session_key = config.session_key
58 | local session_aes_key = config.session_aes_key
59 | local refresh_cookie = config.refresh_cookie
60 | local timeout = config.timeout
61 |
62 | -- session_aes_secret must be 8 charactors to respect lua-resty-string v0.10+
63 | local session_aes_secret = config.session_aes_secret or config.secret or "12345678"
64 | if string_len(session_aes_secret) < 8 then
65 | for i=1,8-string_len(session_aes_secret),1 do
66 | session_aes_secret = session_aes_secret .. "0"
67 | end
68 | end
69 | session_aes_secret = string_sub(session_aes_secret, 1, 8)
70 |
71 | ngx.log(ngx.INFO, "session middleware initialized")
72 | return function(req, res, next)
73 | if not session_aes_key then
74 | return next(err_tip)
75 | end
76 |
77 | local cookie, err = ck:new()
78 | if not cookie then
79 | ngx.log(ngx.ERR, "cookie is nil:", err)
80 | end
81 |
82 | local current_session
83 | local session_data, err = cookie:get(session_key)
84 | if err then
85 | ngx.log(ngx.ERR, "cannot get session_data:", err)
86 | else
87 | if session_data then
88 | current_session = parse_session(session_data, session_aes_key, session_aes_secret)
89 | end
90 | end
91 | current_session = current_session or {}
92 |
93 | req.session = {
94 | set = function(...)
95 | local p = ...
96 | if type(p) == "table" then
97 | for i, v in pairs(p) do
98 | current_session[i] = v
99 | end
100 | else
101 | local params = { ... }
102 | if type(params[2]) == "table" then -- set("k", {1, 2, 3})
103 | current_session[params[1]] = params[2]
104 | else -- set("k", "123")
105 | current_session[params[1]] = params[2] or ""
106 | end
107 | end
108 |
109 | local value = encode_data(current_session, session_aes_key, session_aes_secret)
110 | local expires = http_time(ngx_time() + timeout)
111 | local max_age = timeout
112 | local ok, err = cookie:set({
113 | key = session_key,
114 | value = value or "",
115 | expires = expires,
116 | max_age = max_age,
117 | path = "/"
118 | })
119 |
120 | ngx.log(ngx.INFO, "session.set: ", value)
121 |
122 | if err or not ok then
123 | return ngx.log(ngx.ERR, "session.set error:", err)
124 | end
125 | end,
126 |
127 | refresh = function()
128 | if session_data and session_data ~= "" then
129 | local expires = http_time(ngx_time() + timeout)
130 | local max_age = timeout
131 | local ok, err = cookie:set({
132 | key = session_key,
133 | value = session_data or "",
134 | expires = expires,
135 | max_age = max_age,
136 | path = "/"
137 | })
138 | if err or not ok then
139 | return ngx.log(ngx.ERR, "session.refresh error:", err)
140 | end
141 | end
142 | end,
143 |
144 | get = function(key)
145 | return current_session[key]
146 | end,
147 |
148 | destroy = function()
149 | local expires = "Thu, 01 Jan 1970 00:00:01 GMT"
150 | local max_age = 0
151 | local ok, err = cookie:set({
152 | key = session_key,
153 | value = "",
154 | expires = expires,
155 | max_age = max_age,
156 | path = "/"
157 | })
158 | if err or not ok then
159 | ngx.log(ngx.ERR, "session.destroy error:", err)
160 | return false
161 | end
162 |
163 | return true
164 | end
165 | }
166 |
167 | if refresh_cookie then
168 | local e, ok
169 | ok = xpcall(function()
170 | req.session.refresh()
171 | end, function()
172 | e = traceback()
173 | end)
174 |
175 | if not ok then
176 | ngx.log(ngx.ERR, "refresh cookie error:", e)
177 | end
178 | end
179 |
180 | next()
181 | end
182 | end
183 |
184 | return session_middleware
185 |
--------------------------------------------------------------------------------
/lib/lor/lib/node.lua:
--------------------------------------------------------------------------------
1 | local setmetatable = setmetatable
2 | local type = type
3 | local next = next
4 | local ipairs = ipairs
5 | local table_insert = table.insert
6 | local string_lower = string.lower
7 | local string_format = string.format
8 |
9 | local utils = require("lor.lib.utils.utils")
10 | local supported_http_methods = require("lor.lib.methods")
11 | local ActionHolder = require("lor.lib.holder").ActionHolder
12 | local handler_error_tip = "handler must be `function` that matches `function(req, res, next) ... end`"
13 | local middlware_error_tip = "middlware must be `function` that matches `function(req, res, next) ... end`"
14 | local error_middlware_error_tip = "error middlware must be `function` that matches `function(err, req, res, next) ... end`"
15 | local node_count = 0
16 |
17 | local function gen_node_id()
18 | local prefix = "node-"
19 | local worker_part = "dw"
20 | if ngx and ngx.worker and ngx.worker.id() then
21 | worker_part = ngx.worker.id()
22 | end
23 | node_count = node_count + 1 -- simply count for lua vm level
24 | local unique_part = node_count
25 | local random_part = utils.random()
26 | local node_id = prefix .. worker_part .. "-" .. unique_part .. "-" .. random_part
27 | return node_id
28 | end
29 |
30 | local function check_method(method)
31 | if not method then return false end
32 |
33 | method = string_lower(method)
34 | if not supported_http_methods[method] then
35 | return false
36 | end
37 |
38 | return true
39 | end
40 |
41 | local Node = {}
42 |
43 | function Node:new(root)
44 | local is_root = false
45 | if root == true then
46 | is_root = true
47 | end
48 |
49 | local instance = {
50 | id = gen_node_id(),
51 | is_root = is_root,
52 | name = "",
53 | allow = "",
54 | pattern = "",
55 | endpoint = false,
56 | parent = nil,
57 | colon_parent = nil,
58 | children = {},
59 | colon_child= nil,
60 | handlers = {},
61 | middlewares = {},
62 | error_middlewares = {},
63 | regex = nil
64 | }
65 | setmetatable(instance, {
66 | __index = self,
67 | __tostring = function(s)
68 | local ok, result = pcall(function()
69 | return string_format("name: %s", s.id)
70 | end)
71 | if ok then
72 | return result
73 | else
74 | return "node.tostring() error"
75 | end
76 | end
77 | })
78 | return instance
79 | end
80 |
81 | function Node:find_child(key)
82 | --print("find_child: ", self.id, self.name, self.children)
83 | for _, c in ipairs(self.children) do
84 | if key == c.key then
85 | return c.val
86 | end
87 | end
88 | return nil
89 | end
90 |
91 | function Node:find_handler(method)
92 | method = string_lower(method)
93 | if not self.handlers or not self.handlers[method] or #self.handlers[method] == 0 then
94 | return false
95 | end
96 |
97 | return true
98 | end
99 |
100 | function Node:use(...)
101 | local middlewares = {...}
102 | if not next(middlewares) then
103 | error("middleware should not be nil or empty")
104 | end
105 |
106 | local empty = true
107 | for _, h in ipairs(middlewares) do
108 | if type(h) == "function" then
109 | local action = ActionHolder:new(h, self, "middleware")
110 | table_insert(self.middlewares, action)
111 | empty = false
112 | elseif type(h) == "table" then
113 | for _, hh in ipairs(h) do
114 | if type(hh) == "function" then
115 | local action = ActionHolder:new(hh, self, "middleware")
116 | table_insert(self.middlewares, action)
117 | empty = false
118 | else
119 | error(middlware_error_tip)
120 | end
121 | end
122 | else
123 | error(middlware_error_tip)
124 | end
125 | end
126 |
127 | if empty then
128 | error("middleware should not be empty")
129 | end
130 |
131 | return self
132 | end
133 |
134 | function Node:error_use(...)
135 | local middlewares = {...}
136 | if not next(middlewares) then
137 | error("error middleware should not be nil or empty")
138 | end
139 |
140 | local empty = true
141 | for _, h in ipairs(middlewares) do
142 | if type(h) == "function" then
143 | local action = ActionHolder:new(h, self, "error_middleware")
144 | table_insert(self.error_middlewares, action)
145 | empty = false
146 | elseif type(h) == "table" then
147 | for _, hh in ipairs(h) do
148 | if type(hh) == "function" then
149 | local action = ActionHolder:new(hh, self, "error_middleware")
150 | table_insert(self.error_middlewares, action)
151 | empty = false
152 | else
153 | error(error_middlware_error_tip)
154 | end
155 | end
156 | else
157 | error(error_middlware_error_tip)
158 | end
159 | end
160 |
161 | if empty then
162 | error("error middleware should not be empty")
163 | end
164 |
165 | return self
166 | end
167 |
168 | function Node:handle(method, ...)
169 | method = string_lower(method)
170 | if not check_method(method) then
171 | error("error method: ", method or "nil")
172 | end
173 |
174 | if self:find_handler(method) then
175 | error("[" .. self.pattern .. "] " .. method .. " handler exists yet!")
176 | end
177 |
178 | if not self.handlers[method] then
179 | self.handlers[method] = {}
180 | end
181 |
182 | local empty = true
183 | local handlers = {...}
184 | if not next(handlers) then
185 | error("handler should not be nil or empty")
186 | end
187 |
188 | for _, h in ipairs(handlers) do
189 | if type(h) == "function" then
190 | local action = ActionHolder:new(h, self, "handler")
191 | table_insert(self.handlers[method], action)
192 | empty = false
193 | elseif type(h) == "table" then
194 | for _, hh in ipairs(h) do
195 | if type(hh) == "function" then
196 | local action = ActionHolder:new(hh, self, "handler")
197 | table_insert(self.handlers[method], action)
198 | empty = false
199 | else
200 | error(handler_error_tip)
201 | end
202 | end
203 | else
204 | error(handler_error_tip)
205 | end
206 | end
207 |
208 | if empty then
209 | error("handler should not be empty")
210 | end
211 |
212 | if self.allow == "" then
213 | self.allow = method
214 | else
215 | self.allow = self.allow .. ", " .. method
216 | end
217 |
218 | return self
219 | end
220 |
221 | function Node:get_allow()
222 | return self.allow
223 | end
224 |
225 | function Node:remove_nested_property(node)
226 | if not node then return end
227 | if node.parent then
228 | node.parent = nil
229 | end
230 |
231 | if node.colon_child then
232 | if node.colon_child.handlers then
233 | for _, h in pairs(node.colon_child.handlers) do
234 | if h then
235 | for _, action in ipairs(h) do
236 | action.func = nil
237 | action.node = nil
238 | end
239 | end
240 | end
241 | end
242 | self:remove_nested_property(node.colon_child)
243 | end
244 |
245 | local children = node.children
246 | if children and #children > 0 then
247 | for _, v in ipairs(children) do
248 | local c = v.val
249 | if c.handlers then -- remove action func
250 | for _, h in pairs(c.handlers) do
251 | if h then
252 | for _, action in ipairs(h) do
253 | action.func = nil
254 | action.node = nil
255 | end
256 | end
257 | end
258 | end
259 |
260 | self:remove_nested_property(v.val)
261 | end
262 | end
263 | end
264 |
265 | return Node
266 |
--------------------------------------------------------------------------------
/lib/lor/lib/request.lua:
--------------------------------------------------------------------------------
1 | local sfind = string.find
2 | local pairs = pairs
3 | local type = type
4 | local setmetatable = setmetatable
5 | local utils = require("lor.lib.utils.utils")
6 |
7 | local Request = {}
8 |
9 | -- new request: init args/params/body etc from http request
10 | function Request:new()
11 | local body = {} -- body params
12 | local headers = ngx.req.get_headers()
13 |
14 | local header = headers['Content-Type']
15 | -- the post request have Content-Type header set
16 | if header then
17 | if sfind(header, "application/x-www-form-urlencoded", 1, true) then
18 | ngx.req.read_body()
19 | local post_args = ngx.req.get_post_args()
20 | if post_args and type(post_args) == "table" then
21 | for k,v in pairs(post_args) do
22 | body[k] = v
23 | end
24 | end
25 | elseif sfind(header, "application/json", 1, true) then
26 | ngx.req.read_body()
27 | local json_str = ngx.req.get_body_data()
28 | body = utils.json_decode(json_str)
29 | -- form-data request
30 | elseif sfind(header, "multipart", 1, true) then
31 | -- upload request, should not invoke ngx.req.read_body()
32 | -- parsed as raw by default
33 | else
34 | ngx.req.read_body()
35 | body = ngx.req.get_body_data()
36 | end
37 | -- the post request have no Content-Type header set will be parsed as x-www-form-urlencoded by default
38 | else
39 | ngx.req.read_body()
40 | local post_args = ngx.req.get_post_args()
41 | if post_args and type(post_args) == "table" then
42 | for k,v in pairs(post_args) do
43 | body[k] = v
44 | end
45 | end
46 | end
47 |
48 | local instance = {
49 | path = ngx.var.uri, -- uri
50 | method = ngx.req.get_method(),
51 | query = ngx.req.get_uri_args(),
52 | params = {},
53 | body = body,
54 | body_raw = ngx.req.get_body_data(),
55 | url = ngx.var.request_uri,
56 | origin_uri = ngx.var.request_uri,
57 | uri = ngx.var.request_uri,
58 | headers = headers, -- request headers
59 |
60 | req_args = ngx.var.args,
61 | found = false -- 404 or not
62 | }
63 | setmetatable(instance, { __index = self })
64 | return instance
65 | end
66 |
67 | function Request:is_found()
68 | return self.found
69 | end
70 |
71 | function Request:set_found(found)
72 | self.found = found
73 | end
74 |
75 | return Request
76 |
--------------------------------------------------------------------------------
/lib/lor/lib/response.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local type = type
3 | local setmetatable = setmetatable
4 | local tinsert = table.insert
5 | local tconcat = table.concat
6 | local utils = require("lor.lib.utils.utils")
7 |
8 | local Response = {}
9 |
10 | function Response:new()
11 | --ngx.status = 200
12 | local instance = {
13 | http_status = nil,
14 | headers = {},
15 | locals = {},
16 | body = '--default body. you should not see this by default--',
17 | view = nil
18 | }
19 |
20 | setmetatable(instance, { __index = self })
21 | return instance
22 | end
23 |
24 | -- todo: optimize-compile before used
25 | function Response:render(view_file, data)
26 | if not self.view then
27 | ngx.log(ngx.ERR, "`view` object is nil, maybe you disabled the view engine.")
28 | error("`view` object is nil, maybe you disabled the view engine.")
29 | else
30 | self:set_header('Content-Type', 'text/html; charset=UTF-8')
31 | data = data or {}
32 | data.locals = self.locals -- inject res.locals
33 |
34 | local body = self.view:render(view_file, data)
35 | self:_send(body)
36 | end
37 | end
38 |
39 |
40 | function Response:html(data)
41 | self:set_header('Content-Type', 'text/html; charset=UTF-8')
42 | self:_send(data)
43 | end
44 |
45 | function Response:json(data, empty_table_as_object)
46 | self:set_header('Content-Type', 'application/json; charset=utf-8')
47 | self:_send(utils.json_encode(data, empty_table_as_object))
48 | end
49 |
50 | function Response:redirect(url, code, query)
51 | if url and not code and not query then -- only one param
52 | ngx.redirect(url)
53 | elseif url and code and not query then -- two param
54 | if type(code) == "number" then
55 | ngx.redirect(url ,code)
56 | elseif type(code) == "table" then
57 | query = code
58 | local q = {}
59 | local is_q_exist = false
60 | if query and type(query) == "table" then
61 | for i,v in pairs(query) do
62 | tinsert(q, i .. "=" .. v)
63 | is_q_exist = true
64 | end
65 | end
66 |
67 | if is_q_exist then
68 | url = url .. "?" .. tconcat(q, "&")
69 | end
70 |
71 | ngx.redirect(url)
72 | else
73 | ngx.redirect(url)
74 | end
75 | else -- three param
76 | local q = {}
77 | local is_q_exist = false
78 | if query and type(query) == "table" then
79 | for i,v in pairs(query) do
80 | tinsert(q, i .. "=" .. v)
81 | is_q_exist = true
82 | end
83 | end
84 |
85 | if is_q_exist then
86 | url = url .. "?" .. tconcat(q, "&")
87 | end
88 | ngx.redirect(url ,code)
89 | end
90 | end
91 |
92 | function Response:location(url, data)
93 | if data and type(data) == "table" then
94 | ngx.req.set_uri_args(data)
95 | ngx.req.set_uri(url, false)
96 | else
97 | ngx.say(url)
98 | ngx.req.set_uri(url, false)
99 | end
100 | end
101 |
102 | function Response:send(text)
103 | self:set_header('Content-Type', 'text/plain; charset=UTF-8')
104 | self:_send(text)
105 | end
106 |
107 | --~=============================================================
108 |
109 | function Response:_send(content)
110 | ngx.status = self.http_status or 200
111 | ngx.say(content)
112 | end
113 |
114 | function Response:get_body()
115 | return self.body
116 | end
117 |
118 | function Response:get_headers()
119 | return self.headers
120 | end
121 |
122 | function Response:get_header(key)
123 | return self.headers[key]
124 | end
125 |
126 | function Response:set_body(body)
127 | if body ~= nil then self.body = body end
128 | end
129 |
130 | function Response:status(status)
131 | ngx.status = status
132 | self.http_status = status
133 | return self
134 | end
135 |
136 | function Response:set_header(key, value)
137 | ngx.header[key] = value
138 | end
139 |
140 | return Response
141 |
--------------------------------------------------------------------------------
/lib/lor/lib/router/group.lua:
--------------------------------------------------------------------------------
1 | local setmetatable = setmetatable
2 | local pairs = pairs
3 | local type = type
4 | local error = error
5 | local next = next
6 | local string_format = string.format
7 | local string_lower = string.lower
8 | local table_insert = table.insert
9 | local unpack = table.unpack or unpack
10 |
11 | local supported_http_methods = require("lor.lib.methods")
12 | local debug = require("lor.lib.debug")
13 | local utils = require("lor.lib.utils.utils")
14 | local random = utils.random
15 | local clone = utils.clone
16 | local handler_error_tip = "handler must be `function` that matches `function(req, res, next) ... end`"
17 |
18 | local Group = {}
19 |
20 | function Group:new()
21 | local group = {}
22 |
23 | group.id = random()
24 | group.name = "group-" .. group.id
25 | group.is_group = true
26 | group.apis = {}
27 | self:build_method()
28 |
29 | setmetatable(group, {
30 | __index = self,
31 | __call = self._call,
32 | __tostring = function(s)
33 | return s.name
34 | end
35 | })
36 |
37 | return group
38 | end
39 |
40 | --- a magick for usage like `lor:Router()`
41 | -- generate a new group for different routes group
42 | function Group:_call()
43 | local cloned = clone(self)
44 | cloned.id = random()
45 | cloned.name = cloned.name .. ":clone-" .. cloned.id
46 | return cloned
47 | end
48 |
49 | function Group:get_apis()
50 | return self.apis
51 | end
52 |
53 | function Group:set_api(path, method, ...)
54 | if not path or not method then
55 | return error("`path` & `method` should not be nil.")
56 | end
57 |
58 | local handlers = {...}
59 | if not next(handlers) then
60 | return error("handler should not be nil or empty")
61 | end
62 |
63 | if type(path) ~= "string" or type(method) ~= "string" or type(handlers) ~= "table" then
64 | return error("params type error.")
65 | end
66 |
67 | local extended_handlers = {}
68 | for _, h in ipairs(handlers) do
69 | if type(h) == "function" then
70 | table_insert(extended_handlers, h)
71 | elseif type(h) == "table" then
72 | for _, hh in ipairs(h) do
73 | if type(hh) == "function" then
74 | table_insert(extended_handlers, hh)
75 | else
76 | error(handler_error_tip)
77 | end
78 | end
79 | else
80 | error(handler_error_tip)
81 | end
82 | end
83 |
84 | method = string_lower(method)
85 | if not supported_http_methods[method] then
86 | return error(string_format("[%s] method is not supported yet.", method))
87 | end
88 |
89 | self.apis[path] = self.apis[path] or {}
90 | self.apis[path][method] = extended_handlers
91 | end
92 |
93 | function Group:build_method()
94 | for m, _ in pairs(supported_http_methods) do
95 | m = string_lower(m)
96 |
97 | -- 1. group_router:get(func1)
98 | -- 2. group_router:get(func1, func2)
99 | -- 3. group_router:get({func1, func2})
100 | -- 4. group_router:get(path, func1)
101 | -- 5. group_router:get(path, func1, func2)
102 | -- 6. group_router:get(path, {func1, func2})
103 | Group[m] = function(myself, ...)
104 | local params = {...}
105 | if not next(params) then return error("params should not be nil or empty") end
106 |
107 | -- case 1 or 3
108 | if #params == 1 then
109 | if type(params[1]) ~= "function" and type(params[1]) ~= "table" then
110 | return error("it must be an function if there's only one param")
111 | end
112 |
113 | if type(params[1]) == "table" and #(params[1]) == 0 then
114 | return error("params should not be nil or empty")
115 | end
116 |
117 | return Group.set_api(myself, "", m, ...)
118 | end
119 |
120 | -- case 2,4,5,6
121 | if #params > 1 then
122 | if type(params[1]) == "string" then -- case 4,5,6
123 | return Group.set_api(myself, params[1], m, unpack(params, 2))
124 | else -- case 2
125 | return Group.set_api(myself, "", m, ...)
126 | end
127 | end
128 |
129 | error("error params for group route define")
130 | end
131 | end
132 | end
133 |
134 | function Group:clone()
135 | local cloned = clone(self)
136 | cloned.id = random()
137 | cloned.name = cloned.name .. ":clone-" .. cloned.id
138 | return cloned
139 | end
140 |
141 | return Group
142 |
--------------------------------------------------------------------------------
/lib/lor/lib/router/router.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local ipairs = ipairs
3 | local pcall = pcall
4 | local xpcall = xpcall
5 | local type = type
6 | local error = error
7 | local setmetatable = setmetatable
8 | local traceback = debug.traceback
9 | local tinsert = table.insert
10 | local table_concat = table.concat
11 | local string_format = string.format
12 | local string_lower = string.lower
13 |
14 | local utils = require("lor.lib.utils.utils")
15 | local supported_http_methods = require("lor.lib.methods")
16 | local debug = require("lor.lib.debug")
17 | local Trie = require("lor.lib.trie")
18 | local random = utils.random
19 | local mixin = utils.mixin
20 |
21 | local allowed_conf = {
22 | strict_route = {
23 | t = "boolean"
24 | },
25 | ignore_case = {
26 | t = "boolean"
27 | },
28 | max_uri_segments = {
29 | t = "number"
30 | },
31 | max_fallback_depth = {
32 | t = "number"
33 | },
34 | }
35 |
36 | local function restore(fn, obj)
37 | local origin = {
38 | path = obj['path'],
39 | query = obj['query'],
40 | next = obj['next'],
41 | locals = obj['locals'],
42 | }
43 |
44 | return function(err)
45 | obj['path'] = origin.path
46 | obj['query'] = origin.query
47 | obj['next'] = origin.next
48 | obj['locals'] = origin.locals
49 | fn(err)
50 | end
51 | end
52 |
53 | local function compose_func(matched, method)
54 | if not matched or type(matched.pipeline) ~= "table" then
55 | return nil
56 | end
57 |
58 | local exact_node = matched.node
59 | local pipeline = matched.pipeline or {}
60 | if not exact_node or not pipeline then
61 | return nil
62 | end
63 |
64 | local stack = {}
65 | for _, p in ipairs(pipeline) do
66 | local middlewares = p.middlewares
67 | local handlers = p.handlers
68 | if middlewares then
69 | for _, middleware in ipairs(middlewares) do
70 | tinsert(stack, middleware)
71 | end
72 | end
73 |
74 | if p.id == exact_node.id and handlers and handlers[method] then
75 | for _, handler in ipairs(handlers[method]) do
76 | tinsert(stack, handler)
77 | end
78 | end
79 | end
80 |
81 | return stack
82 | end
83 |
84 | local function compose_error_handler(node)
85 | if not node then
86 | return nil
87 | end
88 |
89 | local stack = {}
90 | while node do
91 | for _, middleware in ipairs(node.error_middlewares) do
92 | tinsert(stack, middleware)
93 | end
94 | node = node.parent
95 | end
96 |
97 | return stack
98 | end
99 |
100 |
101 | local Router = {}
102 |
103 | function Router:new(options)
104 | local opts = options or {}
105 | local router = {}
106 |
107 | router.name = "router-" .. random()
108 | router.trie = Trie:new({
109 | ignore_case = opts.ignore_case,
110 | strict_route = opts.strict_route,
111 | max_uri_segments = opts.max_uri_segments,
112 | max_fallback_depth = opts.max_fallback_depth
113 | })
114 |
115 | self:init()
116 | setmetatable(router, {
117 | __index = self,
118 | __tostring = function(s)
119 | local ok, result = pcall(function()
120 | return string_format("name: %s", s.name)
121 | end)
122 | if ok then
123 | return result
124 | else
125 | return "router.tostring() error"
126 | end
127 | end
128 | })
129 |
130 | return router
131 | end
132 |
133 | --- a magick to convert `router()` to `router:handle()`
134 | -- so a router() could be regarded as a `middleware`
135 | function Router:call()
136 | return function(req, res, next)
137 | return self:handle(req, res, next)
138 | end
139 | end
140 |
141 | -- dispatch a request
142 | function Router:handle(req, res, out)
143 | local path = req.path
144 | if not path or path == "" then
145 | path = ""
146 | end
147 | local method = req.method and string_lower(req.method)
148 | local done = out
149 |
150 | local stack = nil
151 | local matched = self.trie:match(path)
152 | local matched_node = matched.node
153 |
154 | if not method or not matched_node then
155 | if res.status then res:status(404) end
156 | return self:error_handle("404! not found.", req, res, self.trie.root, done)
157 | else
158 | local matched_handlers = matched_node.handlers and matched_node.handlers[method]
159 | if not matched_handlers or #matched_handlers <= 0 then
160 | return self:error_handle("Oh! no handler to process method: " .. method, req, res, self.trie.root, done)
161 | end
162 |
163 | stack = compose_func(matched, method)
164 | if not stack or #stack <= 0 then
165 | return self:error_handle("Oh! no handlers found.", req, res, self.trie.root, done)
166 | end
167 | end
168 |
169 | local stack_len = #stack
170 | req:set_found(true)
171 | local parsed_params = matched.params or {} -- origin params, parsed
172 | req.params = parsed_params
173 |
174 | local idx = 0
175 | local function next(err)
176 | if err then
177 | return self:error_handle(err, req, res, stack[idx].node, done)
178 | end
179 |
180 | if idx > stack_len then
181 | return done(err) -- err is nil or not
182 | end
183 |
184 | idx = idx + 1
185 | local handler = stack[idx]
186 | if not handler then
187 | return done(err)
188 | end
189 |
190 | local err_msg
191 | local ok, ee = xpcall(function()
192 | handler.func(req, res, next)
193 | req.params = mixin(parsed_params, req.params)
194 | end, function(msg)
195 | if msg then
196 | if type(msg) == "string" then
197 | err_msg = msg
198 | elseif type(msg) == "table" then
199 | err_msg = "[ERROR]" .. table_concat(msg, "|") .. "[/ERROR]"
200 | end
201 | else
202 | err_msg = ""
203 | end
204 | err_msg = err_msg .. "\n" .. traceback()
205 | end)
206 |
207 | if not ok then
208 | --debug("handler func:call error ---> to error_handle,", ok, "err_msg:", err_msg)
209 | return self:error_handle(err_msg, req, res, handler.node, done)
210 | end
211 | end
212 |
213 | next()
214 | end
215 |
216 | -- dispatch an error
217 | function Router:error_handle(err_msg, req, res, node, done)
218 | local stack = compose_error_handler(node)
219 | if not stack or #stack <= 0 then
220 | return done(err_msg)
221 | end
222 |
223 | local idx = 0
224 | local stack_len = #stack
225 | local function next(err)
226 | if idx >= stack_len then
227 | return done(err)
228 | end
229 |
230 | idx = idx + 1
231 | local error_handler = stack[idx]
232 | if not error_handler then
233 | return done(err)
234 | end
235 |
236 | local ok, ee = xpcall(function()
237 | error_handler.func(err, req, res, next)
238 | end, function(msg)
239 | if msg then
240 | if type(msg) == "string" then
241 | err_msg = msg
242 | elseif type(msg) == "table" then
243 | err_msg = "[ERROR]" .. table_concat(msg, "|") .. "[/ERROR]"
244 | end
245 | else
246 | err_msg = ""
247 | end
248 |
249 | err_msg = string_format("%s\n[ERROR in ErrorMiddleware#%s(%s)] %s \n%s", err, idx, error_handler.id, err_msg, traceback())
250 | end)
251 |
252 | if not ok then
253 | return done(err_msg)
254 | end
255 | end
256 |
257 | next(err_msg)
258 | end
259 |
260 | function Router:use(path, fn, fn_args_length)
261 | if type(fn) == "function" then -- fn is a function
262 | local node
263 | if not path then
264 | node = self.trie.root
265 | else
266 | node = self.trie:add_node(path)
267 | end
268 | if fn_args_length == 3 then
269 | node:use(fn)
270 | elseif fn_args_length == 4 then
271 | node:error_use(fn)
272 | end
273 | elseif fn and fn.is_group == true then -- fn is a group router
274 | if fn_args_length ~= 3 then
275 | error("illegal param, fn_args_length should be 3")
276 | end
277 |
278 | path = path or "" -- if path is nil, then mount it on `root`
279 | self:merge_group(path, fn)
280 | end
281 |
282 | return self
283 | end
284 |
285 | function Router:merge_group(prefix, group)
286 | local apis = group:get_apis()
287 |
288 | if apis then
289 | for uri, api_methods in pairs(apis) do
290 | if type(api_methods) == "table" then
291 | local path
292 | if uri == "" then -- for group index route
293 | path = utils.clear_slash(prefix)
294 | else
295 | path = utils.clear_slash(prefix .. "/" .. uri)
296 | end
297 |
298 | local node = self.trie:add_node(path)
299 | if not node then
300 | return error("cann't define node on router trie, path:" .. path)
301 | end
302 |
303 | for method, handlers in pairs(api_methods) do
304 | local m = string_lower(method)
305 | if supported_http_methods[m] == true then
306 | node:handle(m, handlers)
307 | end -- supported method
308 | end
309 | end
310 | end
311 | end -- ugly arrow style for missing `continue`
312 |
313 | return self
314 | end
315 |
316 | function Router:app_route(http_method, path, ...)
317 | local node = self.trie:add_node(path)
318 | node:handle(http_method, ...)
319 | return self
320 | end
321 |
322 | function Router:init()
323 | for http_method, _ in pairs(supported_http_methods) do
324 | self[http_method] = function(s, path, ...)
325 | local node = s.trie:add_node(path)
326 | node:handle(http_method, ...)
327 | return s
328 | end
329 | end
330 | end
331 |
332 | function Router:conf(setting, val)
333 | local allow = allowed_conf[setting]
334 | if allow then
335 | if allow.t == "boolean" then
336 |
337 | if val == "true" or val == true then
338 | self.trie[setting] = true
339 | elseif val == "false" or val == false then
340 | self.trie[setting] = false
341 | end
342 | elseif allow.t == "number" then
343 | val = tonumber(val)
344 | self.trie[setting] = val or self[setting]
345 | end
346 | end
347 |
348 | return self
349 | end
350 |
351 | return Router
352 |
--------------------------------------------------------------------------------
/lib/lor/lib/trie.lua:
--------------------------------------------------------------------------------
1 | local setmetatable = setmetatable
2 | local tonumber = tonumber
3 | local string_lower = string.lower
4 | local string_find = string.find
5 | local string_sub = string.sub
6 | local string_gsub = string.gsub
7 | local string_len = string.len
8 | local string_format = string.format
9 | local table_insert = table.insert
10 | local table_remove = table.remove
11 | local table_concat = table.concat
12 |
13 | local utils = require("lor.lib.utils.utils")
14 | local holder = require("lor.lib.holder")
15 | local Node = require("lor.lib.node")
16 | local NodeHolder = holder.NodeHolder
17 | local Matched = holder.Matched
18 | local mixin = utils.mixin
19 | local valid_segment_tip = "valid path should only contains: [A-Za-z0-9._%-~]"
20 |
21 |
22 | local function check_segment(segment)
23 | local tmp = string_gsub(segment, "([A-Za-z0-9._%%-~]+)", "")
24 | if tmp ~= "" then
25 | return false
26 | end
27 | return true
28 | end
29 |
30 | local function check_colon_child(node, colon_child)
31 | if not node or not colon_child then
32 | return false, nil
33 | end
34 |
35 | if node.name ~= colon_child.name or node.regex ~= colon_child.regex then
36 | return false, colon_child
37 | end
38 |
39 | return true, nil -- could be added
40 | end
41 |
42 | local function get_or_new_node(parent, frag, ignore_case)
43 | if not frag or frag == "/" or frag == "" then
44 | frag = ""
45 | end
46 |
47 | if ignore_case == true then
48 | frag = string_lower(frag)
49 | end
50 |
51 | local node = parent:find_child(frag)
52 | if node then
53 | return node
54 | end
55 |
56 | node = Node:new()
57 | node.parent = parent
58 |
59 | if frag == "" then
60 | local nodePack = NodeHolder:new()
61 | nodePack.key = frag
62 | nodePack.val = node
63 | table_insert(parent.children, nodePack)
64 | else
65 | local first = string_sub(frag, 1, 1)
66 | if first == ":" then
67 | local name = string_sub(frag, 2)
68 | local trailing = string_sub(name, -1)
69 |
70 | if trailing == ')' then
71 | local index = string_find(name, "%(")
72 | if index and index > 1 then
73 | local regex = string_sub(name, index+1, #name-1)
74 | if #regex > 0 then
75 | name = string_sub(name, 1, index-1 )
76 | node.regex = regex
77 | else
78 | error("invalid pattern[1]: " .. frag)
79 | end
80 | end
81 | end
82 |
83 | local is_name_valid = check_segment(name)
84 | if not is_name_valid then
85 | error("invalid pattern[2], illegal path:" .. name .. ", " .. valid_segment_tip)
86 | end
87 | node.name = name
88 |
89 | local colon_child = parent.colon_child
90 | if colon_child then
91 | local valid, conflict = check_colon_child(node, colon_child)
92 | if not valid then
93 | error("invalid pattern[3]: [" .. name .. "] conflict with [" .. conflict.name .. "]")
94 | else
95 | return colon_child
96 | end
97 | end
98 |
99 | parent.colon_child = node
100 | else
101 | local is_name_valid = check_segment(frag)
102 | if not is_name_valid then
103 | error("invalid pattern[6]: " .. frag .. ", " .. valid_segment_tip)
104 | end
105 |
106 | local nodePack = NodeHolder:new()
107 | nodePack.key = frag
108 | nodePack.val = node
109 | table_insert(parent.children, nodePack)
110 | end
111 | end
112 |
113 | return node
114 | end
115 |
116 | local function insert_node(parent, frags, ignore_case)
117 | local frag = frags[1]
118 | local child = get_or_new_node(parent, frag, ignore_case)
119 |
120 | if #frags >= 1 then
121 | table_remove(frags, 1)
122 | end
123 |
124 | if #frags == 0 then
125 | child.endpoint = true
126 | return child
127 | end
128 |
129 | return insert_node(child, frags, ignore_case)
130 | end
131 |
132 | local function get_pipeline(node)
133 | local pipeline = {}
134 | if not node then return pipeline end
135 |
136 | local tmp = {}
137 | local origin_node = node
138 | table_insert(tmp, origin_node)
139 | while node.parent
140 | do
141 | table_insert(tmp, node.parent)
142 | node = node.parent
143 | end
144 |
145 | for i = #tmp, 1, -1 do
146 | table_insert(pipeline, tmp[i])
147 | end
148 |
149 | return pipeline
150 | end
151 |
152 |
153 | local Trie = {}
154 |
155 | function Trie:new(opts)
156 | opts = opts or {}
157 | local trie = {
158 | -- limit to avoid dead `while` or attack for fallback lookup
159 | max_fallback_depth = 100,
160 |
161 | -- limit to avoid uri attack. e.g. a long uri, /a/b/c/d/e/f/g/h/i/j/k...
162 | max_uri_segments = 100,
163 |
164 | -- should ignore case or not
165 | ignore_case = true,
166 |
167 | -- [true]: "test.com/" is not the same with "test.com".
168 | -- [false]: "test.com/" will match "test.com/" first, then try to math "test.com" if not exists
169 | strict_route = true,
170 |
171 | -- the root node of this trie structure
172 | root = Node:new(true)
173 | }
174 |
175 | trie.max_fallback_depth = tonumber(opts.max_fallback_depth) or trie.max_fallback_depth
176 | trie.max_uri_segments = tonumber(opts.max_uri_segments) or trie.max_uri_segments
177 | trie.ignore_case = opts.ignore_case or trie.ignore_case
178 | trie.strict_route = not (opts.strict_route == false)
179 |
180 | setmetatable(trie, {
181 | __index = self,
182 | __tostring = function(s)
183 | return string_format("Trie, ignore_case:%s strict_route:%s max_uri_segments:%d max_fallback_depth:%d",
184 | s.ignore_case, s.strict_route, s.max_uri_segments, s.max_fallback_depth)
185 | end
186 | })
187 |
188 | return trie
189 | end
190 |
191 | function Trie:add_node(pattern)
192 | pattern = utils.trim_path_spaces(pattern)
193 |
194 | if string_find(pattern, "//") then
195 | error("`//` is not allowed: " .. pattern)
196 | end
197 |
198 | local tmp_pattern = utils.trim_prefix_slash(pattern)
199 | local tmp_segments = utils.split(tmp_pattern, "/")
200 |
201 | local node = insert_node(self.root, tmp_segments, self.ignore_case)
202 | if node.pattern == "" then
203 | node.pattern = pattern
204 | end
205 |
206 | return node
207 | end
208 |
209 | --- get matched colon node
210 | function Trie:get_colon_node(parent, segment)
211 | local child = parent.colon_child
212 | if child and child.regex and not utils.is_match(segment, child.regex) then
213 | child = nil -- illegal & not mathed regrex
214 | end
215 | return child
216 | end
217 |
218 | --- retry to fallback to lookup the colon nodes in `stack`
219 | function Trie:fallback_lookup(fallback_stack, segments, params)
220 | if #fallback_stack == 0 then
221 | return false
222 | end
223 |
224 | local fallback = table_remove(fallback_stack, #fallback_stack)
225 | local segment_index = fallback.segment_index
226 | local parent = fallback.colon_node
227 | local matched = Matched:new()
228 |
229 | if parent.name ~= "" then -- fallback to the colon node and fill param if matched
230 | matched.params[parent.name] = segments[segment_index]
231 | end
232 | mixin(params, matched.params) -- mixin params parsed before
233 |
234 | local flag = true
235 | for i, s in ipairs(segments) do
236 | if i <= segment_index then -- mind: should use <= not <
237 | -- continue
238 | else
239 | local node, colon_node, is_same = self:find_matched_child(parent, s)
240 | if self.ignore_case and node == nil then
241 | node, colon_node, is_same = self:find_matched_child(parent, string_lower(s))
242 | end
243 |
244 | if colon_node and not is_same then
245 | -- save colon node to fallback stack
246 | table_insert(fallback_stack, {
247 | segment_index = i,
248 | colon_node = colon_node
249 | })
250 | end
251 |
252 | if node == nil then -- both exact child and colon child is nil
253 | flag = false -- should not set parent value
254 | break
255 | end
256 |
257 | parent = node
258 | end
259 | end
260 |
261 | if flag and parent.endpoint then
262 | matched.node = parent
263 | matched.pipeline = get_pipeline(parent)
264 | end
265 |
266 | if matched.node then
267 | return matched
268 | else
269 | return false
270 | end
271 | end
272 |
273 | --- find exactly mathed node and colon node
274 | function Trie:find_matched_child(parent, segment)
275 | local child = parent:find_child(segment)
276 | local colon_node = self:get_colon_node(parent, segment)
277 |
278 | if child then
279 | if colon_node then
280 | return child, colon_node, false
281 | else
282 | return child, nil, false
283 | end
284 | else -- not child
285 | if colon_node then
286 | return colon_node, colon_node, true -- 后续不再压栈
287 | else
288 | return nil, nil, false
289 | end
290 | end
291 | end
292 |
293 | function Trie:match(path)
294 | if not path or path == "" then
295 | error("`path` should not be nil or empty")
296 | end
297 |
298 | path = utils.slim_path(path)
299 |
300 | local first = string_sub(path, 1, 1)
301 | if first ~= '/' then
302 | error("`path` is not start with prefix /: " .. path)
303 | end
304 |
305 | if path == "" then -- special case: regard "test.com" as "test.com/"
306 | path = "/"
307 | end
308 |
309 | local matched = self:_match(path)
310 | if not matched.node and self.strict_route ~= true then
311 | if string_sub(path, -1) == '/' then -- retry to find path without last slash
312 | matched = self:_match(string_sub(path, 1, -2))
313 | end
314 | end
315 |
316 | return matched
317 | end
318 |
319 | function Trie:_match(path)
320 | local start_pos = 2
321 | local end_pos = string_len(path) + 1
322 | local segments = {}
323 | for i = 2, end_pos, 1 do -- should set max depth to avoid attack
324 | if i < end_pos and string_sub(path, i, i) ~= '/' then
325 | -- continue
326 | else
327 | local segment = string_sub(path, start_pos, i-1)
328 | table_insert(segments, segment)
329 | start_pos = i + 1
330 | end
331 | end
332 |
333 | local flag = true -- whether to continue to find matched node or not
334 | local matched = Matched:new()
335 | local parent = self.root
336 | local fallback_stack = {}
337 | for i, s in ipairs(segments) do
338 | local node, colon_node, is_same = self:find_matched_child(parent, s)
339 | if self.ignore_case and node == nil then
340 | node, colon_node, is_same = self:find_matched_child(parent, string_lower(s))
341 | end
342 |
343 | if colon_node and not is_same then
344 | table_insert(fallback_stack, {
345 | segment_index = i,
346 | colon_node = colon_node
347 | })
348 | end
349 |
350 | if node == nil then -- both exact child and colon child is nil
351 | flag = false -- should not set parent value
352 | break
353 | end
354 |
355 | parent = node
356 | if parent.name ~= "" then
357 | matched.params[parent.name] = s
358 | end
359 | end
360 |
361 | if flag and parent.endpoint then
362 | matched.node = parent
363 | end
364 |
365 | local params = matched.params or {}
366 | if not matched.node then
367 | local depth = 0
368 | local exit = false
369 |
370 | while not exit do
371 | depth = depth + 1
372 | if depth > self.max_fallback_depth then
373 | error("fallback lookup reaches the limit: " .. self.max_fallback_depth)
374 | end
375 |
376 | exit = self:fallback_lookup(fallback_stack, segments, params)
377 | if exit then
378 | matched = exit
379 | break
380 | end
381 |
382 | if #fallback_stack == 0 then
383 | break
384 | end
385 | end
386 | end
387 |
388 | matched.params = params
389 | if matched.node then
390 | matched.pipeline = get_pipeline(matched.node)
391 | end
392 |
393 | return matched
394 | end
395 |
396 | --- only for dev purpose: pretty json preview
397 | -- must not be invoked in runtime
398 | function Trie:remove_nested_property(node)
399 | if not node then return end
400 | if node.parent then
401 | node.parent = nil
402 | end
403 | if node.handlers then
404 | for _, h in pairs(node.handlers) do
405 | if h then
406 | for _, action in ipairs(h) do
407 | action.func = nil
408 | action.node = nil
409 | end
410 | end
411 | end
412 | end
413 | if node.middlewares then
414 | for _, m in pairs(node.middlewares) do
415 | if m then
416 | m.func = nil
417 | m.node = nil
418 | end
419 | end
420 | end
421 | if node.error_middlewares then
422 | for _, m in pairs(node.error_middlewares) do
423 | if m then
424 | m.func = nil
425 | m.node = nil
426 | end
427 | end
428 | end
429 |
430 | if node.colon_child then
431 | if node.colon_child.handlers then
432 | for _, h in pairs(node.colon_child.handlers) do
433 | if h then
434 | for _, action in ipairs(h) do
435 | action.func = nil
436 | action.node = nil
437 | end
438 | end
439 | end
440 | end
441 | if node.colon_child.middlewares then
442 | for _, m in pairs(node.colon_child.middlewares) do
443 | if m then
444 | m.func = nil
445 | m.node = nil
446 | end
447 | end
448 | end
449 | if node.colon_child.error_middlewares then
450 | for _, m in pairs(node.colon_child.error_middlewares) do
451 | if m then
452 | m.func = nil
453 | m.node = nil
454 | end
455 | end
456 | end
457 | self:remove_nested_property(node.colon_child)
458 | end
459 |
460 | local children = node.children
461 | if children and #children > 0 then
462 | for _, v in ipairs(children) do
463 | local c = v.val
464 | if c.handlers then -- remove action func
465 | for _, h in pairs(c.handlers) do
466 | if h then
467 | for _, action in ipairs(h) do
468 | action.func = nil
469 | action.node = nil
470 | end
471 | end
472 | end
473 | end
474 | if c.middlewares then
475 | for _, m in pairs(c.middlewares) do
476 | if m then
477 | m.func = nil
478 | m.node = nil
479 | end
480 | end
481 | end
482 | if c.error_middlewares then
483 | for _, m in pairs(c.error_middlewares) do
484 | if m then
485 | m.func = nil
486 | m.node = nil
487 | end
488 | end
489 | end
490 |
491 | self:remove_nested_property(v.val)
492 | end
493 | end
494 | end
495 |
496 | --- only for dev purpose: graph preview
497 | -- must not be invoked in runtime
498 | function Trie:gen_graph()
499 | local cloned_trie = utils.clone(self)
500 | cloned_trie:remove_nested_property(cloned_trie.root)
501 | local result = {"graph TD", cloned_trie.root.id .. "((root))"}
502 |
503 | local function recursive_draw(node, res)
504 | if node.is_root then node.key = "root" end
505 |
506 | local colon_child = node.colon_child
507 | if colon_child then
508 | table_insert(res, node.id .. "-->" .. colon_child.id .. "(:" .. colon_child.name .. "
" .. colon_child.id .. ")")
509 | recursive_draw(colon_child, res)
510 | end
511 |
512 | local children = node.children
513 | if children and #children > 0 then
514 | for _, v in ipairs(children) do
515 | if v.key == "" then
516 | --table_insert(res, node.id .. "-->" .. v.val.id .. "[*EMPTY*]")
517 | local text = {node.id, "-->", v.val.id, "(