├── .gitignore ├── README.markdown ├── dist.ini ├── examples └── hello │ └── conf │ └── nginx.conf ├── lib └── resty │ ├── stack.lua │ └── stack │ ├── db.lua │ ├── string.lua │ └── validation.lua └── t ├── authorize.t ├── method.t ├── module.t ├── request.t ├── router.t └── sanity.t /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | go 5 | t/servroot/ 6 | reindex 7 | *.t_ 8 | tags 9 | .hg 10 | .hgignore 11 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | lua-resty-stack 2 | =============== 3 | 4 | Openresty Simple Application Stack 5 | 6 | Table of Contents 7 | ================= 8 | * [Status](#status) 9 | * [Description](#description) 10 | * [TODO](#todo) 11 | * [Installation](#installation) 12 | * [How to use](#how-to-use) 13 | * [Methods](#methods) 14 | * [Copyright and License](#copyright-and-license) 15 | * [See Also](#see-also) 16 | 17 | Status 18 | ======== 19 | 20 | Beta Quality and used in production 21 | 22 | 23 | Description 24 | =========== 25 | 26 | REST based Application Stack 27 | 28 | 29 | TODO 30 | ==== 31 | * more test 32 | * user defined routing module/:id/action 33 | 34 | Installation 35 | ============ 36 | 37 | * download or clone this repo 38 | * copy lib/resty/stack.lua to (Openresty Path)/lualib/resty/ or to (Application path)/resty 39 | 40 | [Back to TOC](#table-of-contents) 41 | 42 | How to use 43 | ========== 44 | 45 | Recommended Application folder structure 46 | * conf 47 | * nginx.conf 48 | * resty 49 | * stack.lua 50 | * [post.lua](#see-also) -- optional for advance post process like formdata, json and file upload 51 | * [template.lua](#see-also) -- optional for templating for override render 52 | * api 53 | * config.lua 54 | * app.lua 55 | * hello.lua 56 | 57 | ```nginx.conf 58 | daemon off; 59 | master process off; 60 | error_log log/error.log warn; 61 | event {} 62 | http { 63 | client_body_temp_path logs; 64 | fastcgi_temp_path logs; 65 | proxy_temp_path logs; 66 | scgi_temp_path logs; 67 | uwsgi_temp_path logs; 68 | init_by_lua_file "api/app.lua"; 69 | lua_code_cache off; 70 | server { 71 | listen 127.0.0.1:8080; 72 | location /api { 73 | content_by_lua_block { 74 | local app = require 'api.app' 75 | app:run() 76 | } 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | config.lua 83 | ```lua 84 | return { 85 | debug = true, 86 | redis = { host = '127.0.0.1', port = 6379 }, 87 | } 88 | ``` 89 | 90 | app.lua 91 | ```lua 92 | local stack = require 'resty.stack' 93 | local config = require 'api.config' 94 | local app = stack:new(config) 95 | app:service ({ api = { 96 | 'hello' 97 | }}) 98 | return app 99 | ``` 100 | 101 | hello.lua 102 | ```lua 103 | local _M = {} 104 | 105 | function _M.get(self) 106 | return "get Hello" 107 | end 108 | 109 | function _M.post(self) 110 | return "post Hello" 111 | end 112 | 113 | function _M.delete(self) 114 | return "delete Hello" 115 | end 116 | 117 | return _M 118 | ``` 119 | 120 | ```sh 121 | $ nginx -p . 122 | ``` 123 | 124 | [Back to TOC](#table-of-contents) 125 | 126 | 127 | Methods 128 | ======= 129 | 130 | [Back to TOC](#table-of-contents) 131 | 132 | new 133 | --- 134 | 135 | `syntax: app = stack:new(config)` 136 | 137 | Initate new stack apps with config parameter 138 | 139 | use 140 | --- 141 | 142 | `syntax: app:use(path, fn)` 143 | 144 | register function or module 145 | 146 | * `path` 147 | 148 | the route url path matching with function. 149 | if path is function the path is current location 150 | 151 | * `fn` 152 | 153 | function to execute when path is accessed 154 | 155 | service 156 | ------ 157 | 158 | `syntax: app:service(services)` 159 | 160 | register servicese using lua table 161 | 162 | * `services` 163 | 164 | table of services to load 165 | 166 | run 167 | --- 168 | 169 | `syntax: app:run()` 170 | 171 | running the application 172 | 173 | authorize 174 | --------- 175 | 176 | `syntax: function app.authorize(self) end` 177 | 178 | implement authorize function for secure module 179 | 180 | 181 | render 182 | ------ 183 | 184 | `syntax: function app.render(self) end` 185 | 186 | implement plugable render to override default render 187 | 188 | 189 | begin\_request 190 | ------------- 191 | 192 | `syntax: function app.begin_request(self) end` 193 | 194 | implement begin request hook 195 | 196 | 197 | end\_request 198 | ------------ 199 | 200 | `syntax: function app.end_request(self) end` 201 | 202 | implement end request hook 203 | 204 | 205 | [Back to TOC](#table-of-contents) 206 | 207 | Copyright and License 208 | ===================== 209 | 210 | This module is licensed under the BSD license. 211 | 212 | Copyright (C) 2014 - 2015, by Anton Heryanto . 213 | 214 | All rights reserved. 215 | 216 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 217 | 218 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 219 | 220 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 221 | 222 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 223 | 224 | See Also 225 | ======= 226 | 227 | * [lua-resty-post](https://github.com/antonheryanto/lua-resty-post) 228 | * [lua-resty-smtp](https://github.com/antonheryanto/lua-resty-smtp) 229 | * [lua-resty-pdf](https://github.com/antonheryanto/lua-resty-pdf) 230 | * [lua-resty-search](https://github.com/antonheryanto/lua-resty-search) 231 | * [lua-resty-upload](https://github.com/openresty/lua-resty-upload) 232 | * [lua-nginx-module](https://github.com/openresty/lua-nginx-module) 233 | 234 | [Back to TOC](#table-of-contents) 235 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-stack 2 | abstract = Openresty Simple Application Stack 3 | author = Anton Heryanto 4 | is_original = yes 5 | license = 2bsd 6 | lib_dir = lib 7 | doc_dir = lib 8 | repo_link = https://github.com/antonheryanto/lua-resty-stack 9 | main_module = lib/resty/stack.lua 10 | -------------------------------------------------------------------------------- /examples/hello/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | master_process off; 3 | worker_processes 1; 4 | error_log stderr info; 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | client_body_temp_path logs; 11 | fastcgi_temp_path logs; 12 | proxy_temp_path logs; 13 | scgi_temp_path logs; 14 | uwsgi_temp_path logs; 15 | access_log off; 16 | lua_package_path "${prefix}../../lib/?.lua;;"; 17 | 18 | server { 19 | listen 127.0.0.1:8080; 20 | lua_code_cache off; 21 | 22 | location / { 23 | content_by_lua " 24 | local stack = require 'resty.stack' 25 | local app = stack:new() 26 | app:use(function(self) 27 | return 'Hello from Openresty Stack Framework' 28 | end) 29 | app:run() 30 | "; 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /lib/resty/stack.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2014 - 2015 Anton heryanto. 2 | 3 | local pcall = pcall 4 | local setmetatable = setmetatable 5 | local pairs = pairs 6 | local type = type 7 | local tonumber = tonumber 8 | local tostring = tostring 9 | local sub = string.sub 10 | local lower = string.lower 11 | local byte = string.byte 12 | local new_tab = require 'table.new' 13 | local cjson = require 'cjson' 14 | local has_resty_post, resty_post = pcall(require, 'resty.post') 15 | local ngx = ngx 16 | local var = ngx.var 17 | local req = ngx.req 18 | local print = ngx.print 19 | local log = ngx.log 20 | local exit = ngx.exit 21 | local read_body = ngx.req.read_body 22 | local get_post_args = ngx.req.get_post_args 23 | local get_uri_args = ngx.req.get_uri_args 24 | local re_find = ngx.re.find 25 | local WARN = ngx.WARN 26 | local HTTP_OK = ngx.HTTP_OK 27 | local HTTP_NOT_FOUND = ngx.HTTP_NOT_FOUND 28 | local HTTP_UNAUTHORIZED = ngx.HTTP_UNAUTHORIZED 29 | 30 | local _M = new_tab(0, 9) 31 | local mt = { __index = _M } 32 | _M._VERSION = '0.3.1' 33 | 34 | function _M.new(self, config) 35 | config = config or {} 36 | config.base_length = config.base and #config.base + 2 or 2 37 | local post = has_resty_post and resty_post:new(config.upload_path) 38 | config.upload_path = config.upload_path or post and post.path 39 | return setmetatable({ 40 | post = post, 41 | config = config, 42 | services = {} 43 | }, mt) 44 | end 45 | 46 | -- register service 47 | function _M.service(self, services) 48 | if not services or type(services) ~= 'table' then 49 | return 50 | end 51 | 52 | -- service 'string', 'function', 'table' 53 | -- sub service { 'string', 'function', 'table' } 54 | for k,v in pairs(services) do 55 | if type(v) == 'table' then 56 | for sk,sv in pairs(v) do 57 | local tsk = type(sk) 58 | if tsk == 'number' then 59 | sk = sv 60 | end 61 | 62 | local spath = tsk == 'string' and k..'/'..sk or k 63 | local sfn = sv 64 | if type(sv) == 'string' then 65 | spath = k ..'/'.. sk 66 | sfn = k ..'.'.. sk 67 | end 68 | 69 | _M.use(self, spath, sfn) 70 | end 71 | else 72 | if type(k) == 'number' then 73 | k = v 74 | end 75 | 76 | _M.use(self, k, v) 77 | end 78 | end 79 | end 80 | 81 | -- provides routes table 82 | -- validate authorization support at service and method level 83 | -- FIXME: only used for init state as pairs is NYI 84 | -- TODO: predefines method and regex options 85 | local function router(self, path, service) 86 | local auth = service.AUTHORIZE 87 | for m,o in pairs(service) do 88 | local mt = type(o) 89 | local mp = path..'/'..m 90 | local authorize = auth == true or auth and auth[m] 91 | if mt == 'function' then 92 | self.services[mp] = { service = o, authorize = authorize } 93 | elseif mt == 'table' then -- recursive add routes 94 | router(self, mp, o) 95 | end 96 | end 97 | end 98 | 99 | -- FIXME provides single param instead of multiple for simplicity 100 | -- FIXME string, function, or table for complete 101 | function _M.use(self, path, fn, authorize) 102 | if not path then return end 103 | 104 | -- validate path 105 | local config = self.config 106 | local services = self.services 107 | local tp = type(path) 108 | if tp ~= 'string' then 109 | authorize = fn 110 | fn = path 111 | path = var.uri 112 | end 113 | 114 | if byte(path, 1) == 47 then -- char '/' 115 | path = sub(path, config.base_length) 116 | end 117 | 118 | if not fn then 119 | fn = path 120 | end 121 | 122 | local tf = type(fn) 123 | if tf == 'function' then 124 | services[path] = { service = fn, authorize = authorize } 125 | elseif tf == 'table' then 126 | router(self, path, fn) 127 | elseif tf == 'string' then 128 | router(self, path, require(fn)) 129 | end 130 | end 131 | 132 | -- default header and body render 133 | -- default handling json only 134 | -- FIXME handle return text, html, binary 135 | function _M.render(self, body) 136 | local header = ngx.header 137 | if not body then 138 | header['Content-Length'] = 0 139 | return 140 | end 141 | 142 | -- json when service return table 143 | local typ = type(body) 144 | if typ == 'table' then 145 | header['Content-Type'] = 'application/json' 146 | body = cjson.encode(body) 147 | end 148 | 149 | if typ ~= 'string' then 150 | body = tostring(body) 151 | end 152 | 153 | -- print string body, type define by service 154 | header['Content-Length'] = #body 155 | print(body) 156 | end 157 | 158 | -- FIXME simplify best way to modify header 159 | function _M.set_header(self) 160 | local header = ngx.header 161 | header['Access-Control-Allow-Origin'] = '*' 162 | header['Access-Control-Max-Age'] = 2520 163 | header['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,HEAD,OPTIONS' 164 | end 165 | 166 | 167 | -- main method run app request 168 | -- FIXME: uses return instead of status 169 | -- TODO: plugable render for content 170 | function _M.run(self) 171 | local status, body = self:load() 172 | if status then 173 | ngx.status = status 174 | if not body then 175 | return exit(status) 176 | end 177 | end 178 | self:render(body) 179 | return exit(status or HTTP_OK) 180 | end 181 | 182 | function _M.load(self, uri) 183 | local services = self.services 184 | if not services then 185 | return HTTP_NOT_FOUND 186 | end 187 | 188 | if not uri then 189 | uri = var.uri 190 | end 191 | 192 | local config = self.config 193 | local slash = byte(uri, #uri) == 47 and #uri - 1 or nil -- char '/' 194 | local path = sub(uri, config.base_length, slash) 195 | local arg = get_uri_args() 196 | local method = lower(arg.method or var.request_method) 197 | if (method == 'head' or method == 'options') and services[path..'/get'] then 198 | return HTTP_OK 199 | end 200 | 201 | -- check path or path/method 202 | local route = services[path] or services[path..'/'..method] 203 | 204 | -- check args number service/:id/action 205 | if not route then 206 | local from, to, err = re_find(path, '([0-9]+)', 'jo') 207 | if from then 208 | local service = sub(path, 1, from - 2) 209 | local action = sub(path, to + 2) 210 | if action == '' then 211 | action = method 212 | end 213 | 214 | route = services[service..'/'..action] 215 | arg.id = sub(path, from, to) 216 | end 217 | end 218 | 219 | if not route then 220 | return HTTP_NOT_FOUND 221 | end 222 | 223 | if config.debug then 224 | log(WARN, 'path: ', path, ' method: ', method, ' id: ', arg.id, 225 | ' authorize ', type(route.authorize)) 226 | end 227 | 228 | -- setup service and params 229 | local service = route.service 230 | local params = { 231 | authorize = route.authorize, 232 | config = self.config, 233 | arg = arg 234 | } 235 | 236 | -- execute begin request hook 237 | if self.begin_request then 238 | self.begin_request(params) 239 | end 240 | 241 | -- validate authorization 242 | local authorize = self.authorize 243 | if authorize and route.authorize and not authorize(params) then 244 | -- execute end request hook 245 | if self.end_request then 246 | self.end_request(params) 247 | end 248 | 249 | return HTTP_UNAUTHORIZED 250 | end 251 | 252 | -- process post/put data 253 | local post = self.post 254 | if method == 'post' or method == 'put' then 255 | if post then 256 | params.data = post:read() 257 | else 258 | read_body() 259 | params.data = get_post_args() 260 | end 261 | end 262 | 263 | local body, status = service(params) 264 | 265 | -- execute end request hook 266 | if self.end_request then 267 | self.end_request(params) 268 | end 269 | 270 | return status, body 271 | end 272 | 273 | return _M 274 | 275 | -------------------------------------------------------------------------------- /lib/resty/stack/db.lua: -------------------------------------------------------------------------------- 1 | require "resty.stack.string" 2 | local new_tab = require "table.new" 3 | local redis = require "resty.redis" 4 | local mysql = require "resty.mysql" 5 | local concat = table.concat 6 | local trim = string.trim 7 | local unpack = unpack 8 | local type = type 9 | local null = ngx.null 10 | local log = ngx.log 11 | local ERR = ngx.ERR 12 | local WARN = ngx.WARN 13 | 14 | local _M = new_tab(0,3) 15 | 16 | function _M.mysql(conf) 17 | conf = conf or {} 18 | conf.host = conf.host or '127.0.0.1' 19 | conf.port = conf.port or 3306 20 | conf.database = conf.database or 'test' 21 | conf.user = conf.user or 'root' 22 | conf.password = conf.password or '' 23 | 24 | local db = mysql:new() 25 | db:set_timeout(conf.timeout or 1000) 26 | local ok,err = db:connect(conf) 27 | if not ok then 28 | log(ERR, 'failed connect to mysql with message: ', err) 29 | return 30 | end 31 | 32 | return db, conf 33 | end 34 | 35 | function _M.redis(conf) 36 | conf = conf or {} 37 | local r = redis:new() 38 | r:set_timeout(conf.timeout or 1000) 39 | local ok,err = conf.socket and r:connect(conf.socket) 40 | or r:connect(conf.host or '127.0.0.1', conf.port or 6379) 41 | 42 | if not ok then 43 | log(ERR, "failed connect to redis with message : ", err) 44 | return 45 | end 46 | 47 | -- add method to redis 48 | function r.key(r, ...) 49 | return concat({...},':') 50 | end 51 | 52 | function r.hash_get(r, key, ...) 53 | local args = {...} 54 | local fields = (#args == 1 and type(args[1]) == 'table') and args[1] 55 | or args 56 | local n = #fields 57 | local m = new_tab(0, n) 58 | r:init_pipeline(n) 59 | for i = 1, n do 60 | r:hget(key, fields[i]) 61 | end 62 | 63 | local data = r:commit_pipeline() 64 | for i = 1, n do 65 | local v = data[i] 66 | if v ~= null then m[fields[i]] = v end 67 | end 68 | 69 | return m 70 | end 71 | 72 | function r.hash_save(r, key, data, fields, n) 73 | n = n or #fields 74 | for i=1,n do 75 | local k = fields[i] 76 | local v = trim(data[k]) 77 | local t = type(v) 78 | if v then 79 | if v == '' then 80 | r:hdel(key, k) 81 | elseif v ~= null and t ~= 'table' and t ~= 'function' then 82 | r:hset(key, k, v) 83 | end 84 | end 85 | end 86 | end 87 | 88 | return r, conf 89 | end 90 | 91 | function _M.init(conf, fn) 92 | conf = conf or {} 93 | conf.host = conf.host or "127.0.0.1" 94 | conf.timeout = conf.timeout or 1000 95 | conf.keep_size = conf.keep_size or 1024 96 | conf.keep_idle = conf.keep_idle or 0 97 | fn = fn or (conf.type and _M[conf.type]) 98 | if fn then return fn(conf) end 99 | end 100 | 101 | function _M.keep(db, conf) 102 | if not db or not db.set_keepalive then return end 103 | conf = conf or {} 104 | 105 | if conf.debug then 106 | local times, ex = db:get_reused_times() 107 | log(WARN, "reused: ", times, " error: ", ex) 108 | end 109 | 110 | local ok,err = db:set_keepalive(conf.keep_idle or 1000, 111 | conf.keep_size or 1024) 112 | if not ok then 113 | log(ERR, "failed to keepalive with message: ", err) 114 | end 115 | end 116 | 117 | return _M 118 | -------------------------------------------------------------------------------- /lib/resty/stack/string.lua: -------------------------------------------------------------------------------- 1 | local new_tab = require "table.new" 2 | local string = string 3 | local find = string.find 4 | local sub = string.sub 5 | local gsub = string.gsub 6 | local lower = string.lower 7 | local upper = string.upper 8 | local match = string.match 9 | 10 | local smallwords = { 11 | a=1, ["and"]=1, as=1, at=1, but=1, by=1, en=1, ["for"]=1, ["if"]=1, 12 | ["in"]=1, of=1, the=1, to=1, vs=1, ["vs."]=1, v=1, ["v."]=1, via=1 13 | } 14 | 15 | function string.titlecase(self) 16 | local title = lower(self) 17 | return (gsub(title, "()([%w&`'''\".@:/{%(%[<>_]+)(-? *)()", 18 | function (index, nonspace, space, endpos) 19 | local low = lower(nonspace) 20 | if (index > 1) and (sub(title, index - 2,index - 2) ~= ':') 21 | and (endpos < #title) and smallwords[low] then 22 | return low .. space 23 | elseif match(sub(title, index - 1, index + 1), "['\"_{%(%[]/)") then 24 | return sub(nonspace, 1, 1) .. upper(sub(nonspace, 2, 2)) 25 | .. sub(nonspace, 3) .. space 26 | elseif match(sub(nonspace, 2),"[A-Z&]") 27 | or match(sub(nonspace, 2),"%w[%._]%w") then 28 | return nonspace .. space 29 | end 30 | return upper(sub(nonspace, 1, 1)) .. sub(nonspace, 2) .. space 31 | end)) 32 | end 33 | 34 | 35 | function string.trim(self) 36 | if not self or type(self) ~= "string" then return self end 37 | return (gsub(self, "^%s*(.-)%s*$", "%1")) 38 | end 39 | 40 | function string.split(self, delimiter, limit) 41 | if not self or type(self) ~= "string" then return end 42 | local length = #self 43 | 44 | if self ~= delimiter and length == 1 then return {self} end 45 | 46 | local result = limit and new_tab(limit, 0) or {} 47 | local index = 0 48 | local n = 1 49 | 50 | while true do 51 | if limit and n > limit then break end 52 | 53 | -- find the next d in the string 54 | local pos = find(self,delimiter,index,true) 55 | if pos ~= nil then -- if "not not" found then.. 56 | result[n] = sub(self,index, pos - 1) -- Save it in our array. 57 | -- save just after where we found it for searching next time. 58 | index = pos + 1 59 | else 60 | result[n] = sub(self,index) -- Save what's left in our array. 61 | break -- Break at end, as it should be, according to the lua manual. 62 | end 63 | 64 | n = n + 1 65 | end 66 | 67 | return result, n 68 | end 69 | 70 | -- local usage insted of global extend string 71 | local _M = new_tab(0,3) 72 | _M.titlecase = string.titlecase 73 | _M.trim = string.trim 74 | _M.split = string.split 75 | return _M 76 | -------------------------------------------------------------------------------- /lib/resty/stack/validation.lua: -------------------------------------------------------------------------------- 1 | local new_tab = require 'table.new' 2 | local trim = require 'resty.stack.string'.trim 3 | local _M = new_tab(0,1) 4 | 5 | function _M.required(model, properties) 6 | if not model or not properties then return {'data is empty'} end 7 | 8 | local n = #properties 9 | if n == 0 then return end 10 | 11 | local errors = new_tab(0,n) 12 | local e = 0 13 | for i=1,n do 14 | local p = properties[i] 15 | local v = trim(model[p]) 16 | if not v or v == '' then 17 | e = e + 1 18 | errors[p] = "is required" 19 | end 20 | end 21 | return errors, e 22 | end 23 | 24 | return _M 25 | -------------------------------------------------------------------------------- /t/authorize.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 3); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = <<"_EOC_"; 11 | lua_package_path "$pwd/t/servroot/html/?.lua;$pwd/lib/?.lua;;"; 12 | init_by_lua " 13 | local stack = require 'resty.stack' 14 | app = stack:new({ debug = true }) 15 | app:use('authorize', { 16 | AUTHORIZE = true, 17 | get = function() end, 18 | post = function() end 19 | }) 20 | app:use('authorizes', { 21 | AUTHORIZE = { get = true, delete = {'read write'} }, 22 | get = function() end, 23 | post = function() end, 24 | delete = function() end 25 | }) 26 | function auth(self) 27 | if self.arg.auth then 28 | return true 29 | end 30 | end 31 | "; 32 | _EOC_ 33 | 34 | no_long_string(); 35 | #no_diff(); 36 | 37 | run_tests(); 38 | 39 | __DATA__ 40 | 41 | === TEST 1: Authorize 42 | --- http_config eval: $::HttpConfig 43 | --- config 44 | location /authorize { 45 | content_by_lua "app:run()"; 46 | } 47 | --- request eval 48 | ['GET /authorize','POST /authorize', 'GET /authorize?auth=1'] 49 | --- error_code eval 50 | [200, 200, 200] 51 | 52 | === TEST 2: Authorizes 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location /authorizes { 56 | content_by_lua "app:run()"; 57 | } 58 | --- request eval 59 | ['GET /authorizes','POST /authorizes', 'GET /authorizes?auth=1'] 60 | --- error_code eval 61 | [200, 200, 200] 62 | 63 | === TEST 3: use Authorize 64 | --- http_config eval: $::HttpConfig 65 | --- config 66 | location /authorize { 67 | content_by_lua " 68 | app.authorize = auth 69 | app:run() 70 | "; 71 | } 72 | --- request eval 73 | ['GET /authorize','POST /authorize', 'GET /authorize?auth=1'] 74 | --- error_code eval 75 | [401, 401, 200] 76 | 77 | === TEST 4: use Authorizes 78 | --- http_config eval: $::HttpConfig 79 | --- config 80 | location /authorizes { 81 | content_by_lua " 82 | app.authorize = auth 83 | app:run() 84 | "; 85 | } 86 | --- request eval 87 | ['GET /authorizes','POST /authorizes', 'GET /authorizes?auth=1'] 88 | --- error_code eval 89 | [401, 200, 200] 90 | 91 | === TEST 5: use function authorize 92 | --- http_config eval: $::HttpConfig 93 | --- config 94 | location =/public { 95 | content_by_lua " 96 | app:use(function() end) 97 | app:run() 98 | "; 99 | } 100 | 101 | location =/auth { 102 | content_by_lua " 103 | app.authorize = auth 104 | app:use(function() end, true) 105 | app:run() 106 | "; 107 | } 108 | --- request eval 109 | ['GET /public','GET /auth', 'GET /auth?auth=1'] 110 | --- error_code eval 111 | [200, 401, 200] 112 | 113 | -------------------------------------------------------------------------------- /t/method.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 18); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = <<"_EOC_"; 11 | lua_package_path "$pwd/t/servroot/html/?.lua;/$pwd/lib/?.lua;;"; 12 | _EOC_ 13 | 14 | no_long_string(); 15 | #no_diff(); 16 | 17 | run_tests(); 18 | 19 | __DATA__ 20 | 21 | === TEST 1: response to each methods 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location /t { 25 | content_by_lua " 26 | local stack = require 'resty.stack' 27 | app = stack:new() 28 | app:use(require 'method') 29 | app:run() 30 | "; 31 | } 32 | --- user_files 33 | >>> method.lua 34 | local _M = {} 35 | function _M.get() 36 | return 'get' 37 | end 38 | function _M.post() 39 | return 'post' 40 | end 41 | function _M.put() 42 | return 'put' 43 | end 44 | function _M.delete() 45 | return 'delete' 46 | end 47 | return _M 48 | --- request eval 49 | ['GET /t', 'POST /t', 'PUT /t', 'DELETE /t', 50 | 'GET /t?method=POST', 'GET /t?method=PUT', 'GET /t?method=DELETE', 51 | 'HEAD /t', 'OPTIONS /t'] 52 | --- response_body eval 53 | ['get', 'post', 'put', 'delete', 'post', 'put', 'delete', '', ''] 54 | 55 | -------------------------------------------------------------------------------- /t/module.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 18); 7 | 8 | no_long_string(); 9 | #no_diff(); 10 | 11 | run_tests(); 12 | 13 | __DATA__ 14 | 15 | === TEST 1: response to each methods 16 | --- http_config 17 | lua_package_path "${prefix}../../lib/?.lua;/${prefix}/html/?.lua;;"; 18 | init_by_lua " 19 | local stack = require 'resty.stack' 20 | app = stack:new() 21 | app:service({'t', t = {'m'}, a = 't.m'}) 22 | "; 23 | --- config 24 | location /t { 25 | content_by_lua "app:run()"; 26 | } 27 | location /a { 28 | content_by_lua "app:run()"; 29 | } 30 | --- user_files 31 | >>> t.lua 32 | local _M = {} 33 | function _M.get(self) 34 | return 't'..(self.arg.id or '') 35 | end 36 | function _M.x(self) 37 | return 'tx'..(self.arg.id or '') 38 | end 39 | return _M 40 | >>> t/m.lua 41 | local _M = {} 42 | function _M.get(self) 43 | return 'tm'..(self.arg.id or '') 44 | end 45 | function _M.x(self) 46 | return 'tmx'..(self.arg.id or '') 47 | end 48 | return _M 49 | --- request eval 50 | ['GET /t', 'GET /t/1', 'GET /t/1/x' 51 | ,'GET /t/m', 'GET /t/m/1', 'GET /t/m/1/x' 52 | ,'GET /a', 'GET /a/1', 'GET /a/1/x'] 53 | --- response_body eval 54 | ['t', 't1', 'tx1', 'tm', 'tm1', 'tmx1', 55 | 'tm', 'tm1', 'tmx1'] 56 | 57 | -------------------------------------------------------------------------------- /t/request.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 3); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = <<"_EOC_"; 11 | lua_package_path "$pwd/t/servroot/html/?.lua;$pwd/lib/?.lua;;"; 12 | init_by_lua " 13 | local db = require 'resty.stack.db' 14 | local stack = require 'resty.stack' 15 | app = stack:new() 16 | app:use('t', function(self) 17 | return self.r:get('cat') 18 | end) 19 | function app.begin_request(self) 20 | self.r = db.redis(self.config.redis) 21 | self.r:set('cat', 'tiger') 22 | end 23 | function app.end_request(self) 24 | db.keep(self.r, self.config.redis) 25 | end 26 | "; 27 | _EOC_ 28 | 29 | no_long_string(); 30 | run_tests(); 31 | 32 | __DATA__ 33 | 34 | === TEST 1: begin and end request 35 | --- http_config eval: $::HttpConfig 36 | --- config 37 | location /t { 38 | content_by_lua "app:run()"; 39 | } 40 | --- request 41 | GET /t 42 | --- response_body: tiger 43 | --- no_error_log 44 | [error] 45 | -------------------------------------------------------------------------------- /t/router.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 10); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = <<"_EOC_"; 11 | lua_package_path "$pwd/t/servroot/html/?.lua;/$pwd/lib/?.lua;;"; 12 | init_by_lua " 13 | local _M = {} 14 | function _M.get(self) 15 | local id = self.arg.id or '' 16 | return 'get'..id 17 | end 18 | function _M.x(self) 19 | local id = self.arg.id or '' 20 | return 'x'..id 21 | end 22 | local stack = require 'resty.stack' 23 | app = stack:new({debug = true}) 24 | app:use('t', _M) 25 | app:use('t/m', _M) 26 | "; 27 | _EOC_ 28 | 29 | no_long_string(); 30 | #no_diff(); 31 | 32 | run_tests(); 33 | 34 | __DATA__ 35 | 36 | === TEST 1: first level routing 37 | --- http_config eval: $::HttpConfig 38 | --- config 39 | location /t { 40 | content_by_lua "app:run()"; 41 | } 42 | --- request eval 43 | ['GET /t', 'GET /t?id=1', 'GET /t/x', 'GET /t/1', 'GET /t/1/x'] 44 | --- response_body eval 45 | ['get', 'get1', 'x', 'get1', 'x1'] 46 | 47 | === TEST 2: second level routing 48 | --- http_config eval: $::HttpConfig 49 | --- config 50 | location /t/m { 51 | content_by_lua "app:run()"; 52 | } 53 | --- request eval 54 | ['GET /t/m', 'GET /t/m?id=1', 'GET /t/m/x', 'GET /t/m/1', 'GET /t/m/1/x'] 55 | --- response_body eval 56 | ['get', 'get1', 'x', 'get1', 'x1'] 57 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * (blocks() * 6); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = <<"_EOC_"; 11 | lua_package_path "$pwd/t/servroot/html/?.lua;$pwd/lib/?.lua;;"; 12 | _EOC_ 13 | 14 | no_long_string(); 15 | #no_diff(); 16 | 17 | run_tests(); 18 | 19 | __DATA__ 20 | 21 | === TEST 1: inline use 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location /t { 25 | content_by_lua " 26 | local app = require 'resty.stack':new() 27 | app:use(function(self) 28 | return 'ok' 29 | end) 30 | app:run() 31 | "; 32 | } 33 | 34 | location /base { 35 | content_by_lua " 36 | local stack = require 'resty.stack' 37 | local app = stack:new{ base = '/base/' } 38 | app:use(function(self) 39 | return {base = 'base'} 40 | end) 41 | app:run() 42 | "; 43 | } 44 | --- request eval 45 | ['GET /t', 'GET /base'] 46 | --- response_body eval 47 | ['ok', '{"base":"base"}'] 48 | --- no_error_log 49 | [error] 50 | 51 | === TEST 2: use service 52 | --- http_config 53 | lua_package_path "${prefix}../../lib/?.lua;${prefix}html/?.lua;;"; 54 | init_by_lua " 55 | local stack = require 'resty.stack' 56 | app = stack:new() 57 | app:use('hello') 58 | "; 59 | --- config 60 | location /hello { 61 | content_by_lua "app:run()"; 62 | } 63 | --- user_files 64 | >>> hello.lua 65 | local _M = {} 66 | function _M.get(self) 67 | return '' 68 | end 69 | function _M.empty(self) 70 | return 71 | end 72 | return _M 73 | --- request eval 74 | ['GET /hello', 'GET /hello/empty'] 75 | --- response_body eval 76 | ['', ''] 77 | --- no_error_log 78 | [error] 79 | 80 | === TEST 3: override status 81 | --- http_config eval: $::HttpConfig 82 | --- config 83 | location /t { 84 | content_by_lua " 85 | local app = require 'resty.stack':new() 86 | app:use({ 87 | get = function() return nil, 304 end, 88 | post = function() return 'ACCEPTED', 202 end, 89 | put = function() return 'CREATED', 201 end, 90 | delete = function() return nil, 204 end 91 | }) 92 | app:run() 93 | "; 94 | } 95 | --- request eval 96 | ['GET /t', 'POST /t', 'PUT /t', 'DELETE /t'] 97 | --- error_code eval 98 | [304, 202, 201, 204] 99 | --- response_body eval 100 | ['', 'ACCEPTED', 'CREATED', ''] 101 | 102 | === TEST 4: throw error 103 | --- http_config eval: $::HttpConfig 104 | --- config 105 | location /t { 106 | content_by_lua " 107 | local app = require 'resty.stack':new() 108 | app:use({ 109 | get = function() return 'UNAUTHORIZED', 401 end, 110 | post = function() return 'FORBIDDEN', 403 end, 111 | put = function() return 'NOT_FOUND', 404 end, 112 | delete = function() return 'NOT_ALLOWED', 405 end 113 | }) 114 | app:run() 115 | "; 116 | } 117 | --- request eval 118 | ['GET /t', 'POST /t', 'PUT /t', 'DELETE /t'] 119 | --- error_code eval 120 | [401, 403, 404, 405] 121 | 122 | --------------------------------------------------------------------------------