├── .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.svg?branch=master](https://travis-ci.org/sumory/lor.svg?branch=master)](https://travis-ci.org/sumory/lor) [![GitHub release](https://img.shields.io/github/release/sumory/lor.svg)](https://github.com/sumory/lor/releases/latest) [![license](https://img.shields.io/github/license/sumory/lor.svg)](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.svg?branch=master](https://travis-ci.org/sumory/lor.svg?branch=master)](https://travis-ci.org/sumory/lor) [![GitHub release](https://img.shields.io/github/release/sumory/lor.svg)](https://github.com/sumory/lor/releases/latest) [![license](https://img.shields.io/github/license/sumory/lor.svg)](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, "(
", "*EMPTY*", "
", v.val.id, "
)"} 518 | table_insert(res, table_concat(text, "")) 519 | else 520 | local text = {node.id, "-->", v.val.id, "(
", v.key, "
", v.val.id, "
)"} 521 | table_insert(res, table_concat(text, "")) 522 | end 523 | recursive_draw(v.val, res) 524 | end 525 | end 526 | end 527 | 528 | recursive_draw(cloned_trie.root, result) 529 | return table.concat(result, "\n") 530 | end 531 | 532 | return Trie 533 | -------------------------------------------------------------------------------- /lib/lor/lib/utils/aes.lua: -------------------------------------------------------------------------------- 1 | -- from lua-resty-session 2 | local setmetatable = setmetatable 3 | local tonumber = tonumber 4 | local aes = require "resty.aes" 5 | local cip = aes.cipher 6 | local hashes = aes.hash 7 | local var = ngx.var 8 | 9 | local CIPHER_MODES = { 10 | ecb = "ecb", 11 | cbc = "cbc", 12 | cfb1 = "cfb1", 13 | cfb8 = "cfb8", 14 | cfb128 = "cfb128", 15 | ofb = "ofb", 16 | ctr = "ctr" 17 | } 18 | 19 | local CIPHER_SIZES = { 20 | ["128"] = 128, 21 | ["192"] = 192, 22 | ["256"] = 256 23 | } 24 | 25 | local defaults = { 26 | size = CIPHER_SIZES[var.session_aes_size] or 256, 27 | mode = CIPHER_MODES[var.session_aes_mode] or "cbc", 28 | hash = hashes[var.session_aes_hash] or "sha512", 29 | rounds = tonumber(var.session_aes_rounds) or 1 30 | } 31 | 32 | local cipher = {} 33 | 34 | cipher.__index = cipher 35 | 36 | function cipher.new(config) 37 | local a = config and config.aes or defaults 38 | return setmetatable({ 39 | size = CIPHER_SIZES[a.size or defaults.size] or 256, 40 | mode = CIPHER_MODES[a.mode or defaults.mode] or "cbc", 41 | hash = hashes[a.hash or defaults.hash] or hashes.sha512, 42 | rounds = tonumber(a.rounds or defaults.rounds) or 1 43 | }, cipher) 44 | end 45 | 46 | function cipher:encrypt(d, k, s) 47 | return aes:new(k, s, cip(self.size, self.mode), self.hash, self.rounds):encrypt(d) 48 | end 49 | 50 | function cipher:decrypt(d, k, s) 51 | return aes:new(k, s, cip(self.size, self.mode), self.hash, self.rounds):decrypt(d) 52 | end 53 | 54 | return cipher 55 | -------------------------------------------------------------------------------- /lib/lor/lib/utils/base64.lua: -------------------------------------------------------------------------------- 1 | local ngx = ngx 2 | local base64enc = ngx.encode_base64 3 | local base64dec = ngx.decode_base64 4 | 5 | local ENCODE_CHARS = { 6 | ["+"] = "-", 7 | ["/"] = "_", 8 | ["="] = "." 9 | } 10 | 11 | local DECODE_CHARS = { 12 | ["-"] = "+", 13 | ["_"] = "/", 14 | ["."] = "=" 15 | } 16 | 17 | local base64 = {} 18 | 19 | function base64.encode(value) 20 | return (base64enc(value):gsub("[+/=]", ENCODE_CHARS)) 21 | end 22 | 23 | function base64.decode(value) 24 | return base64dec((value:gsub("[-_.]", DECODE_CHARS))) 25 | end 26 | 27 | return base64 28 | -------------------------------------------------------------------------------- /lib/lor/lib/utils/utils.lua: -------------------------------------------------------------------------------- 1 | local type = type 2 | local pairs = pairs 3 | local setmetatable = setmetatable 4 | local mrandom = math.random 5 | local sreverse = string.reverse 6 | local sfind = string.find 7 | local sgsub = string.gsub 8 | local smatch = string.match 9 | local table_insert = table.insert 10 | local json = require("cjson") 11 | 12 | local _M = {} 13 | 14 | function _M.clone(o) 15 | local lookup_table = {} 16 | local function _copy(object) 17 | if type(object) ~= "table" then 18 | return object 19 | elseif lookup_table[object] then 20 | return lookup_table[object] 21 | end 22 | local new_object = {} 23 | lookup_table[object] = new_object 24 | for key, value in pairs(object) do 25 | new_object[_copy(key)] = _copy(value) 26 | end 27 | return setmetatable(new_object, getmetatable(object)) 28 | end 29 | return _copy(o) 30 | end 31 | 32 | function _M.clear_slash(s) 33 | local r, _ = sgsub(s, "(/+)", "/") 34 | return r 35 | end 36 | 37 | function _M.is_table_empty(t) 38 | if t == nil or _G.next(t) == nil then 39 | return true 40 | else 41 | return false 42 | end 43 | end 44 | 45 | function _M.table_is_array(t) 46 | if type(t) ~= "table" then return false end 47 | local i = 0 48 | for _ in pairs(t) do 49 | i = i + 1 50 | if t[i] == nil then return false end 51 | end 52 | return true 53 | end 54 | 55 | function _M.mixin(a, b) 56 | if a and b then 57 | for k, _ in pairs(b) do 58 | a[k] = b[k] 59 | end 60 | end 61 | return a 62 | end 63 | 64 | function _M.random() 65 | return mrandom(0, 10000) 66 | end 67 | 68 | function _M.json_encode(data, empty_table_as_object) 69 | local json_value 70 | if json.encode_empty_table_as_object then 71 | -- empty table encoded as array default 72 | json.encode_empty_table_as_object(empty_table_as_object or false) 73 | end 74 | if require("ffi").os ~= "Windows" then 75 | json.encode_sparse_array(true) 76 | end 77 | pcall(function(d) json_value = json.encode(d) end, data) 78 | return json_value 79 | end 80 | 81 | function _M.json_decode(str) 82 | local ok, data = pcall(json.decode, str) 83 | if ok then 84 | return data 85 | end 86 | end 87 | 88 | function _M.start_with(str, substr) 89 | if str == nil or substr == nil then 90 | return false 91 | end 92 | if sfind(str, substr) ~= 1 then 93 | return false 94 | else 95 | return true 96 | end 97 | end 98 | 99 | function _M.end_with(str, substr) 100 | if str == nil or substr == nil then 101 | return false 102 | end 103 | local str_reverse = sreverse(str) 104 | local substr_reverse = sreverse(substr) 105 | if sfind(str_reverse, substr_reverse) ~= 1 then 106 | return false 107 | else 108 | return true 109 | end 110 | end 111 | 112 | function _M.is_match(uri, pattern) 113 | if not pattern then 114 | return false 115 | end 116 | 117 | local ok = smatch(uri, pattern) 118 | if ok then return true else return false end 119 | end 120 | 121 | function _M.trim_prefix_slash(s) 122 | local str, _ = sgsub(s, "^(//*)", "") 123 | return str 124 | end 125 | 126 | function _M.trim_suffix_slash(s) 127 | local str, _ = sgsub(s, "(//*)$", "") 128 | return str 129 | end 130 | 131 | function _M.trim_path_spaces(path) 132 | if not path or path == "" then return path end 133 | return sgsub(path, "( *)", "") 134 | end 135 | 136 | function _M.slim_path(path) 137 | if not path or path == "" then return path end 138 | return sgsub(path, "(//*)", "/") 139 | end 140 | 141 | function _M.split(str, delimiter) 142 | if not str or str == "" then return {} end 143 | if not delimiter or delimiter == "" then return { str } end 144 | 145 | local result = {} 146 | for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do 147 | table_insert(result, match) 148 | end 149 | return result 150 | end 151 | 152 | return _M 153 | -------------------------------------------------------------------------------- /lib/lor/lib/view.lua: -------------------------------------------------------------------------------- 1 | local pairs = pairs 2 | local type = type 3 | local setmetatable = setmetatable 4 | local tostring = tostring 5 | local template = require "resty.template" 6 | local template_new = template.new 7 | 8 | local View = {} 9 | 10 | function View:new(view_config) 11 | local instance = {} 12 | instance.view_enable = view_config.view_enable 13 | if instance.view_enable then 14 | if ngx.var.template_root then 15 | ngx.var.template_root = view_config.views 16 | else 17 | ngx.log(ngx.ERR, "$template_root is not set in nginx.conf") 18 | end 19 | end 20 | instance.view_engine = view_config.view_engine 21 | instance.view_ext = view_config.view_ext 22 | instance.view_layout = view_config.view_layout 23 | instance.views = view_config.views 24 | 25 | setmetatable(instance, {__index = self}) 26 | return instance 27 | end 28 | 29 | function View:caching() 30 | end 31 | 32 | -- to optimize 33 | function View:render(view_file, data) 34 | if not self.view_enable then 35 | ngx.log(ngx.ERR, "view is not enabled. you may need `app:conf('view enable', true)`") 36 | else 37 | local view_file_name = view_file .. "." .. self.view_ext 38 | local layout_file_name = self.view_layout .. "." .. self.view_ext 39 | 40 | local t = template_new(view_file_name) 41 | if self.view_layout ~= "" then 42 | t = template_new(view_file_name,layout_file_name) 43 | end 44 | if data and type(data) == 'table' then 45 | for k,v in pairs(data) do 46 | t[k] = v 47 | end 48 | end 49 | 50 | return tostring(t) 51 | end 52 | end 53 | 54 | return View -------------------------------------------------------------------------------- /lib/lor/lib/wrap.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | 3 | local _M = {} 4 | 5 | function _M:new(create_app, Router, Group, Request, Response) 6 | local instance = {} 7 | instance.router = Router 8 | instance.group = Group 9 | instance.request = Request 10 | instance.response = Response 11 | instance.fn = create_app 12 | instance.app = nil 13 | 14 | setmetatable(instance, { 15 | __index = self, 16 | __call = self.create_app 17 | }) 18 | 19 | return instance 20 | end 21 | 22 | -- Generally, this should only be used by `lor` framework itself. 23 | function _M:create_app(options) 24 | self.app = self.fn(options) 25 | return self.app 26 | end 27 | 28 | function _M:Router(options) 29 | return self.group:new(options) 30 | end 31 | 32 | function _M:Request() 33 | return self.request:new() 34 | end 35 | 36 | function _M:Response() 37 | return self.response:new() 38 | end 39 | 40 | return _M 41 | -------------------------------------------------------------------------------- /lib/lor/version.lua: -------------------------------------------------------------------------------- 1 | return "0.3.4" 2 | -------------------------------------------------------------------------------- /resty/cookie.lua: -------------------------------------------------------------------------------- 1 | -- https://github.com/cloudflare/lua-resty-cookie/blob/master/lib/resty/cookie.lua 2 | 3 | -- Copyright (C) 2013 Jiale Zhi (calio), Cloudflare Inc. 4 | -- See RFC6265 http://tools.ietf.org/search/rfc6265 5 | -- require "luacov" 6 | 7 | local type = type 8 | local byte = string.byte 9 | local sub = string.sub 10 | local format = string.format 11 | local log = ngx.log 12 | local ERR = ngx.ERR 13 | local ngx_header = ngx.header 14 | 15 | local EQUAL = byte("=") 16 | local SEMICOLON = byte(";") 17 | local SPACE = byte(" ") 18 | local HTAB = byte("\t") 19 | 20 | -- table.new(narr, nrec) 21 | local ok, new_tab = pcall(require, "table.new") 22 | if not ok then 23 | new_tab = function () return {} end 24 | end 25 | 26 | local ok, clear_tab = pcall(require, "table.clear") 27 | if not ok then 28 | clear_tab = function(tab) for k, _ in pairs(tab) do tab[k] = nil end end 29 | end 30 | 31 | local _M = new_tab(0, 2) 32 | 33 | _M._VERSION = '0.01' 34 | 35 | 36 | local function get_cookie_table(text_cookie) 37 | if type(text_cookie) ~= "string" then 38 | log(ERR, format("expect text_cookie to be \"string\" but found %s", 39 | type(text_cookie))) 40 | return {} 41 | end 42 | 43 | local EXPECT_KEY = 1 44 | local EXPECT_VALUE = 2 45 | local EXPECT_SP = 3 46 | 47 | local n = 0 48 | local len = #text_cookie 49 | 50 | for i=1, len do 51 | if byte(text_cookie, i) == SEMICOLON then 52 | n = n + 1 53 | end 54 | end 55 | 56 | local cookie_table = new_tab(0, n + 1) 57 | 58 | local state = EXPECT_SP 59 | local i = 1 60 | local j = 1 61 | local key, value 62 | 63 | while j <= len do 64 | if state == EXPECT_KEY then 65 | if byte(text_cookie, j) == EQUAL then 66 | key = sub(text_cookie, i, j - 1) 67 | state = EXPECT_VALUE 68 | i = j + 1 69 | end 70 | elseif state == EXPECT_VALUE then 71 | if byte(text_cookie, j) == SEMICOLON 72 | or byte(text_cookie, j) == SPACE 73 | or byte(text_cookie, j) == HTAB 74 | then 75 | value = sub(text_cookie, i, j - 1) 76 | cookie_table[key] = value 77 | 78 | key, value = nil, nil 79 | state = EXPECT_SP 80 | i = j + 1 81 | end 82 | elseif state == EXPECT_SP then 83 | if byte(text_cookie, j) ~= SPACE 84 | and byte(text_cookie, j) ~= HTAB 85 | then 86 | state = EXPECT_KEY 87 | i = j 88 | j = j - 1 89 | end 90 | end 91 | j = j + 1 92 | end 93 | 94 | if key ~= nil and value == nil then 95 | cookie_table[key] = sub(text_cookie, i) 96 | end 97 | 98 | return cookie_table 99 | end 100 | 101 | function _M.new(self) 102 | local _cookie = ngx.var.http_cookie 103 | --if not _cookie then 104 | --return nil, "no cookie found in current request" 105 | --end 106 | return setmetatable({ _cookie = _cookie, set_cookie_table = new_tab(4, 0) }, 107 | { __index = self }) 108 | end 109 | 110 | function _M.get(self, key) 111 | if not self._cookie then 112 | return nil, "no cookie found in the current request" 113 | end 114 | if self.cookie_table == nil then 115 | self.cookie_table = get_cookie_table(self._cookie) 116 | end 117 | 118 | return self.cookie_table[key] 119 | end 120 | 121 | function _M.get_all(self) 122 | if not self._cookie then 123 | return nil, "no cookie found in the current request" 124 | end 125 | 126 | if self.cookie_table == nil then 127 | self.cookie_table = get_cookie_table(self._cookie) 128 | end 129 | 130 | return self.cookie_table 131 | end 132 | 133 | local function bake(cookie) 134 | if not cookie.key or not cookie.value then 135 | return nil, 'missing cookie field "key" or "value"' 136 | end 137 | 138 | if cookie["max-age"] then 139 | cookie.max_age = cookie["max-age"] 140 | end 141 | local str = cookie.key .. "=" .. cookie.value 142 | .. (cookie.expires and "; Expires=" .. cookie.expires or "") 143 | .. (cookie.max_age and "; Max-Age=" .. cookie.max_age or "") 144 | .. (cookie.domain and "; Domain=" .. cookie.domain or "") 145 | .. (cookie.path and "; Path=" .. cookie.path or "") 146 | .. (cookie.secure and "; Secure" or "") 147 | .. (cookie.httponly and "; HttpOnly" or "") 148 | .. (cookie.extension and "; " .. cookie.extension or "") 149 | return str 150 | end 151 | 152 | function _M.set(self, cookie) 153 | local cookie_str, err = bake(cookie) 154 | if not cookie_str then 155 | return nil, err 156 | end 157 | 158 | local set_cookie = ngx_header['Set-Cookie'] 159 | local set_cookie_type = type(set_cookie) 160 | local t = self.set_cookie_table 161 | clear_tab(t) 162 | 163 | if set_cookie_type == "string" then 164 | -- only one cookie has been setted 165 | if set_cookie ~= cookie_str then 166 | t[1] = set_cookie 167 | t[2] = cookie_str 168 | ngx_header['Set-Cookie'] = t 169 | end 170 | elseif set_cookie_type == "table" then 171 | -- more than one cookies has been setted 172 | local size = #set_cookie 173 | 174 | -- we can not set cookie like ngx.header['Set-Cookie'][3] = val 175 | -- so create a new table, copy all the values, and then set it back 176 | for i=1, size do 177 | t[i] = ngx_header['Set-Cookie'][i] 178 | if t[i] == cookie_str then 179 | -- new cookie is duplicated 180 | return true 181 | end 182 | end 183 | t[size + 1] = cookie_str 184 | ngx_header['Set-Cookie'] = t 185 | else 186 | -- no cookie has been setted 187 | ngx_header['Set-Cookie'] = cookie_str 188 | end 189 | return true 190 | end 191 | 192 | return _M -------------------------------------------------------------------------------- /resty/template.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local loadstring = loadstring 3 | local loadchunk 4 | local tostring = tostring 5 | local setfenv = setfenv 6 | local require = require 7 | local capture 8 | local concat = table.concat 9 | local assert = assert 10 | local prefix 11 | local write = io.write 12 | local pcall = pcall 13 | local phase 14 | local open = io.open 15 | local load = load 16 | local type = type 17 | local dump = string.dump 18 | local find = string.find 19 | local gsub = string.gsub 20 | local byte = string.byte 21 | local null 22 | local sub = string.sub 23 | local ngx = ngx 24 | local jit = jit 25 | local var 26 | 27 | local _VERSION = _VERSION 28 | local _ENV = _ENV 29 | local _G = _G 30 | 31 | local HTML_ENTITIES = { 32 | ["&"] = "&", 33 | ["<"] = "<", 34 | [">"] = ">", 35 | ['"'] = """, 36 | ["'"] = "'", 37 | ["/"] = "/" 38 | } 39 | 40 | local CODE_ENTITIES = { 41 | ["{"] = "{", 42 | ["}"] = "}", 43 | ["&"] = "&", 44 | ["<"] = "<", 45 | [">"] = ">", 46 | ['"'] = """, 47 | ["'"] = "'", 48 | ["/"] = "/" 49 | } 50 | 51 | local VAR_PHASES 52 | 53 | local ok, newtab = pcall(require, "table.new") 54 | if not ok then newtab = function() return {} end end 55 | 56 | local caching = true 57 | local template = newtab(0, 12) 58 | 59 | template._VERSION = "1.9" 60 | template.cache = {} 61 | 62 | local function enabled(val) 63 | if val == nil then return true end 64 | return val == true or (val == "1" or val == "true" or val == "on") 65 | end 66 | 67 | local function trim(s) 68 | return gsub(gsub(s, "^%s+", ""), "%s+$", "") 69 | end 70 | 71 | local function rpos(view, s) 72 | while s > 0 do 73 | local c = sub(view, s, s) 74 | if c == " " or c == "\t" or c == "\0" or c == "\x0B" then 75 | s = s - 1 76 | else 77 | break 78 | end 79 | end 80 | return s 81 | end 82 | 83 | local function escaped(view, s) 84 | if s > 1 and sub(view, s - 1, s - 1) == "\\" then 85 | if s > 2 and sub(view, s - 2, s - 2) == "\\" then 86 | return false, 1 87 | else 88 | return true, 1 89 | end 90 | end 91 | return false, 0 92 | end 93 | 94 | local function readfile(path) 95 | local file = open(path, "rb") 96 | if not file then return nil end 97 | local content = file:read "*a" 98 | file:close() 99 | return content 100 | end 101 | 102 | local function loadlua(path) 103 | return readfile(path) or path 104 | end 105 | 106 | local function loadngx(path) 107 | local vars = VAR_PHASES[phase()] 108 | local file, location = path, vars and var.template_location 109 | if sub(file, 1) == "/" then file = sub(file, 2) end 110 | if location and location ~= "" then 111 | if sub(location, -1) == "/" then location = sub(location, 1, -2) end 112 | local res = capture(concat{ location, '/', file}) 113 | if res.status == 200 then return res.body end 114 | end 115 | local root = vars and (var.template_root or var.document_root) or prefix 116 | if sub(root, -1) == "/" then root = sub(root, 1, -2) end 117 | return readfile(concat{ root, "/", file }) or path 118 | end 119 | 120 | do 121 | if ngx then 122 | VAR_PHASES = { 123 | set = true, 124 | rewrite = true, 125 | access = true, 126 | content = true, 127 | header_filter = true, 128 | body_filter = true, 129 | log = true 130 | } 131 | template.print = ngx.print or write 132 | template.load = loadngx 133 | prefix, var, capture, null, phase = ngx.config.prefix(), ngx.var, ngx.location.capture, ngx.null, ngx.get_phase 134 | if VAR_PHASES[phase()] then 135 | caching = enabled(var.template_cache) 136 | end 137 | else 138 | template.print = write 139 | template.load = loadlua 140 | end 141 | if _VERSION == "Lua 5.1" then 142 | local context = { __index = function(t, k) 143 | return t.context[k] or t.template[k] or _G[k] 144 | end } 145 | if jit then 146 | loadchunk = function(view) 147 | return assert(load(view, nil, nil, setmetatable({ template = template }, context))) 148 | end 149 | else 150 | loadchunk = function(view) 151 | local func = assert(loadstring(view)) 152 | setfenv(func, setmetatable({ template = template }, context)) 153 | return func 154 | end 155 | end 156 | else 157 | local context = { __index = function(t, k) 158 | return t.context[k] or t.template[k] or _ENV[k] 159 | end } 160 | loadchunk = function(view) 161 | return assert(load(view, nil, nil, setmetatable({ template = template }, context))) 162 | end 163 | end 164 | end 165 | 166 | function template.caching(enable) 167 | if enable ~= nil then caching = enable == true end 168 | return caching 169 | end 170 | 171 | function template.output(s) 172 | if s == nil or s == null then return "" end 173 | if type(s) == "function" then return template.output(s()) end 174 | return tostring(s) 175 | end 176 | 177 | function template.escape(s, c) 178 | if type(s) == "string" then 179 | if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end 180 | return gsub(s, "[\">/<'&]", HTML_ENTITIES) 181 | end 182 | return template.output(s) 183 | end 184 | 185 | function template.new(view, layout) 186 | assert(view, "view was not provided for template.new(view, layout).") 187 | local render, compile = template.render, template.compile 188 | if layout then 189 | if type(layout) == "table" then 190 | return setmetatable({ render = function(self, context) 191 | local context = context or self 192 | context.blocks = context.blocks or {} 193 | context.view = compile(view)(context) 194 | layout.blocks = context.blocks or {} 195 | layout.view = context.view or "" 196 | return layout:render() 197 | end }, { __tostring = function(self) 198 | local context = self 199 | context.blocks = context.blocks or {} 200 | context.view = compile(view)(context) 201 | layout.blocks = context.blocks or {} 202 | layout.view = context.view 203 | return tostring(layout) 204 | end }) 205 | else 206 | return setmetatable({ render = function(self, context) 207 | local context = context or self 208 | context.blocks = context.blocks or {} 209 | context.view = compile(view)(context) 210 | return render(layout, context) 211 | end }, { __tostring = function(self) 212 | local context = self 213 | context.blocks = context.blocks or {} 214 | context.view = compile(view)(context) 215 | return compile(layout)(context) 216 | end }) 217 | end 218 | end 219 | return setmetatable({ render = function(self, context) 220 | return render(view, context or self) 221 | end }, { __tostring = function(self) 222 | return compile(view)(self) 223 | end }) 224 | end 225 | 226 | function template.precompile(view, path, strip) 227 | local chunk = dump(template.compile(view), strip ~= false) 228 | if path then 229 | local file = open(path, "wb") 230 | file:write(chunk) 231 | file:close() 232 | end 233 | return chunk 234 | end 235 | 236 | function template.compile(view, key, plain) 237 | assert(view, "view was not provided for template.compile(view, key, plain).") 238 | if key == "no-cache" then 239 | return loadchunk(template.parse(view, plain)), false 240 | end 241 | key = key or view 242 | local cache = template.cache 243 | if cache[key] then return cache[key], true end 244 | local func = loadchunk(template.parse(view, plain)) 245 | if caching then cache[key] = func end 246 | return func, false 247 | end 248 | 249 | function template.parse(view, plain) 250 | assert(view, "view was not provided for template.parse(view, plain).") 251 | if not plain then 252 | view = template.load(view) 253 | if byte(view, 1, 1) == 27 then return view end 254 | end 255 | local j = 2 256 | local c = {[[ 257 | context=... or {} 258 | local function include(v, c) return template.compile(v)(c or context) end 259 | local ___,blocks,layout={},blocks or {} 260 | ]] } 261 | local i, s = 1, find(view, "{", 1, true) 262 | while s do 263 | local t, p = sub(view, s + 1, s + 1), s + 2 264 | if t == "{" then 265 | local e = find(view, "}}", p, true) 266 | if e then 267 | local z, w = escaped(view, s) 268 | if i < s - w then 269 | c[j] = "___[#___+1]=[=[\n" 270 | c[j+1] = sub(view, i, s - 1 - w) 271 | c[j+2] = "]=]\n" 272 | j=j+3 273 | end 274 | if z then 275 | i = s 276 | else 277 | c[j] = "___[#___+1]=template.escape(" 278 | c[j+1] = trim(sub(view, p, e - 1)) 279 | c[j+2] = ")\n" 280 | j=j+3 281 | s, i = e + 1, e + 2 282 | end 283 | end 284 | elseif t == "*" then 285 | local e = find(view, "*}", p, true) 286 | if e then 287 | local z, w = escaped(view, s) 288 | if i < s - w then 289 | c[j] = "___[#___+1]=[=[\n" 290 | c[j+1] = sub(view, i, s - 1 - w) 291 | c[j+2] = "]=]\n" 292 | j=j+3 293 | end 294 | if z then 295 | i = s 296 | else 297 | c[j] = "___[#___+1]=template.output(" 298 | c[j+1] = trim(sub(view, p, e - 1)) 299 | c[j+2] = ")\n" 300 | j=j+3 301 | s, i = e + 1, e + 2 302 | end 303 | end 304 | elseif t == "%" then 305 | local e = find(view, "%}", p, true) 306 | if e then 307 | local z, w = escaped(view, s) 308 | if z then 309 | if i < s - w then 310 | c[j] = "___[#___+1]=[=[\n" 311 | c[j+1] = sub(view, i, s - 1 - w) 312 | c[j+2] = "]=]\n" 313 | j=j+3 314 | end 315 | i = s 316 | else 317 | local n = e + 2 318 | if sub(view, n, n) == "\n" then 319 | n = n + 1 320 | end 321 | local r = rpos(view, s - 1) 322 | if i <= r then 323 | c[j] = "___[#___+1]=[=[\n" 324 | c[j+1] = sub(view, i, r) 325 | c[j+2] = "]=]\n" 326 | j=j+3 327 | end 328 | c[j] = trim(sub(view, p, e - 1)) 329 | c[j+1] = "\n" 330 | j=j+2 331 | s, i = n - 1, n 332 | end 333 | end 334 | elseif t == "(" then 335 | local e = find(view, ")}", p, true) 336 | if e then 337 | local z, w = escaped(view, s) 338 | if i < s - w then 339 | c[j] = "___[#___+1]=[=[\n" 340 | c[j+1] = sub(view, i, s - 1 - w) 341 | c[j+2] = "]=]\n" 342 | j=j+3 343 | end 344 | if z then 345 | i = s 346 | else 347 | local f = sub(view, p, e - 1) 348 | local x = find(f, ",", 2, true) 349 | if x then 350 | c[j] = "___[#___+1]=include([=[" 351 | c[j+1] = trim(sub(f, 1, x - 1)) 352 | c[j+2] = "]=]," 353 | c[j+3] = trim(sub(f, x + 1)) 354 | c[j+4] = ")\n" 355 | j=j+5 356 | else 357 | c[j] = "___[#___+1]=include([=[" 358 | c[j+1] = trim(f) 359 | c[j+2] = "]=])\n" 360 | j=j+3 361 | end 362 | s, i = e + 1, e + 2 363 | end 364 | end 365 | elseif t == "[" then 366 | local e = find(view, "]}", p, true) 367 | if e then 368 | local z, w = escaped(view, s) 369 | if i < s - w then 370 | c[j] = "___[#___+1]=[=[\n" 371 | c[j+1] = sub(view, i, s - 1 - w) 372 | c[j+2] = "]=]\n" 373 | j=j+3 374 | end 375 | if z then 376 | i = s 377 | else 378 | c[j] = "___[#___+1]=include(" 379 | c[j+1] = trim(sub(view, p, e - 1)) 380 | c[j+2] = ")\n" 381 | j=j+3 382 | s, i = e + 1, e + 2 383 | end 384 | end 385 | elseif t == "-" then 386 | local e = find(view, "-}", p, true) 387 | if e then 388 | local x, y = find(view, sub(view, s, e + 1), e + 2, true) 389 | if x then 390 | local z, w = escaped(view, s) 391 | if z then 392 | if i < s - w then 393 | c[j] = "___[#___+1]=[=[\n" 394 | c[j+1] = sub(view, i, s - 1 - w) 395 | c[j+2] = "]=]\n" 396 | j=j+3 397 | end 398 | i = s 399 | else 400 | y = y + 1 401 | x = x - 1 402 | if sub(view, y, y) == "\n" then 403 | y = y + 1 404 | end 405 | local b = trim(sub(view, p, e - 1)) 406 | if b == "verbatim" or b == "raw" then 407 | if i < s - w then 408 | c[j] = "___[#___+1]=[=[\n" 409 | c[j+1] = sub(view, i, s - 1 - w) 410 | c[j+2] = "]=]\n" 411 | j=j+3 412 | end 413 | c[j] = "___[#___+1]=[=[" 414 | c[j+1] = sub(view, e + 2, x) 415 | c[j+2] = "]=]\n" 416 | j=j+3 417 | else 418 | if sub(view, x, x) == "\n" then 419 | x = x - 1 420 | end 421 | local r = rpos(view, s - 1) 422 | if i <= r then 423 | c[j] = "___[#___+1]=[=[\n" 424 | c[j+1] = sub(view, i, r) 425 | c[j+2] = "]=]\n" 426 | j=j+3 427 | end 428 | c[j] = 'blocks["' 429 | c[j+1] = b 430 | c[j+2] = '"]=include[=[' 431 | c[j+3] = sub(view, e + 2, x) 432 | c[j+4] = "]=]\n" 433 | j=j+5 434 | end 435 | s, i = y - 1, y 436 | end 437 | end 438 | end 439 | elseif t == "#" then 440 | local e = find(view, "#}", p, true) 441 | if e then 442 | local z, w = escaped(view, s) 443 | if i < s - w then 444 | c[j] = "___[#___+1]=[=[\n" 445 | c[j+1] = sub(view, i, s - 1 - w) 446 | c[j+2] = "]=]\n" 447 | j=j+3 448 | end 449 | if z then 450 | i = s 451 | else 452 | e = e + 2 453 | if sub(view, e, e) == "\n" then 454 | e = e + 1 455 | end 456 | s, i = e - 1, e 457 | end 458 | end 459 | end 460 | s = find(view, "{", s + 1, true) 461 | end 462 | s = sub(view, i) 463 | if s and s ~= "" then 464 | c[j] = "___[#___+1]=[=[\n" 465 | c[j+1] = s 466 | c[j+2] = "]=]\n" 467 | j=j+3 468 | end 469 | c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)" 470 | return concat(c) 471 | end 472 | 473 | function template.render(view, context, key, plain) 474 | assert(view, "view was not provided for template.render(view, context, key, plain).") 475 | return template.print(template.compile(view, key, plain)(context)) 476 | end 477 | 478 | return template 479 | -------------------------------------------------------------------------------- /resty/template/html.lua: -------------------------------------------------------------------------------- 1 | local template = require "resty.template" 2 | local setmetatable = setmetatable 3 | local escape = template.escape 4 | local concat = table.concat 5 | local pairs = pairs 6 | local type = type 7 | 8 | local function tag(name, content, attr) 9 | local r, a, content = {}, {}, content or attr 10 | r[#r + 1] = "<" 11 | r[#r + 1] = name 12 | if attr then 13 | for k, v in pairs(attr) do 14 | if type(k) == "number" then 15 | a[#a + 1] = escape(v) 16 | else 17 | a[#a + 1] = k .. '="' .. escape(v) .. '"' 18 | end 19 | end 20 | if #a > 0 then 21 | r[#r + 1] = " " 22 | r[#r + 1] = concat(a, " ") 23 | end 24 | end 25 | if type(content) == "string" then 26 | r[#r + 1] = ">" 27 | r[#r + 1] = escape(content) 28 | r[#r + 1] = "" 31 | else 32 | r[#r + 1] = " />" 33 | end 34 | return concat(r) 35 | end 36 | 37 | local html = { __index = function(_, name) 38 | return function(attr) 39 | if type(attr) == "table" then 40 | return function(content) 41 | return tag(name, content, attr) 42 | end 43 | else 44 | return tag(name, attr) 45 | end 46 | end 47 | end } 48 | 49 | template.html = setmetatable(html, html) 50 | 51 | return template.html 52 | -------------------------------------------------------------------------------- /resty/template/microbenchmark.lua: -------------------------------------------------------------------------------- 1 | local template = require "resty.template" 2 | 3 | local ok, new_tab = pcall(require, "table.new") 4 | if not ok then 5 | new_tab = function() return {} end 6 | end 7 | 8 | local function run(iterations) 9 | local gc, total, print, parse, compile, iterations, clock, format = collectgarbage, 0, ngx and ngx.say or print, template.parse, template.compile, iterations or 1000, os.clock, string.format 10 | local view = [[ 11 | ]] 16 | 17 | print(format("Running %d iterations in each test", iterations)) 18 | 19 | gc() 20 | gc() 21 | 22 | local x = clock() 23 | for _ = 1, iterations do 24 | parse(view, true) 25 | end 26 | local z = clock() - x 27 | print(format(" Parsing Time: %.6f", z)) 28 | total = total + z 29 | 30 | gc() 31 | gc() 32 | 33 | x = clock() 34 | for _ = 1, iterations do 35 | compile(view, nil, true) 36 | template.cache = {} 37 | end 38 | z = clock() - x 39 | print(format("Compilation Time: %.6f (template)", z)) 40 | total = total + z 41 | 42 | compile(view, nil, true) 43 | 44 | gc() 45 | gc() 46 | 47 | x = clock() 48 | for _ = 1, iterations do 49 | compile(view, 1, true) 50 | end 51 | z = clock() - x 52 | print(format("Compilation Time: %.6f (template, cached)", z)) 53 | total = total + z 54 | 55 | local context = { "Emma", "James", "Nicholas", "Mary" } 56 | 57 | template.cache = {} 58 | 59 | gc() 60 | gc() 61 | 62 | x = clock() 63 | for _ = 1, iterations do 64 | compile(view, 1, true)(context) 65 | template.cache = {} 66 | end 67 | z = clock() - x 68 | print(format(" Execution Time: %.6f (same template)", z)) 69 | total = total + z 70 | 71 | template.cache = {} 72 | compile(view, 1, true) 73 | 74 | gc() 75 | gc() 76 | 77 | x = clock() 78 | for _ = 1, iterations do 79 | compile(view, 1, true)(context) 80 | end 81 | z = clock() - x 82 | print(format(" Execution Time: %.6f (same template, cached)", z)) 83 | total = total + z 84 | 85 | template.cache = {} 86 | 87 | local views = new_tab(iterations, 0) 88 | for i = 1, iterations do 89 | views[i] = "

Iteration " .. i .. "

\n" .. view 90 | end 91 | 92 | gc() 93 | gc() 94 | 95 | x = clock() 96 | for i = 1, iterations do 97 | compile(views[i], i, true)(context) 98 | end 99 | z = clock() - x 100 | print(format(" Execution Time: %.6f (different template)", z)) 101 | total = total + z 102 | 103 | gc() 104 | gc() 105 | 106 | x = clock() 107 | for i = 1, iterations do 108 | compile(views[i], i, true)(context) 109 | end 110 | z = clock() - x 111 | print(format(" Execution Time: %.6f (different template, cached)", z)) 112 | total = total + z 113 | 114 | local contexts = new_tab(iterations, 0) 115 | 116 | for i = 1, iterations do 117 | contexts[i] = { "Emma", "James", "Nicholas", "Mary" } 118 | end 119 | 120 | template.cache = {} 121 | 122 | gc() 123 | gc() 124 | 125 | x = clock() 126 | for i = 1, iterations do 127 | compile(views[i], i, true)(contexts[i]) 128 | end 129 | z = clock() - x 130 | print(format(" Execution Time: %.6f (different template, different context)", z)) 131 | total = total + z 132 | 133 | gc() 134 | gc() 135 | 136 | x = clock() 137 | for i = 1, iterations do 138 | compile(views[i], i, true)(contexts[i]) 139 | end 140 | z = clock() - x 141 | print(format(" Execution Time: %.6f (different template, different context, cached)", z)) 142 | total = total + z 143 | print(format(" Total Time: %.6f", total)) 144 | end 145 | 146 | return { 147 | run = run 148 | } -------------------------------------------------------------------------------- /spec/cases/basic_spec.lua: -------------------------------------------------------------------------------- 1 | expose("expose modules", function() 2 | package.path = '../../lib/?.lua;' .. '../../?.lua;'.. './lib/?.lua;' .. package.path 3 | _G.lor = require("lor.index") 4 | _G.request = require("spec.cases.mock_request") 5 | _G.response = require("spec.cases.mock_response") 6 | 7 | _G.json_view = function(t) 8 | local cjson 9 | pcall(function() cjson = require("cjson") end) 10 | if not cjson then 11 | print("\n[cjson should be installed...]\n") 12 | else 13 | if t.root then 14 | t:remove_nested_property(t.root) 15 | print("\n", cjson.encode(t.root), "\n") 16 | else 17 | t:remove_nested_property(t) 18 | print("\n", cjson.encode(t), "\n") 19 | end 20 | end 21 | end 22 | 23 | _G._debug = nil 24 | pcall(function() _G._debug = require("lor.lib.debug") end) 25 | if not _G._debug then 26 | _G._debug = print 27 | end 28 | end) 29 | -------------------------------------------------------------------------------- /spec/cases/common_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | describe("basic test for common usages", function() 22 | it("use middleware should works.", function() 23 | local count = 1 24 | app:use("/user", function(req, res, next) 25 | count = 2 26 | next() 27 | count = 5 28 | end) 29 | 30 | app:use("/user/123", function(req, res, next) 31 | count = 3 32 | next() 33 | end) 34 | 35 | app:get("/user/:id/create", function(req, res, next) 36 | count = 4 37 | end) 38 | 39 | req.path ="/user/123/create" 40 | req.method = "get" 41 | app:handle(req, res) 42 | assert.is.equals(count, 5) 43 | end) 44 | 45 | it("error middleware should work.", function() 46 | local origin_error_msg, error_msg = "this is an error", "" 47 | app:use("/user", function(req, res, next) 48 | next() 49 | end) 50 | 51 | app:get("/user/123/create", function(req, res, next) 52 | next(origin_error_msg) -- let other handlers continue... 53 | end) 54 | 55 | app:erroruse(function(err, req, res, next) 56 | error_msg = err 57 | end) 58 | 59 | req.path = "/user/123/create" 60 | req.method = "get" 61 | app:handle(req, res) 62 | assert.is.equals(error_msg, origin_error_msg) 63 | end) 64 | end) 65 | -------------------------------------------------------------------------------- /spec/cases/error_middleware_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | describe("error middleware test:", function() 23 | it("error middleware should stop the left error middlewares if has no `next`.", function() 24 | local count = 1 25 | app:use("/user", function(req, res, next) 26 | count = 2 27 | next() 28 | end) 29 | 30 | app:use("/user/123", function(req, res, next) 31 | count = 3 32 | next() 33 | end) 34 | 35 | app:get("/user/123/create", function(req, res, next) 36 | count = 4 37 | error("an error occurs") 38 | end) 39 | 40 | app:erroruse(function(err, req, res, next) 41 | count = 5 42 | end) 43 | 44 | app:erroruse(function(err, req, res, next) 45 | count = 100 46 | end) 47 | 48 | req.path = "/user/123/create" 49 | req.method = "get" 50 | app:handle(req, res) 51 | assert.is.equals(count, 5) 52 | end) 53 | 54 | it("error middleware should continue the left error middlewares if has `next`.", function() 55 | local count = 1 56 | app:use("/user", function(req, res, next) 57 | count = 2 58 | next() 59 | end) 60 | 61 | app:use("/user/123", function(req, res, next) 62 | count = 3 63 | next() 64 | end) 65 | 66 | app:get("/user/123/create", function(req, res, next) 67 | count = 4 68 | error("an error occurs") 69 | end) 70 | 71 | app:erroruse(function(err, req, res, next) 72 | count = 5 73 | next(err) 74 | end) 75 | 76 | app:erroruse(function(err, req, res, next) 77 | assert.is.truthy(err) 78 | count = 100 79 | end) 80 | 81 | req.path = "/user/123/create" 82 | req.method = "get" 83 | app:handle(req, res) 84 | assert.is.equals(count, 100) 85 | end) 86 | 87 | describe("if finall handler defined, it will always be executed", function() 88 | it("error middleware should continue the left error middlewares if has `next`.", function() 89 | local count = 1 90 | app:use("/user", function(req, res, next) 91 | count = 2 92 | next() 93 | end) 94 | 95 | app:use("/user/123", function(req, res, next) 96 | count = 3 97 | next() 98 | end) 99 | 100 | req.path = "/user/123/create" 101 | req.method = "get" 102 | app:handle(req, res, function(err) 103 | count = 111 104 | end) 105 | assert.is.equals(count, 111) 106 | end) 107 | end) 108 | end) 109 | -------------------------------------------------------------------------------- /spec/cases/final_handler_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | -- remind: final handler is the last middleware that could be used to handle errors 23 | -- it will alwayes be executed but `err` object is not nil only when error occurs 24 | describe("if finall handler defined, it will always be executed.", function() 25 | it("the request has no execution", function() 26 | local count = 1 27 | app:use("/user", function(req, res, next) 28 | count = 2 29 | next() 30 | end) 31 | 32 | app:use("/user/123", function(req, res, next) 33 | count = 3 34 | next() 35 | end) 36 | 37 | req.path = "/user/123/create" 38 | req.method = "get" 39 | app:handle(req, res, function(err) 40 | count = 111 41 | end) 42 | assert.is.equals(count, 111) 43 | end) 44 | 45 | it("404! should reach the final handler", function() 46 | local count = 1 47 | 48 | app:use("/user", function(req, res, next) -- won't enter 49 | count = 2 50 | next() 51 | end) 52 | 53 | app:get("/user/123", function(req, res, next) -- won't enter 54 | count = 4 55 | next() 56 | end) 57 | 58 | req.path = "/user/123/create" -- won't match app:get("/user/123", function...) 59 | req.method = "get" 60 | app:handle(req, res, function(err) -- 404! not found error 61 | assert.is_truthy(err) 62 | if err then 63 | count = 404 64 | end 65 | end) 66 | assert.is.equals(404, count) 67 | end) 68 | end) 69 | 70 | 71 | describe("the request has one successful execution. final handler execs but `err` should be nil.", function() 72 | it("test case 2", function() 73 | local count = 1 74 | app:use("/user", function(req, res, next) 75 | count = 2 76 | next() 77 | end) 78 | 79 | app:get("/user/123/create", function(req, res, next) 80 | count = 4 81 | next() 82 | end) 83 | 84 | req.path = "/user/123/create" 85 | req.method = "get" 86 | app:handle(req, res, function(err) 87 | assert.is_falsy(err) -- err should be nil 88 | assert.is_true(req:is_found()) -- matched app:get("/user/123/create") 89 | count = 222 -- 90 | end) 91 | assert.is.equals(count, 222) 92 | end) 93 | end) 94 | 95 | describe("the previous error middleware pass or not pass the `err` object.", function() 96 | it("test case 1.", function() 97 | local count = 1 98 | app:use("/user", function(req, res, next) 99 | count = 2 100 | next() 101 | end) 102 | 103 | app:erroruse(function(err, req, res, next) 104 | count = 5 105 | end) 106 | 107 | req.path = "/user/123/create" 108 | req.method = "get" 109 | app:handle(req, res, function(err) 110 | assert.is.equals(count, 5) -- not found: should match error middleware, so count is 5 111 | count = 444 112 | if err then 113 | count = 333 114 | end 115 | 116 | assert.is.equals(count, 333) 117 | end) 118 | end) 119 | 120 | 121 | it("test case 2.", function() 122 | local count = 1 123 | 124 | app:get("/user/123", function(req, res, next) 125 | count = 4 126 | error("abc") 127 | end) 128 | 129 | app:erroruse(function(err, req, res, next) 130 | count = 5 131 | assert.is.equals(true, string.find(err, "abc")>0) 132 | next("def") 133 | end) 134 | 135 | app:erroruse(function(err, req, res, next) 136 | count = 6 137 | assert.is.equals("def", err) 138 | next("123") 139 | end) 140 | 141 | req.path = "/user/123" 142 | req.method = "get" 143 | app:handle(req, res, function(err) 144 | count = 333 145 | if err then 146 | count = 222 147 | end 148 | assert.is.equals(222, count) 149 | end) 150 | end) 151 | 152 | it("test case 3, when error occurs in `error middleware`, the process will jump to the final handler immediately.", function() 153 | app:get("/user/123", function(req, res, next) 154 | error("ERROR1") 155 | end) 156 | 157 | -- error middleware 1 158 | app:erroruse(function(err, req, res, next) 159 | local test_var = 1 / tonumber("error number") -- error occurs here 160 | next(err .. "\nERROR2") -- won't be reached 161 | end) 162 | 163 | -- error middleware 2 164 | -- won't be matched because an error `in error middleware` occurs before it 165 | app:erroruse(function(err, req, res, next) 166 | next(err .. "\nERROR3") 167 | end) 168 | 169 | req.path = "/user/123" 170 | req.method = "get" 171 | app:handle(req, res, function(err) 172 | assert.is.equals(true, string.find(err, "ERROR1") > 0) 173 | assert.is.equals(nil, string.find(err, "ERROR2")) -- matched `error middleware1`, but error occured 174 | assert.is.equals(nil, string.find(err, "ERROR3")) -- not matched `error middleware2` 175 | assert.is.equals(true, string.find(err, "perform arithmetic on a nil value") > 0) 176 | end) 177 | end) 178 | end) 179 | -------------------------------------------------------------------------------- /spec/cases/group_index_route_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | describe("group index route: basic usages", function() 23 | it("should be error when giving wrong params", function() 24 | local flag = 0 25 | local test_router = lor:Router() 26 | 27 | assert.has_error(function() test_router:get() end, "params should not be nil or empty") 28 | assert.has_error(function() test_router:get({}) end, "params should not be nil or empty") 29 | 30 | assert.has_error(function() test_router:get("/test") end, "it must be an function if there's only one param") 31 | assert.has_error(function() test_router:get("/test", "abc") end) 32 | end) 33 | 34 | it("uri should mathed", function() 35 | local flag = 0 36 | 37 | local test_router = lor:Router() 38 | test_router:get(function(req, res, next) 39 | flag = 1 40 | end) 41 | 42 | app:use("/test", test_router()) 43 | 44 | req.path = "/test" 45 | req.method = "get" 46 | app:handle(req, res) 47 | assert.is.equals(1, flag) 48 | end) 49 | 50 | it("uri should not mathed", function() 51 | local flag = 0 52 | 53 | local test_router = lor:Router() 54 | test_router:get(function(req, res, next) 55 | flag = 1 56 | end) 57 | 58 | app:use("/test", test_router()) 59 | app:erroruse(function(err, req, res, next) -- 404 error 60 | assert.is.truthy(err) 61 | assert.is.equals(false, req:is_found()) 62 | flag = 999 63 | end) 64 | 65 | req.path = "/test/" 66 | req.method = "get" 67 | app:handle(req, res) 68 | assert.is.equals(999, flag) 69 | end) 70 | end) 71 | 72 | 73 | describe("group index route: multi funcs", function() 74 | it("array params", function() 75 | local flag = 0 76 | local test_router = lor:Router() 77 | local func1 = function(req, res, next) 78 | flag = 1 79 | next() 80 | end 81 | local func2 = function(req, res, next) 82 | flag = 2 83 | next() 84 | end 85 | local last_func = function(req, res, next) 86 | flag = 3 87 | end 88 | test_router:post({func1, func2, last_func}) 89 | app:use("/test", test_router()) 90 | 91 | req.path = "/test" 92 | req.method = "post" 93 | app:handle(req, res) 94 | assert.is.equals(3, flag) 95 | end) 96 | 97 | it("unpacked params", function() 98 | local flag = 0 99 | local test_router = lor:Router() 100 | local func1 = function(req, res, next) 101 | flag = 1 102 | next() 103 | end 104 | local func2 = function(req, res, next) 105 | flag = 2 106 | next() 107 | end 108 | local last_func = function(req, res, next) 109 | flag = 3 110 | end 111 | test_router:put(func1, func2, last_func) 112 | app:use("/test", test_router()) 113 | 114 | req.path = "/test" 115 | req.method = "put" 116 | app:handle(req, res) 117 | assert.is.equals(3, flag) 118 | end) 119 | 120 | it("mixed params, case1", function() 121 | local flag = 0 122 | local test_router = lor:Router() 123 | local func1 = function(req, res, next) 124 | flag = 1 125 | next() 126 | end 127 | local func2 = function(req, res, next) 128 | flag = 2 129 | next() 130 | end 131 | local last_func = function(req, res, next) 132 | flag = 3 133 | end 134 | test_router:get({func1, func2}, last_func) 135 | app:use("/test", test_router()) 136 | 137 | req.path = "/test" 138 | req.method = "get" 139 | app:handle(req, res) 140 | assert.is.equals(3, flag) 141 | end) 142 | 143 | it("mixed params, case2", function() 144 | local flag = 0 145 | local test_router = lor:Router() 146 | local func1 = function(req, res, next) 147 | flag = 1 148 | next() 149 | end 150 | local func2 = function(req, res, next) 151 | flag = 2 152 | next() 153 | end 154 | local last_func = function(req, res, next) 155 | flag = 3 156 | end 157 | test_router:put({func1}, func2, {last_func}) 158 | app:use("/test", test_router()) 159 | 160 | req.path = "/test" 161 | req.method = "put" 162 | app:handle(req, res) 163 | assert.is.equals(3, flag) 164 | end) 165 | end) 166 | -------------------------------------------------------------------------------- /spec/cases/group_router_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | describe("single group router middleware", function() 23 | it("basic usage", function() 24 | local count = 0 25 | local userRouter = lor:Router() 26 | 27 | userRouter:get("/find/:id", function(req, res, next) 28 | count = 1 29 | next() 30 | end) 31 | 32 | userRouter:post("/create/:id", function(req, res, next) 33 | count = 2 34 | next() 35 | end) 36 | 37 | app:use("/user", userRouter()) 38 | 39 | -- should not be reached, because `next` has no params and no error occurs 40 | app:erroruse(function(err, req, res, next) 41 | count = 3 42 | end) 43 | 44 | req.path = "/user/create/123" 45 | req.method = "post" 46 | app:handle(req, res) 47 | assert.is.equals(2, count) 48 | 49 | req.path = "/user/find/123" 50 | req.method = "get" 51 | app:handle(req, res) 52 | assert.is.equals(1, count) 53 | end) 54 | 55 | 56 | it("error middleware should work as normal", function() 57 | local count = 0 58 | local errorMsg = "an error occurs." 59 | local userRouter = lor:Router() 60 | 61 | userRouter:get("/find/:id", function(req, res, next) 62 | count = 1 63 | error(errorMsg) 64 | end) 65 | 66 | userRouter:post("/create/:id", function(req, res, next) 67 | count = 2 68 | next(errorMsg) -- has one param, so this will pass to an error middleware 69 | end) 70 | 71 | userRouter:post("/edit/:id", function(req, res, next) 72 | count = 3 73 | end) 74 | 75 | app:use("/user", userRouter()) 76 | 77 | app:erroruse(function(err, req, res, next) 78 | if err then -- double check 79 | count = err 80 | req.params.id = "11111111" 81 | end 82 | end) 83 | 84 | req.path = "/user/find/456" 85 | req.method = "get" 86 | app:handle(req, res) 87 | assert.is.truthy(string.match(count, errorMsg)) 88 | 89 | req.path = "/user/create/123" 90 | req.method = "post" 91 | app:handle(req, res) 92 | assert.is.truthy(string.match(count, errorMsg)) -- count contains errorMsg 93 | 94 | req.path = "/user/edit/987" 95 | req.method = "post" 96 | app:handle(req, res) 97 | assert.is.equals(3, count) -- no error occurs 98 | end) 99 | 100 | it("path variable parser should work", function() 101 | local count = 0 102 | local errorMsg = "an error occurs." 103 | local userRouter = lor:Router() 104 | 105 | userRouter:get("/find/:id", function(req, res, next) 106 | count = 1 107 | error(errorMsg) 108 | end) 109 | 110 | userRouter:post("/create/:id", function(req, res, next) 111 | count = 2 112 | next(errorMsg) -- has one param, so this will pass to an error middleware 113 | end) 114 | 115 | userRouter:post("/edit/:id", function(req, res, next) 116 | count = 3 117 | end) 118 | 119 | app:use("/user", userRouter()) 120 | 121 | app:erroruse("/user", function(err, req, res, next) 122 | --print("user error middleware", err) 123 | --if err then -- double check 124 | count = err 125 | req.params.id = "22222" 126 | --end 127 | next(err) -- 继续传递,只会被下一个erroruse覆盖 128 | end) 129 | 130 | app:erroruse(function(err, req, res, next) 131 | --print("common error middleware", err) 132 | if err then -- double check 133 | count = err 134 | req.params.id = "11111111" 135 | end 136 | end) 137 | 138 | req.path = "/user/find/456" 139 | req.method = "get" 140 | app:handle(req, res) 141 | assert.is.equals("11111111", req.params.id) 142 | assert.is.truthy(string.match(count, errorMsg)) 143 | 144 | req.path = "/user/create/123" 145 | req.method = "post" 146 | app:handle(req, res) 147 | assert.is.equals("11111111", req.params.id) 148 | assert.is.truthy(string.match(count, errorMsg)) -- count contains errorMsg 149 | 150 | req.path = "/user/edit/987" 151 | req.method = "post" 152 | app:handle(req, res) 153 | assert.is.equals("987", req.params.id) 154 | assert.is.equals(3, count) 155 | end) 156 | end) 157 | 158 | describe("multi group router middleware", function() 159 | it("basic usage", function() 160 | local flag = "" 161 | local userRouter = lor:Router() 162 | local bookRouter = lor:Router() 163 | 164 | app:use(function(req, res, next) 165 | flag = "first use" 166 | req.params.flag = "origin value" 167 | next() 168 | end) 169 | 170 | userRouter:get("/find/:id", function(req, res, next) 171 | flag = 1 172 | req.params.flag = 1 173 | next("error occurs") 174 | end) 175 | 176 | userRouter:post("/create/:id", function(req, res, next) 177 | flag = 2 178 | req.params.flag = 2 179 | next() 180 | end) 181 | 182 | bookRouter:get("/view/:id", function(req, res, next) 183 | flag = 3 184 | req.params.flag = 3 185 | error("common error") 186 | req.params.flag = 33 187 | end) 188 | 189 | app:use("/user", userRouter()) -- must invoke before `erroruse` 190 | app:use("/book", bookRouter()) -- must invoke before `erroruse` 191 | 192 | app:erroruse("/user", function(err, req, res, next) 193 | --print("------------- user error m", err) 194 | assert.is.truthy(err) 195 | flag = "user_error_middleware" 196 | req.params.flag = 111 197 | end) 198 | 199 | app:erroruse(function(err, req, res, next) 200 | --print("------------- common error m", err) 201 | flag = "common_error_middleware" 202 | req.params.flag = 333 203 | assert.is.truthy(err) 204 | end) 205 | 206 | req.path = "/user/create/123" 207 | req.method = "post" 208 | app:handle(req, res) 209 | assert.is.equals(2, flag) 210 | assert.is.equals(2, req.params.flag) 211 | 212 | req.path = "/user/find/123" 213 | req.method = "get" 214 | app:handle(req, res) 215 | assert.is.equals("user_error_middleware", flag) 216 | assert.is.equals(111, req.params.flag) 217 | 218 | req.path = "/book/view/999" 219 | req.method = "get" 220 | app:handle(req, res, function(err) 221 | if err then 222 | print(err) 223 | end 224 | -- not found path will be here 225 | -- not processed error will be here 226 | end) 227 | assert.is.equals("common_error_middleware", flag) 228 | assert.is.equals(333, req.params.flag) 229 | end) 230 | end) 231 | 232 | describe("'use' the same group router middleware", function() 233 | it("basic usage", function() 234 | local userRouter = lor:Router() 235 | 236 | local reach_first = false 237 | local reach_second = false 238 | 239 | userRouter:get("/get", function(req, res, next) 240 | reach_first = true 241 | reach_second = true 242 | end) 243 | 244 | app:use("/user", userRouter()) 245 | app:use("/u", userRouter()) 246 | 247 | req.path = "/user/get" 248 | req.method = "get" 249 | app:handle(req, res) 250 | assert.is.equals(true, reach_first) 251 | 252 | -- reset values 253 | reach_first = false 254 | reach_second = false 255 | 256 | req.path = "/u/get" 257 | req.method = "get" 258 | app:handle(req, res) 259 | assert.is.equals(true, reach_second) 260 | end) 261 | end) 262 | -------------------------------------------------------------------------------- /spec/cases/mock_request.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | 3 | local Request = {} 4 | 5 | function Request:new() 6 | local body = {} -- body params 7 | 8 | local params = {} 9 | 10 | local instance = { 11 | method = "", 12 | query = {}, 13 | params = {}, 14 | body = body, 15 | path = "", 16 | url = "", 17 | uri = "", 18 | req_args = {}, 19 | baseUrl = "", 20 | found = false 21 | } 22 | setmetatable(instance, { __index = self }) 23 | return instance 24 | end 25 | 26 | function Request:is_found() 27 | return self.found 28 | end 29 | 30 | function Request:set_found(found) 31 | self.found = found 32 | end 33 | 34 | 35 | return Request -------------------------------------------------------------------------------- /spec/cases/mock_response.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | 3 | local Response = {} 4 | 5 | function Response:new() 6 | local instance = { 7 | headers = {}, 8 | body = '--default body. you should not see this by default--', 9 | view = nil, 10 | } 11 | 12 | setmetatable(instance, {__index = self}) 13 | return instance 14 | end 15 | 16 | function Response:json(data) 17 | self:_send(data) 18 | end 19 | 20 | function Response:send(text) 21 | self:_send(text) 22 | end 23 | 24 | function Response:_send(content) 25 | print(content) 26 | end 27 | 28 | function Response:setHeader(key, value) 29 | end 30 | 31 | function Response:status(code) 32 | self.http_status = code 33 | return self 34 | end 35 | 36 | return Response 37 | -------------------------------------------------------------------------------- /spec/cases/multi_route_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | describe("multi route: mounted on `app`", function() 23 | it("array param", function() 24 | local flag = 0 25 | local func1 = function(req, res, next) 26 | flag = 1 27 | next() 28 | end 29 | local func2 = function(req, res, next) 30 | flag = 2 31 | next() 32 | end 33 | local last_func = function(req, res, next) 34 | flag = req.query.flag or 3 35 | end 36 | app:get("/flag", {func1, func2, last_func}) 37 | 38 | app:erroruse(function(err, req, res, next) 39 | assert.is.truthy(err) -- should not reach here. 40 | flag = 999 41 | end) 42 | 43 | req.path = "/flag" 44 | req.method = "get" 45 | app:handle(req, res) 46 | assert.is.equals(3, flag) 47 | 48 | req.path = "/flag" 49 | req.query = {flag=111} 50 | req.method = "get" 51 | app:handle(req, res) 52 | assert.is.equals(111, flag) 53 | end) 54 | 55 | it("unpacked params", function() 56 | local flag = 0 57 | local func1 = function(req, res, next) 58 | flag = 1 59 | next() 60 | end 61 | local func2 = function(req, res, next) 62 | flag = 2 63 | next() 64 | end 65 | local last_func = function(req, res, next) 66 | flag = req.query.flag or 3 67 | end 68 | app:get("/flag", func1, func2, last_func) 69 | 70 | app:erroruse(function(err, req, res, next) 71 | assert.is.truthy(err) -- should not reach here. 72 | flag = 999 73 | end) 74 | 75 | req.path = "/flag" 76 | req.method = "get" 77 | app:handle(req, res) 78 | assert.is.equals(3, flag) 79 | 80 | req.path = "/flag" 81 | req.query = {flag=111} 82 | req.method = "get" 83 | app:handle(req, res) 84 | assert.is.equals(111, flag) 85 | end) 86 | end) 87 | 88 | describe("multi route: mounted on `group router`", function() 89 | it("array param", function() 90 | local flag = 0 91 | 92 | local test_router = lor:Router() 93 | local func1 = function(req, res, next) 94 | flag = 1 95 | next() 96 | end 97 | local func2 = function(req, res, next) 98 | flag = 2 99 | next() 100 | end 101 | local last_func = function(req, res, next) 102 | flag = req.query.flag or 3 103 | end 104 | test_router:get("/flag", {func1, func2, last_func}) 105 | 106 | app:use("/test", test_router()) 107 | app:erroruse(function(err, req, res, next) 108 | assert.is.truthy(err) -- should not reach here. 109 | flag = 999 110 | end) 111 | 112 | req.path = "/test/flag" 113 | req.method = "get" 114 | app:handle(req, res) 115 | assert.is.equals(3, flag) 116 | 117 | req.path = "/test/flag" 118 | req.query = {flag=111} 119 | req.method = "get" 120 | app:handle(req, res) 121 | assert.is.equals(111, flag) 122 | end) 123 | 124 | it("unpacked params", function() 125 | local flag = 0 126 | 127 | local test_router = lor:Router() 128 | local func1 = function(req, res, next) 129 | flag = 1 130 | next() 131 | end 132 | local func2 = function(req, res, next) 133 | flag = 2 134 | next() 135 | end 136 | local last_func = function(req, res, next) 137 | flag = req.query.flag or 3 138 | end 139 | test_router:get("/flag", func1, func2, last_func) 140 | 141 | app:use("/test", test_router()) 142 | app:erroruse(function(err, req, res, next) 143 | assert.is.truthy(err) -- should not reach here. 144 | flag = 999 145 | end) 146 | 147 | req.path = "/test/flag" 148 | req.method = "get" 149 | app:handle(req, res) 150 | assert.is.equals(3, flag) 151 | 152 | req.path = "/test/flag" 153 | req.query = {flag=111} 154 | req.method = "get" 155 | app:handle(req, res) 156 | assert.is.equals(111, flag) 157 | end) 158 | end) 159 | 160 | describe("multi route: muixed funcs for group router", function() 161 | it("mixed params, case1", function() 162 | local flag = 0 163 | local test_router = lor:Router() 164 | local func1 = function(req, res, next) 165 | flag = 1 166 | next() 167 | end 168 | local func2 = function(req, res, next) 169 | flag = 2 170 | next() 171 | end 172 | local last_func = function(req, res, next) 173 | flag = 3 174 | end 175 | test_router:put("mixed", {func1, func2}, last_func) 176 | app:use("/test", test_router()) 177 | 178 | req.path = "/test/mixed" 179 | req.method = "put" 180 | app:handle(req, res) 181 | assert.is.equals(3, flag) 182 | end) 183 | 184 | it("mixed params, case2", function() 185 | local flag = 0 186 | local test_router = lor:Router() 187 | local func1 = function(req, res, next) 188 | flag = 1 189 | next() 190 | end 191 | local func2 = function(req, res, next) 192 | flag = 2 193 | next() 194 | end 195 | local last_func = function(req, res, next) 196 | flag = 3 197 | end 198 | test_router:get("mixed", {func1}, func2, {last_func}) 199 | app:use("/test", test_router()) 200 | 201 | req.path = "/test/mixed" 202 | req.method = "get" 203 | app:handle(req, res) 204 | assert.is.equals(3, flag) 205 | end) 206 | end) 207 | 208 | describe("multi route: muixed funcs for `app`", function() 209 | it("mixed params, case1", function() 210 | local flag = 0 211 | local func1 = function(req, res, next) 212 | flag = 1 213 | next() 214 | end 215 | local func2 = function(req, res, next) 216 | flag = 2 217 | next() 218 | end 219 | local last_func = function(req, res, next) 220 | flag = 3 221 | end 222 | app:get("mixed", {func1, func2}, last_func) 223 | 224 | req.path = "/mixed" 225 | req.method = "get" 226 | app:handle(req, res) 227 | assert.is.equals(3, flag) 228 | end) 229 | 230 | it("mixed params, case2", function() 231 | local flag = 0 232 | local func1 = function(req, res, next) 233 | flag = 1 234 | next() 235 | end 236 | local func2 = function(req, res, next) 237 | flag = 2 238 | next() 239 | end 240 | local func3 = function(req, res, next) 241 | flag = 3 242 | next() 243 | end 244 | local last_func = function(req, res, next) 245 | flag = 4 246 | end 247 | app:get("mixed", {func1}, func2, {func3}, last_func) 248 | 249 | req.path = "/mixed" 250 | req.method = "get" 251 | app:handle(req, res) 252 | assert.is.equals(4, flag) 253 | end) 254 | end) 255 | -------------------------------------------------------------------------------- /spec/cases/node_id_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | Trie = require("lor.lib.trie") 3 | t = Trie:new() 4 | end) 5 | 6 | after_each(function() 7 | t = nil 8 | end) 9 | 10 | function table_size(t) 11 | local res = 0 12 | if t then 13 | for _ in pairs(t) do 14 | res = res + 1 15 | end 16 | end 17 | return res 18 | end 19 | 20 | describe("node is should be unique", function() 21 | it("test case 1", function() 22 | local count = 100 23 | local nodes = {} 24 | for i=1,count,1 do 25 | local node = t:add_node(tostring(i)) 26 | table.insert(nodes, node) 27 | end 28 | 29 | assert.is.equals(count, #nodes) 30 | assert.is.equals(count, table_size(nodes)) 31 | 32 | local node_map = {} 33 | for i=1,count,1 do 34 | node_map[nodes[i].id] = true 35 | end 36 | assert.is.equals(count, table_size(node_map)) 37 | end) 38 | end) 39 | -------------------------------------------------------------------------------- /spec/cases/not_found_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | describe("not found test, error middleware and final handler should be reached correctly.", function() 22 | it("test case 1", function() 23 | local count = 0 24 | local errorMsg = "an error occurs." 25 | 26 | local userRouter = lor:Router() 27 | userRouter:get("/find/:id", function(req, res, next) 28 | count = 1 29 | error(errorMsg) 30 | end) 31 | app:use("/user", userRouter()) 32 | 33 | app:erroruse("/user", function(err, req, res, next) 34 | count = err 35 | req.params.id = "2222" 36 | next(err) 37 | end) 38 | 39 | app:erroruse(function(err, req, res, next) 40 | count = err 41 | req.params.id = "1111" 42 | end) 43 | 44 | req.path = "/user/find/456" 45 | req.method = "get" 46 | app:handle(req, res) 47 | assert.is.equals(req.params.id, "1111") 48 | assert.is.equals(true, string.find(count, errorMsg) > 1) 49 | end) 50 | 51 | it("test case 2", function() 52 | local count = 0 53 | local errorMsg = "an error occurs." 54 | 55 | local userRouter = lor:Router() 56 | userRouter:get("/find/:id", function(req, res, next) 57 | count = 1 58 | error(errorMsg) 59 | end) 60 | app:use("/user", userRouter()) 61 | 62 | app:erroruse("/user", function(err, req, res, next) 63 | count = err 64 | req.params.id = "2222" 65 | next(err) 66 | end) 67 | 68 | app:erroruse(function(err, req, res, next) 69 | count = "stop exec final handler" 70 | req.params.id = "1111" 71 | -- next(err) -- not invoke it, the final handler will not be reached! 72 | end) 73 | 74 | req.params.id = nil --empty it 75 | req.path = "/user/notfound" 76 | req.method = "post" 77 | app:handle(req, res, function(err) 78 | if err then 79 | count = "not found error catched" 80 | end 81 | end) 82 | 83 | assert.is.equals(404, res.http_status) 84 | assert.is.equals(req.params.id, "1111") 85 | assert.is.equals("stop exec final handler", count) 86 | end) 87 | 88 | it("test case 3", function() 89 | local count = 0 90 | local errorMsg = "an error occurs." 91 | 92 | local userRouter = lor:Router() 93 | userRouter:get("/find/:id", function(req, res, next) 94 | count = 1 95 | error(errorMsg) 96 | end) 97 | app:use("/user", userRouter()) 98 | 99 | app:erroruse("/user", function(err, req, res, next) 100 | count = err 101 | req.params.id = "2222" 102 | next(err) 103 | end) 104 | 105 | app:erroruse(function(err, req, res, next) 106 | count = "stop exec final handler" 107 | req.params.id = "1111" 108 | next(err) -- invoke it, the final handler will be reached! 109 | end) 110 | 111 | req.params.id = nil --empty it 112 | req.path = "/notfound" 113 | req.method = "post" 114 | app:handle(req, res, function(err) 115 | req.params.id = "3333" 116 | if err then 117 | count = "not found error catched" 118 | end 119 | end) 120 | --print(app.router.trie:gen_graph()) 121 | assert.is.equals(404, res.http_status) 122 | assert.is.equals(req.params.id, "3333") 123 | assert.is.equals("not found error catched", count) 124 | end) 125 | end) 126 | -------------------------------------------------------------------------------- /spec/cases/path_params_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = true 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | end) 11 | 12 | after_each(function() 13 | lor = nil 14 | app = nil 15 | Request = nil 16 | Response = nil 17 | req = nil 18 | res = nil 19 | end) 20 | 21 | 22 | describe("test about variables parsed from path", function() 23 | describe("path variables should be correct after parsed", function() 24 | it("test case 1.", function() 25 | app:use("/user", function(req, res, next) 26 | req.params.default_var = "user" 27 | next() 28 | end) 29 | 30 | app:get("/user/:id/visit", function(req, res, next) 31 | next() 32 | end) 33 | 34 | req.path = "/user/3/visit" 35 | req.method = "get" 36 | app:handle(req, res) 37 | 38 | assert.is.equals('3', req.params.id) 39 | assert.is.equals("user", req.params.default_var) 40 | end) 41 | 42 | it("test case 2.", function() 43 | app:use("/user", function(req, res, next) 44 | assert.is.equals("3", req.params.id) 45 | next() 46 | end) 47 | 48 | app:get("/user/:id/visit", function(req, res, next) 49 | req.params.id = 5 50 | next() 51 | end) 52 | 53 | req.path = "/user/3/visit" 54 | req.method = "get" 55 | 56 | app:handle(req, res) 57 | assert.is.equals(5, req.params.id) 58 | end) 59 | 60 | it("test case 3.", function() 61 | app:get("/user/:id/visit", function(req, res, next) 62 | error("error occurs") 63 | req.params.id = '2' 64 | end) 65 | 66 | app:erroruse("/user/:id/visit", function(err, req, res, next) 67 | assert.is_not_nil(err) 68 | req.params.id = 'error' 69 | end) 70 | 71 | req.path = "/user/3/visit" 72 | req.method = "get" 73 | 74 | app:handle(req, res) 75 | assert.is.equals('error', req.params.id) 76 | end) 77 | 78 | it("test case 4.", function() 79 | app:use("/user", function(req, res, next) 80 | req.params.id = '1' 81 | next() 82 | req.params.id = 'return' 83 | end) 84 | 85 | app:get("/user/:id/visit", function(req, res, next) 86 | error("error occurs") 87 | req.params.id = '2' 88 | end) 89 | 90 | app:erroruse("/user/:id/visit", function(err, req, res, next) 91 | req.params.id = 'error' 92 | end) 93 | 94 | req.path = "/user/3/visit" 95 | req.method = "get" 96 | 97 | app:handle(req, res) 98 | assert.is.equals('return', req.params.id) 99 | end) 100 | 101 | it("test case 5.", function() 102 | app:use("/user", function(req, res, next) 103 | req.params.id = '1' 104 | next() 105 | req.params.id = 'return' 106 | end) 107 | 108 | app:get("/user/:id/visit", function(req, res, next) 109 | error("error occurs") 110 | req.params.id = '2' 111 | end) 112 | 113 | app:erroruse("/user/:id/visit", function(err, req, res, next) 114 | req.params.id = 'error' 115 | end) 116 | 117 | req.path = "/user/3/visit" 118 | req.method = "get" 119 | app:handle(req, res, function(err) 120 | req.params.id = "from final handler" 121 | end) 122 | assert.is.equals('return', req.params.id) 123 | end) 124 | end) 125 | 126 | describe("path variables should be correctly parsed when the next request comes", function() 127 | it("test case 1.", function() 128 | app:use("/todo", function(req, res, next) 129 | if req.params.id == "33" then 130 | req.params.id = '3' 131 | elseif req.params.id == "44" then 132 | req.params.id = "4" 133 | elseif req.params.id == "55" then 134 | req.params.id = "5" 135 | end 136 | next() 137 | end) 138 | 139 | app:post("/todo/delete/:id", function(req, res, next) 140 | --print(req.params.id) 141 | end) 142 | 143 | req.path = "/todo/delete/33" 144 | req.method = "post" 145 | app:handle(req, res) 146 | assert.is.equals('3', req.params.id) 147 | 148 | req.path = "/todo/delete/44" 149 | req.method = "post" 150 | app:handle(req, res) 151 | assert.is.equals('4', req.params.id) 152 | 153 | req.url = "/todo/delete/55" 154 | req.path = req.url 155 | req.method = "post" 156 | app:handle(req, res) 157 | assert.is.equals('5', req.params.id) 158 | end) 159 | 160 | it("test case 2.", function() 161 | app:use("/todo", function(req, res, next) 162 | next() 163 | end) 164 | 165 | app:post("/todo/view/:id/:name", function(req, res, next) 166 | end) 167 | 168 | req.path = "/todo/view/44/two" 169 | req.method = "post" 170 | app:handle(req, res) 171 | assert.is.equals('44', req.params.id) 172 | assert.is.equals('two', req.params.name) 173 | 174 | req.path = "/todo/view/55/three" 175 | req.method = "post" 176 | app:handle(req, res) 177 | assert.is.equals('55', req.params.id) 178 | assert.is.equals('three', req.params.name) 179 | end) 180 | end) 181 | end) 182 | -------------------------------------------------------------------------------- /spec/cases/path_pattern_1_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = false 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | 11 | count = 0 12 | 13 | app:use("/", function(req, res, next) 14 | count = 1 15 | next() 16 | end) 17 | 18 | app:use("/user/", function(req, res, next) 19 | count = 2 20 | next() 21 | end) 22 | app:use("/user/:id/view", function(req, res, next) 23 | count = 3 24 | next() 25 | end) 26 | app:get("/user/123/view", function(req, res, next) 27 | count = 4 28 | next() 29 | end) 30 | 31 | app:post("/book" , function(req, res, next) 32 | count = 5 33 | next() 34 | end) 35 | 36 | local testRouter = lor:Router() -- 一个新的router,区别于主router 37 | testRouter:get("/get", function(req, res, next) 38 | count = 6 39 | next() 40 | end) 41 | testRouter:post("/foo/bar", function(req, res, next) 42 | count = 7 43 | next() 44 | end) 45 | app:use("/test", testRouter()) 46 | 47 | app:erroruse(function(err, req, res, next) 48 | count = 999 49 | end) 50 | end) 51 | 52 | after_each(function() 53 | lor = nil 54 | app = nil 55 | Request = nil 56 | Response = nil 57 | req = nil 58 | res = nil 59 | end) 60 | 61 | describe("next function usages test", function() 62 | it("test case 1", function() 63 | req.path = "/user/123/view" 64 | req.method = "get" 65 | app:handle(req, res, function(err) 66 | assert.is_true(req:is_found()) 67 | end) 68 | 69 | assert.is.equals(4, count) 70 | assert.is.equals(nil, req.params.id) 71 | end) 72 | 73 | it("test case 2", function() -- route found 74 | app:conf("strict_route", false) -- 设置为非严格匹配 75 | req.path = "/user/123/view/" -- match app:get("/user/123/view", fn()) 76 | req.method = "get" 77 | app:handle(req, res, function(err) 78 | assert.is_true(req:is_found()) 79 | end) 80 | 81 | assert.is.equals(4, count) 82 | assert.is.equals(nil, req.params.id) 83 | end) 84 | 85 | it("test case 3", function() 86 | req.path = "/book" 87 | req.method = "get" 88 | app:handle(req, res) 89 | 90 | assert.is.equals(999, count) 91 | assert.is_nil( req.params.id) 92 | 93 | req.method = "post" -- post match 94 | app:handle(req, res, function(err) 95 | assert.is_true(req:is_found()) 96 | end) 97 | 98 | assert.is.equals(5, count) 99 | assert.is_nil( req.params.id) 100 | end) 101 | 102 | it("test case 4", function() 103 | req.path = "/notfound" 104 | req.method = "get" 105 | app:handle(req, res, function(err) 106 | assert.is_not_true(req:is_found()) 107 | assert.is_nil(err) 108 | end) 109 | 110 | assert.is.equals(999, count) 111 | assert.is_nil(req.params.id) 112 | end) 113 | end) 114 | -------------------------------------------------------------------------------- /spec/cases/path_pattern_2_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = false 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | 11 | count = 0 12 | match = 1 13 | 14 | app:get("/all", function(req, res, next) 15 | count = 1 16 | end) 17 | 18 | local testRouter = lor:Router() 19 | testRouter:get("/all", function(req, res, next) 20 | count = 6 21 | match = 2 22 | next() 23 | end) 24 | testRouter:get("/find/:type", function(req, res, next) 25 | count = 7 26 | next() 27 | end) 28 | app:use("/test", testRouter()) 29 | 30 | end) 31 | 32 | after_each(function() 33 | lor = nil 34 | app = nil 35 | Request = nil 36 | Response = nil 37 | req = nil 38 | res = nil 39 | match = nil 40 | end) 41 | 42 | describe("path match test", function() 43 | it("test case 1", function() 44 | req.path = "/test/all" 45 | req.method = "get" 46 | app:handle(req, res) 47 | assert.is.equals(2, match) 48 | assert.is.equals(6, count) 49 | end) 50 | 51 | it("test case 2", function() 52 | req.path = "/test/find/all" 53 | req.method = "get" 54 | app:handle(req, res) 55 | assert.is.equals(1, match) -- should not match "/test/all" 56 | assert.is.equals(7, count) 57 | end) 58 | 59 | it("test case 3", function() 60 | req.path = "/test/find/all/1" 61 | req.method = "get" 62 | app:erroruse(function(err, req, res, next) -- 404 error 63 | assert.is.truthy(err) 64 | assert.is.equals(false, req:is_found()) 65 | end) 66 | app:handle(req, res) 67 | assert.is.equals(1, match) 68 | assert.is.equals(0, count) 69 | end) 70 | end) 71 | -------------------------------------------------------------------------------- /spec/cases/path_pattern_3_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = false 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | 11 | count = 0 12 | match = 1 13 | 14 | app:get("/hello", function(req, res, next) 15 | count = 1 16 | match = 2 17 | end) 18 | 19 | local testRouter = lor:Router() 20 | testRouter:get("/hello", function(req, res, next) 21 | match = 3 22 | end) 23 | 24 | app:use("/test", testRouter()) 25 | end) 26 | 27 | after_each(function() 28 | lor = nil 29 | app = nil 30 | Request = nil 31 | Response = nil 32 | req = nil 33 | res = nil 34 | match = nil 35 | end) 36 | 37 | 38 | describe("path match test", function() 39 | it("test case 1", function() 40 | req.path = "/test/hello" 41 | req.method = "get" 42 | app:handle(req, res) 43 | assert.is.equals(3, match) 44 | assert.is.equals(0, count) 45 | end) 46 | 47 | it("test case 2", function() 48 | req.path = "/hello" 49 | req.method = "get" 50 | app:handle(req, res) 51 | assert.is.equals(2, match) 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /spec/cases/uri_char_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | lor = _G.lor 3 | app = lor({ 4 | debug = false 5 | }) 6 | Request = _G.request 7 | Response = _G.response 8 | req = Request:new() 9 | res = Response:new() 10 | 11 | flag = 0 12 | end) 13 | 14 | after_each(function() 15 | lor = nil 16 | app = nil 17 | Request = nil 18 | Response = nil 19 | req = nil 20 | res = nil 21 | match = nil 22 | end) 23 | 24 | describe("uri should support `-`", function() 25 | it("test case 1", function() 26 | req.path = "/a-b-c" 27 | req.method = "get" 28 | app:get("/a-b-c", function(req, res, next) 29 | flag = 1 30 | end) 31 | app:handle(req, res) 32 | assert.is.equals(1, flag) 33 | end) 34 | 35 | it("test case 2", function() 36 | req.path = "/a_-b-/cde/-f" 37 | req.method = "get" 38 | app:get("/a_-b-/cde/-f", function(req, res, next) 39 | flag = 2 40 | end) 41 | app:handle(req, res) 42 | assert.is.equals(2, flag) 43 | end) 44 | 45 | it("test case 3", function() 46 | req.path = "/a_-b-%" 47 | req.method = "get" 48 | app:get("/a_-b-%", function(req, res, next) 49 | flag = 3 50 | end) 51 | app:handle(req, res) 52 | assert.is.equals(3, flag) 53 | end) 54 | end) 55 | -------------------------------------------------------------------------------- /spec/trie/basic_spec.lua: -------------------------------------------------------------------------------- 1 | expose("expose modules", function() 2 | package.path = '../../lib/?.lua;' .. '../../?.lua;'.. './lib/?.lua;' .. package.path 3 | _G.Trie = require("lor.lib.trie") 4 | _G.Node = require("lor.lib.node") 5 | 6 | _G.json_view = function(t) 7 | local cjson 8 | pcall(function() cjson = require("cjson") end) 9 | if not cjson then 10 | print("\n[cjson should be installed...]\n") 11 | else 12 | if t.root then 13 | t:remove_nested_property(t.root) 14 | print("\n", cjson.encode(t.root), "\n") 15 | else 16 | t:remove_nested_property(t) 17 | print("\n", cjson.encode(t), "\n") 18 | end 19 | end 20 | end 21 | 22 | _G._debug = nil 23 | pcall(function() _G._debug = require("lor.lib.debug") end) 24 | if not _G._debug then 25 | _G._debug = print 26 | end 27 | end) 28 | -------------------------------------------------------------------------------- /spec/trie/complex_cases_spec.lua: -------------------------------------------------------------------------------- 1 | before_each(function() 2 | Trie = _G.Trie 3 | t = Trie:new() 4 | t1 = Trie:new() 5 | t2 = Trie:new() 6 | end) 7 | 8 | after_each(function() 9 | Trie = nil 10 | t = nil 11 | t1 = nil 12 | t2 = nil 13 | _debug = nil 14 | end) 15 | 16 | describe("complex use cases: ", function() 17 | it("should match the correct colon node.", function() 18 | local n1 = t:add_node("/a/b/c/e") 19 | local colon_n1 = t:add_node("/a/b/:name/d") 20 | 21 | local m1 = t:match("/a/b/c/d") 22 | --print(t:gen_graph()) 23 | assert.are.same(colon_n1, m1.node) 24 | end) 25 | 26 | it("same prefix while different suffix.", function() 27 | local n1 = t:add_node("/a/b/c/e/f") 28 | local colon_n1 = t:add_node("/a/b/:name/d/g") 29 | local colon_n2 = t:add_node("/a/b/:name/e/f") 30 | 31 | local m1 = t:match("/a/b/c/d/g") 32 | assert.are.same(colon_n1, m1.node) 33 | 34 | local m2 = t:match("/a/b/other/e/f") 35 | assert.are.same(colon_n2, m2.node) 36 | end) 37 | 38 | it("confused prefix node", function() 39 | local n1 = t:add_node("/people/:id") 40 | local n2 = t:add_node("/people/list/:id") 41 | local n3 = t:add_node("/people/list") 42 | 43 | local m1 = t:match("/people/123") 44 | assert.are.same(n1, m1.node) 45 | 46 | local m2 = t:match("/people/list") 47 | assert.are.same(n3, m2.node) 48 | 49 | local m3 = t:match("/people/list/123") 50 | assert.are.same(n2, m3.node) 51 | 52 | local m4 = t:match("/people/list/123/456") 53 | assert.are.same(nil, m4.node) 54 | end) 55 | 56 | it("should succeed to match colon & common node.", function() 57 | local n1 = t:add_node("/user") 58 | local n2 = t:add_node("/user/123") 59 | local n3 = t:add_node("/user/:id/create") 60 | 61 | local m1 = t:match("/user/123/create") 62 | assert.are.same(n3, m1.node) -- should not match n2 63 | assert.is.equals("123", m1.params["id"]) 64 | end) 65 | 66 | it("a complicated example.", function() 67 | local n1 = t:add_node("/a/:p1/:p2/:p3/g") 68 | local n2 = t:add_node("/a/:p1/:p2/f/h") 69 | local n3 = t:add_node("/a/:p1/:p2/f") 70 | local n4 = t:add_node("/a/:p1/e") 71 | local n5 = t:add_node("/a/:p1/c/o") 72 | local n6 = t:add_node("/a/d/c") 73 | local n7 = t:add_node("/a/m") 74 | 75 | local m1 = t:match("/a/d/c/o") 76 | local m2 = t:match("/a/n/e/f") 77 | local m3 = t:match("/a/n/e/f/g") 78 | 79 | assert.are.same(n5, m1.node) 80 | assert.is.equals("d", m1.params["p1"]) 81 | 82 | assert.are.same(n3, m2.node) 83 | assert.is.equals("n", m2.params["p1"]) 84 | assert.is.equals("e", m2.params["p2"]) 85 | 86 | assert.are.same(n1, m3.node) 87 | assert.is.equals("n", m3.params["p1"]) 88 | assert.is.equals("e", m3.params["p2"]) 89 | assert.is.equals("f", m3.params["p3"]) 90 | 91 | end) 92 | end) 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /spec/trie/debug_cases.lua: -------------------------------------------------------------------------------- 1 | setup(function() 2 | _G.LOR_FRAMEWORK_DEBUG = false 3 | end) 4 | 5 | teardown(function() 6 | end) 7 | 8 | before_each(function() 9 | Trie = _G.Trie 10 | t = Trie:new() 11 | t1 = Trie:new() 12 | t2 = Trie:new() 13 | end) 14 | 15 | after_each(function() 16 | Trie = nil 17 | t = nil 18 | t1 = nil 19 | t2 = nil 20 | _debug = nil 21 | end) 22 | 23 | 24 | 25 | describe("for debug cases: ", function() 26 | it("a complicated example.", function() 27 | local n1 = t:add_node("/a/:p1/:p2/:p3/g") 28 | local n2 = t:add_node("/a/:p1/:p2/f/h") 29 | local n3 = t:add_node("/a/:p1/:p2/f") 30 | local n4 = t:add_node("/a/:p1/e") 31 | local n5 = t:add_node("/a/:p1/c/o") 32 | local n6 = t:add_node("/a/d/c") 33 | local n7 = t:add_node("/a/m") 34 | 35 | local m3 = t:match("/a/n/e/f/g") 36 | --json_view(t) 37 | --print(t:gen_graph()) 38 | 39 | assert.are.same(n1, m3.node) 40 | assert.is.equals("n", m3.params["p1"]) 41 | assert.is.equals("e", m3.params["p2"]) 42 | assert.is.equals("f", m3.params["p3"]) 43 | 44 | end) 45 | end) 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /spec/trie/define_node_spec.lua: -------------------------------------------------------------------------------- 1 | setup(function() 2 | _G.LOR_FRAMEWORK_DEBUG = true 3 | end) 4 | 5 | teardown(function() 6 | end) 7 | 8 | before_each(function() 9 | Trie = _G.Trie 10 | t = Trie:new() 11 | t1 = Trie:new() 12 | t2 = Trie:new() 13 | end) 14 | 15 | after_each(function() 16 | Trie = nil 17 | t = nil 18 | t1 = nil 19 | t2 = nil 20 | _debug = nil 21 | end) 22 | 23 | describe("objects check: ", function() 24 | it("objects or modules should not be nil.", function() 25 | assert.is.truthy(Trie) 26 | assert.is.truthy(Node) 27 | assert.is.truthy(t) 28 | assert.is.truthy(t1) 29 | assert.is.truthy(t2) 30 | end) 31 | end) 32 | 33 | describe("add node: ", function() 34 | it("wrong patterns with `/` to add.", function() 35 | assert.has_error(function() 36 | t1:add_node("//") 37 | end) 38 | assert.has_error(function() 39 | t1:add_node("///") 40 | end) 41 | assert.has_error(function() 42 | t1:add_node("/a/b//") 43 | end) 44 | assert.has_error(function() 45 | t1:add_node("//a/b/") 46 | end) 47 | assert.has_error(function() 48 | t1:add_node("/a//b/") 49 | end) 50 | assert.has_error(function() 51 | t1:add_node("/a///b/") 52 | end) 53 | end) 54 | 55 | it("wrong patterns with spaces to add.", function() 56 | assert.has_error(function() 57 | t1:add_node(" / / ") 58 | end) 59 | 60 | assert.has_error(function() 61 | t1:add_node("/ /") 62 | end) 63 | end) 64 | 65 | it("correct pattern to add.", function() 66 | assert.has_no_error(function() 67 | t1:add_node(" ") -- slim to "" 68 | t1:add_node("/") 69 | t1:add_node("") 70 | t1:add_node("/a") 71 | t1:add_node("/a/b") 72 | t1:add_node("/a/b/c/") 73 | end) 74 | 75 | assert.has_no.error(function() 76 | t1:add_node(" ") 77 | end) 78 | end) 79 | 80 | it("these patterns should get same node.", function() 81 | local node = t1:add_node("/") 82 | 83 | assert.is.equals(node, t1:add_node("/")) 84 | assert.is.equals(node, t1:add_node("")) 85 | assert.is.equals(t1:add_node("/"), t1:add_node("")) 86 | assert.is.equals(node.parent, t1.root) 87 | 88 | assert.is_not.equals(node, t2:add_node("/")) 89 | assert.is_not.equals(node, t2:add_node("")) 90 | end) 91 | 92 | it("spaces in patterns are trimed.", function() 93 | local node = t:add_node("/") 94 | assert.is.equals(node, t:add_node(" ")) 95 | assert.is.equals(node, t:add_node(" ")) 96 | assert.is.equals(node, t:add_node(" /")) 97 | assert.is.equals(node, t:add_node(" / ")) 98 | 99 | end) 100 | end) 101 | 102 | describe("illegal & legal path: ", function() 103 | it("wrong path", function() 104 | assert.has_error(function() 105 | t:add_node("/#+") 106 | end) 107 | assert.has_error(function() 108 | t:add_node(":+abc") 109 | end) 110 | assert.has_error(function() 111 | t:add_node(":&abc") 112 | end) 113 | 114 | assert.has_error(function() 115 | t:add_node(":abc*") 116 | end) 117 | 118 | end) 119 | it("correct path", function() 120 | assert.has_no_error(function() 121 | t:add_node(":abc") 122 | end) 123 | assert.has_no_error(function() 124 | t:add_node("abc") 125 | end) 126 | assert.has_no_error(function() 127 | t:add_node("/abc") 128 | end) 129 | end) 130 | end) 131 | 132 | describe("node's name: ", function() 133 | it("check names", function() 134 | local node = t1:add_node("/") 135 | assert.is.equals(node.name, "") 136 | end) 137 | end) 138 | 139 | describe("parent/children relation: ", function() 140 | it("use case 1.", function() 141 | local node = t1:add_node("/a/b") 142 | assert.is.equals(node.name, "") 143 | assert.is.equals(node.pattern, "/a/b") 144 | 145 | assert.is.equals(node, t1:add_node("/a/b")) 146 | assert.is_not.equals(node, t1:add_node("a/b/")) 147 | assert.is_not.equals(node, t1:add_node("/a/b/")) 148 | assert.is.equals(t1:add_node("/a/b/"), t1:add_node("a/b/")) 149 | 150 | parent = t1:add_node("/a") 151 | assert.is.equals(node.parent, parent) 152 | assert.is_not.equals(parent.varyChild, node) 153 | assert.is.equals(parent:find_child("b"), node) 154 | child = t1:add_node("/a/b/c") 155 | assert.is.equals(child.parent, node) 156 | assert.is.equals(node:find_child("c"), child) 157 | 158 | assert.has_error(function() 159 | t1:add_node("/a//b") 160 | end) 161 | end) 162 | 163 | it("use case 2.", function() 164 | local root = t.root 165 | 166 | local slash_level = t:add_node("/") 167 | local level0 = t:add_node("/00") 168 | local level1 = t:add_node("/01") 169 | local level2 = t:add_node("/02") 170 | 171 | local level0_0 = t:add_node("/00/0") 172 | local level0_1 = t:add_node("/00/1") 173 | local level0_2 = t:add_node("/00/2") 174 | 175 | local level1_0 = t:add_node("/01/0") 176 | local level1_1 = t:add_node("/01/1") 177 | local level1_2 = t:add_node("/01/2") 178 | 179 | local level2_0 = t:add_node("/02/0") 180 | local level2_1 = t:add_node("/02/1") 181 | local level2_2 = t:add_node("/02/2") 182 | 183 | local level0_0_0 = t:add_node("/00/0/0") 184 | local level0_0_1 = t:add_node("/00/0/1") 185 | local level0_0_2 = t:add_node("/00/0/2") 186 | 187 | assert.is.equals(root.name, "") 188 | assert.is.equals(root.pattern, "") 189 | 190 | assert.is.equals(slash_level.name, "") 191 | assert.is.equals(slash_level.pattern, "/") 192 | assert.is.equals(level0.name, "") 193 | assert.is.equals(level0.pattern, "/00") 194 | 195 | assert.are.same(slash_level.parent, level1.parent) 196 | assert.are.same(level0.parent, level1.parent) 197 | assert.are.same(level1.parent, level2.parent) 198 | 199 | assert.is.equals(root, level0.parent) 200 | assert.is.equals(root, level1.parent) 201 | assert.is.equals(root, level2.parent) 202 | 203 | assert.is.equals(level0, level0_0.parent) 204 | assert.is.equals(level0_0.parent, level0_1.parent) 205 | assert.is.equals(level1, level1_0.parent) 206 | assert.is.equals(level1_0.parent, level1_1.parent) 207 | 208 | assert.is.equals(root, level0_0_0.parent.parent.parent) 209 | assert.is.equals(level0, level0_0_0.parent.parent) 210 | end) 211 | end) 212 | 213 | describe("colon child define: ", function() 214 | it("should failed to define conflict node.", function() 215 | local root = t.root 216 | 217 | local slash_level = t:add_node("/") 218 | local level0 = t:add_node("/00") 219 | 220 | t:add_node("/00/0") 221 | t:add_node("/00/0/:0") 222 | 223 | assert.has_error(function() 224 | t:add_node("/00/0/:1") 225 | end) 226 | 227 | assert.has_error(function() 228 | t:add_node("/00/0/:01") 229 | end) 230 | 231 | assert.has_no_error(function() 232 | t:add_node("/00/0/absolute") 233 | end) 234 | end) 235 | 236 | it("should succeed to define not conflict nodes.", function() 237 | local root = t.root 238 | 239 | local slash_level = t:add_node("/") 240 | local level0 = t:add_node("/00") 241 | 242 | t:add_node("/00/0") 243 | t:add_node("/00/0/:0") 244 | 245 | assert.has_no_error(function() 246 | t:add_node("/00/0/absolute") 247 | end) 248 | 249 | assert.has_no_error(function() 250 | t:add_node("/00/0/123") 251 | end) 252 | 253 | assert.has_no_error(function() 254 | t:add_node("/00/0/123/456") 255 | end) 256 | end) 257 | end) 258 | 259 | describe("shadow child define: ", function() 260 | it("should failed to define conflict node.", function() 261 | local level0_0_0 = t:add_node("/00/0/:0") 262 | local level0_0_0_shadow = t:add_node("/00/0/:0") 263 | local level0_0_0_shadow2 = t:add_node("/00/0/:0") 264 | assert.is.equals(level0_0_0, level0_0_0_shadow) 265 | assert.is.equals(level0_0_0_shadow, level0_0_0_shadow2) 266 | end) 267 | end) 268 | 269 | describe("regex node define: ", function() 270 | it("should failed to define error regex node.", function() 271 | assert.has_error(function() 272 | t:add_node("/a/:(^abc)") 273 | end) 274 | assert.has_error(function() 275 | t:add_node("/a/(^abc)") 276 | end) 277 | assert.has_error(function() 278 | t:add_node("/a/:abc(^abc") 279 | end) 280 | assert.has_error(function() 281 | t:add_node("/a/:abc^abc)") 282 | end) 283 | end) 284 | it("should succeed to define regex node.", function() 285 | assert.has_no_error(function() 286 | t:add_node("/a/:b(^abc)") 287 | end) 288 | assert.has_no_error(function() 289 | t:add_node("/a/b/:c(^abc)") 290 | end) 291 | 292 | 293 | local n1 = t:add_node("/a/:b(^abc)") 294 | assert.is.equals("^abc", n1.regex) 295 | end) 296 | end) 297 | 298 | 299 | describe("just for dev, print json/tree : ", function() 300 | it("case 1.", function() 301 | local root = t.root 302 | 303 | local slash_level = t:add_node("/") 304 | local level0 = t:add_node("/00") 305 | local level0_0 = t:add_node("/00/0") 306 | 307 | local level0_0_1 = t:add_node("/00/0/1") 308 | 309 | local level0_0_0 = t:add_node("/00/0/:0") 310 | local level0_0_0_shadow = t:add_node("/00/0/:0") 311 | assert.is.equals(level0_0_0, level0_0_0_shadow) 312 | 313 | end) 314 | end) 315 | 316 | -------------------------------------------------------------------------------- /spec/trie/find_node_spec.lua: -------------------------------------------------------------------------------- 1 | setup(function() 2 | _G.LOR_FRAMEWORK_DEBUG = false 3 | end) 4 | 5 | teardown(function() 6 | end) 7 | 8 | before_each(function() 9 | Trie = _G.Trie 10 | t = Trie:new() 11 | t1 = Trie:new() 12 | t2 = Trie:new() 13 | end) 14 | 15 | after_each(function() 16 | Trie = nil 17 | t = nil 18 | t1 = nil 19 | t2 = nil 20 | _debug = nil 21 | end) 22 | 23 | describe("path match: ", function() 24 | it("should succeed to match colon node.", function() 25 | local n1 = t:add_node("/a/:name") 26 | 27 | local m1 = t:match("/a/b") 28 | assert.are.same(n1, m1.node) 29 | local m2 = t:match("/a/demo") 30 | assert.are.same(n1, m2.node) 31 | assert.is.equals("demo", m2.params.name) 32 | 33 | t.strict_route = false 34 | local m3 = t:match("/a/mock/") 35 | assert.are.same(n1, m3.node) 36 | assert.is.equals("mock", m3.params["name"]) 37 | 38 | t.strict_route = true 39 | m3 = t:match("/a/mock/") 40 | assert.are.same(nil, m3.node) 41 | end) 42 | end) 43 | 44 | describe("path matched pipeline: ", function() 45 | it("should get correct pipeline.", function() 46 | local n0 = t:add_node("/a") 47 | local n1 = t:add_node("/a/b") 48 | local n2 = t:add_node("/a/b/c") 49 | 50 | local m1 = t:match("/a/b/c") 51 | assert.are.same(n2, m1.node) 52 | 53 | local p1 = m1.pipeline 54 | assert.is.equals(4, #m1.pipeline) 55 | assert.is.equals(t.root.id, p1[1].id) 56 | assert.is.equals(n0.id, p1[2].id) 57 | assert.is.equals(n1.id, p1[3].id) 58 | assert.is.equals(n2.id, p1[4].id) 59 | end) 60 | 61 | it("slash node not included in pipeline.", function() 62 | local slash_node = t:add_node("/") 63 | local n0 = t:add_node("/a") 64 | local n1 = t:add_node("/a/b") 65 | local n2 = t:add_node("/a/b/c") 66 | 67 | local m1 = t:match("/a/b/c") -- won't match slash_node 68 | assert.are.same(n2, m1.node) 69 | 70 | local p1 = m1.pipeline 71 | assert.is_not.equals(5, #m1.pipeline) 72 | assert.is.equals(4, #m1.pipeline) 73 | for i, v in ipairs(p1) do 74 | assert.is_not.equals(slash_node.id, v.id) 75 | end 76 | end) 77 | 78 | it("pipeline contains the right parent node.", function() 79 | local n1 = t:add_node("/a/b/c") 80 | 81 | local m1 = t:match("/a/b/c") 82 | assert.are.same(n1, m1.node) 83 | 84 | local p1 = m1.pipeline 85 | assert.is.equals(4, #m1.pipeline) 86 | assert.is.equals(t.root.id, p1[1].id) 87 | assert.is.equals(n1.parent.parent.id, p1[2].id) 88 | assert.is.equals(n1.parent.id, p1[3].id) 89 | assert.is.equals(n1.id, p1[4].id) 90 | end) 91 | 92 | it("children pipelines should contain same parents node.", function() 93 | local n1 = t:add_node("/a/b/c") 94 | local colon_n1 = t:add_node("/a/b/:name") 95 | 96 | local m1 = t:match("/a/b/c") 97 | assert.are.same(n1, m1.node) 98 | 99 | local m2 = t:match("/a/b/sumory") 100 | assert.are.same(colon_n1, m2.node) 101 | 102 | local p1 = m1.pipeline 103 | assert.is.equals(4, #m1.pipeline) 104 | assert.is.equals(t.root.id, p1[1].id) 105 | assert.is.equals(n1.parent.parent.id, p1[2].id) 106 | assert.is.equals(n1.parent.id, p1[3].id) 107 | assert.is.equals(n1.id, p1[4].id) 108 | 109 | local p2 = m2.pipeline 110 | assert.is.equals(4, #m2.pipeline) 111 | assert.is.equals(t.root.id, p2[1].id) 112 | assert.is.equals(colon_n1.parent.parent.id, p2[2].id) 113 | assert.is.equals(colon_n1.parent.id, p2[3].id) 114 | assert.is.equals(colon_n1.id, p2[4].id) 115 | 116 | assert.is.equals(p1[1].id, p2[1].id) 117 | assert.is.equals(p1[2].id, p2[2].id) 118 | assert.is.equals(p1[3].id, p2[3].id) 119 | assert.is_not.equals(p1[4].id, p2[4].id) 120 | end) 121 | end) 122 | 123 | 124 | describe("use cases that are hard to understand: ", function() 125 | it("absolute & colon node math.", function() 126 | local n1 = t:add_node("/a/b/c") 127 | local colon_n1 = t:add_node("/a/b/:name") 128 | 129 | local m1 = t:match("/a/b/c") 130 | assert.are.same(n1, m1.node) 131 | 132 | local m2 = t:match("/a/b/sumory") 133 | assert.are.same(colon_n1, m2.node) 134 | 135 | end) 136 | 137 | it("confused prefix node", function() 138 | local n1 = t:add_node("/people/:id") 139 | local n2 = t:add_node("/people/list/:id") 140 | 141 | local m1 = t:match("/people/1") 142 | assert.are.same(n1, m1.node) 143 | local m11 = t:match("/people/abc") 144 | assert.are.same(n1, m11.node) 145 | 146 | local m2 = t:match("/people/abc/123") 147 | assert.are.same(nil, m2.node) 148 | 149 | local m3 = t:match("/people/list/123") 150 | assert.are.same(n2, m3.node) 151 | end) 152 | 153 | it("children pipelines should contain same parents node.", function() 154 | local n1 = t:add_node("/a/b/c") 155 | local colon_n1 = t:add_node("/a/b/:name") 156 | 157 | local m1 = t:match("/a/b/c") 158 | assert.are.same(n1, m1.node) 159 | end) 160 | end) 161 | 162 | 163 | -------------------------------------------------------------------------------- /spec/trie/handle_spec.lua: -------------------------------------------------------------------------------- 1 | setup(function() 2 | _G.LOR_FRAMEWORK_DEBUG = false 3 | end) 4 | 5 | teardown(function() 6 | end) 7 | 8 | before_each(function() 9 | Trie = _G.Trie 10 | t = Trie:new() 11 | t1 = Trie:new() 12 | t2 = Trie:new() 13 | end) 14 | 15 | after_each(function() 16 | Trie = nil 17 | t = nil 18 | t1 = nil 19 | t2 = nil 20 | _debug = nil 21 | end) 22 | 23 | describe("`handler` test cases: ", function() 24 | it("should succeed to define handlers.", function() 25 | local n1 = t:add_node("/a") 26 | local m1 = t:add_node("/a/b") 27 | local m2 = t:add_node("/a/c") 28 | local m3 = t:add_node("/a/:name") 29 | 30 | n1:handle("get", function(req, res, next) end) 31 | assert.is.equals(1, #n1.handlers["get"]) 32 | 33 | n1:handle("post", function(req, res, next) end) 34 | assert.is.equals(1, #n1.handlers["post"]) 35 | 36 | n1:handle("put", function(req, res, next) end, function(req, res, next) end, function(req, res, next) end) 37 | assert.is.equals(3, #n1.handlers["put"]) 38 | 39 | n1:handle("DELETE", {function(req, res, next) end, function(req, res, next) end}) 40 | assert.is.equals(2, #n1.handlers["delete"]) 41 | 42 | m2:handle("get", function(req, res, next) end) 43 | assert.is.equals(1, #m2.handlers["get"]) 44 | end) 45 | 46 | it("should failed to define handlers.", function() 47 | local n1 = t:add_node("/a") 48 | 49 | assert.has_error(function() 50 | n1:handle("getabc", function(req, res, next) end) -- wrong `method` name 51 | end) 52 | 53 | assert.has_error(function() 54 | n1:handle("get", {}) 55 | end) 56 | 57 | assert.has_error(function() 58 | n1:handle("get", function(req, res, next) end) 59 | n1:handle("get", function(req, res, next) end) -- define handler repeatly 60 | end) 61 | 62 | -- _G.LOR_FRAMEWORK_DEBUG = true 63 | --_debug(n1) 64 | --json_view(t) 65 | --print(t:gen_graph()) 66 | end) 67 | end) 68 | 69 | -------------------------------------------------------------------------------- /spec/trie/strict_route_spec.lua: -------------------------------------------------------------------------------- 1 | setup(function() 2 | _G.LOR_FRAMEWORK_DEBUG = false 3 | end) 4 | 5 | teardown(function() 6 | end) 7 | 8 | before_each(function() 9 | Trie = _G.Trie 10 | t = Trie:new() 11 | t1 = Trie:new() 12 | t2 = Trie:new() 13 | end) 14 | 15 | after_each(function() 16 | Trie = nil 17 | t = nil 18 | t1 = nil 19 | t2 = nil 20 | _debug = nil 21 | end) 22 | 23 | describe("strict route: ", function() 24 | it("should match if strict route is false.", function() 25 | local t = Trie:new({ 26 | strict_route = false -- default value is true 27 | }) 28 | local n1 = t:add_node("/a/b") 29 | 30 | local m1 = t:match("/a/b/") 31 | assert.are.same(n1, m1.node) 32 | 33 | local m2 = t:match("/a/b") 34 | assert.are.same(n1, m2.node) 35 | end) 36 | 37 | it("should not match if strict route is true.", function() 38 | local t = Trie:new({ 39 | strict_route = true -- default value is true 40 | }) 41 | local n1 = t:add_node("/a/b") 42 | 43 | local m1 = t:match("/a/b") 44 | assert.are.same(n1, m1.node) 45 | 46 | local m2 = t:match("/a/b/") 47 | assert.is.falsy(m2.node) 48 | end) 49 | 50 | it("should match if strict route is false and the exact route is not given.", function() 51 | local t = Trie:new({ 52 | strict_route = false -- default value is true 53 | }) 54 | local n1 = t:add_node("/a/b") 55 | 56 | local m1 = t:match("/a/b/") 57 | assert.are.same(n1, m1.node) 58 | end) 59 | 60 | it("should not match if strict route is true and the exact route is not given.", function() 61 | local t = Trie:new({ 62 | strict_route = true -- default value is true 63 | }) 64 | local n1 = t:add_node("/a/b") 65 | 66 | local m1 = t:match("/a/b/") 67 | assert.is.falsy(m1.node) 68 | assert.is.equals(nil, m1.node) 69 | end) 70 | end) 71 | 72 | 73 | 74 | --------------------------------------------------------------------------------