├── .gitignore
├── README.md
├── TODO.md
├── bootstrap.lua
├── config.lua
├── index.lua
├── lib
├── estrela
│ ├── cli.lua
│ ├── io
│ │ ├── ob.lua
│ │ └── pprint.lua
│ ├── log
│ │ ├── common.lua
│ │ ├── file.lua
│ │ └── ngx.lua
│ ├── ngx
│ │ ├── app.lua
│ │ ├── request.lua
│ │ ├── response.lua
│ │ ├── response
│ │ │ └── cookie.lua
│ │ ├── router.lua
│ │ ├── session
│ │ │ └── engine
│ │ │ │ └── common.lua
│ │ └── tmpl
│ │ │ ├── 404.html
│ │ │ └── 500.html
│ ├── oop
│ │ ├── nano.lua
│ │ └── single.lua
│ ├── storage
│ │ └── engine
│ │ │ ├── common.lua
│ │ │ └── shmem.lua
│ ├── util
│ │ ├── env.lua
│ │ ├── path.lua
│ │ ├── string.lua
│ │ └── table.lua
│ └── web.lua
└── resty
│ ├── README.md
│ └── upload.lua
├── nginx.conf.example
├── tester.lua
└── tests
├── 00_check.lua
├── 01_util_table.lua
├── 02_util_string.lua
├── 03_util_path.lua
├── 04_util_env.lua
├── 05_io_ob.lua
├── 06_io_pprint.lua
├── 07_log_common.lua
├── 08_log_file.lua
├── 09_log_ngx.lua
├── 10_storage_engine_common.lua
├── 11_storage_engine_shmem.lua
├── 12_ngx_response_cookie.lua
└── tester
├── cli.lua
├── common.lua
└── ngx.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | estrela
2 | =======
3 |
4 | Lua framework for nginx (in early stage of development)
5 |
6 | ### In a nutshell
7 |
8 | **bootstrap.lua**
9 | ```lua
10 | local PP = require('estrela.io.pprint').print
11 |
12 | return require('estrela.web').App {
13 | ['$'] = function(app, req, resp)
14 | app:defer(function()
15 | ngx.say 'Hello from defer!'
16 | end)
17 |
18 | local S = app.session:start()
19 |
20 | local cnt = tonumber(S.cnt) or 0
21 | ngx.say('Hello world ', cnt, ' times')
22 | S.cnt = cnt + 1
23 |
24 | app.session:stop()
25 |
26 | return app:forward('/boo')
27 | end,
28 |
29 | [{account = '/:name$', profile = '/profile/:name$'}] = function(app)
30 | local url = app.router:urlFor(app.route.name, app.route.params)
31 | ngx.say('Hello from ', app.req.method, ' ', url)
32 | end,
33 |
34 | ['/boo'] = {
35 | GET = function(app, req)
36 | ngx.say 'def GET'
37 | PP(req.GET)
38 | PP(req.COOKIE)
39 | end,
40 |
41 | POST = function(app, req)
42 | ngx.say 'def POST'
43 | PP(req.POST)
44 | PP(req.FILES)
45 | end,
46 |
47 | [{'HEAD', 'OPTIONS'}] = function()
48 | ngx.status = 403
49 | ngx.say 'Go home!'
50 | return ngx.exit(0)
51 | end,
52 | },
53 |
54 | ['/redirect'] = function()
55 | return app:redirect('http://ater.me/')
56 | end,
57 |
58 | ['/external'] = 'ourcoolsite.routes', -- performs require('ourcoolsite.routes')
59 |
60 | ['/fail$'] = function()
61 | -- this function is not defined => throw a HTTP error #500
62 | -- error will be handled in route 500 below
63 | fooBar()
64 | end,
65 |
66 | [404] = function(app)
67 | ngx.say 'Route is not found'
68 | --return app:defaultErrorPage()
69 | end,
70 |
71 | [500] = function(app, req)
72 | ngx.say('Ooops in ', req.url, '\n', SP(app.error))
73 | --return true
74 | end,
75 | }
76 |
77 | app.router:mount('/admin', {
78 | ['/:action/do$'] = function(app)
79 | if not user_function_for_check_auth() then
80 | return app:abort(404, 'Use cookie, Luke!')
81 | end
82 | ngx.say('admin do ', app.route.params.action, '
')
83 | end,
84 | })
85 |
86 | app.trigger.before_req:add(function(app, req, resp)
87 | resp.headers.content_type = 'text/plain'
88 |
89 | app.log.debug('New request ', app.req.url)
90 |
91 | if req.GET._method then
92 | req.method = req.GET._method:upper()
93 | end
94 | end)
95 |
96 | app.trigger.after_req:add(function()
97 | ngx.say 'Goodbye!'
98 | end)
99 | ```
100 |
101 | **nginx.conf**
102 | ```nginx
103 | http {
104 | lua_package_path '/path/to/lua/?.lua;/path/to/lua/lib/?.lua;;';
105 |
106 | server {
107 | location / {
108 | content_by_lua_file /path/to/lua/index.lua;
109 | }
110 | }
111 | }
112 | ```
113 |
114 | **index.lua**
115 | ```lua
116 | return xpcall(
117 | function()
118 | local app = require('bootstrap')
119 |
120 | local configurator = require('config')
121 | if type(configurator) == 'function' then
122 | configurator(app.config)
123 | end
124 |
125 | return app:serve()
126 | end,
127 | function(err)
128 | ngx.log(ngx.ERR, err)
129 | ngx.status = 500
130 | ngx.print 'Ooops! Something went wrong'
131 | return ngx.exit(0)
132 | end
133 | )
134 | ```
135 |
136 | **config.lua**
137 | ```lua
138 | return function(cfg)
139 | -- Для вывода подробного описания ошибок (если не объявлен 500 роут)
140 | cfg.debug = true
141 |
142 | -- Разрешаем использовать сессии. Без этого app.session использовать нельзя
143 | cfg.session.active = true
144 |
145 | -- nginx.conf "location /estrela {"
146 | -- Если не указать, то пути маршрутизации должны быть полными: ['/estrela/$'], ['/estrela/do/:action'], etc.
147 | cfg.router.pathPrefix = '/estrela'
148 |
149 | -- Вместо логирования в nginx error_log (стандартное поведение) пишем в отдельный файл
150 | cfg.error_logger = require('estrela.log.file'):new('/tmp/estrela.error.log')
151 | end
152 | ```
153 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | TODO (список без приоритетов)
2 | ====
3 |
4 | * хранение шифрованной сессии прямо в куках
5 | * тесты
6 | * документация
7 | * проверить keep-alive с post запросами
8 | * опциональные именованные параметры роутов
9 | * префиксы имен роутов при монтировании для решения проблемы возможного конфликта имен для urlFor
10 | * regexp ограничения на значения именованных параметров
11 | * flash сообщения
12 | * доработать app.trigger для произвольного расширения прикладным кодом
13 | * table.slice
14 | * обойтись в util.string.trim без вызовов ltrim+rtirm
15 |
--------------------------------------------------------------------------------
/bootstrap.lua:
--------------------------------------------------------------------------------
1 | local app = require('estrela.web').App {
2 | ['$'] = function(app)
3 | local S = app.session:start()
4 |
5 | local cnt = tonumber(S.cnt) or 0
6 | ngx.say('Hello world ', cnt, ' times')
7 | S.cnt = cnt + 1
8 |
9 | app.session:stop()
10 | end,
11 | }
12 |
13 | app.trigger.before_req:add(function(app)
14 | app.resp.headers.content_type = 'text/plain'
15 | end)
16 |
17 | app.trigger.after_req:add(function()
18 | ngx.say 'Goodbye!'
19 | end)
20 |
21 | return app
22 |
--------------------------------------------------------------------------------
/config.lua:
--------------------------------------------------------------------------------
1 | return function(cfg)
2 | -- Для вывода подробного описания ошибок (если не объявлен 500 роут)
3 | cfg.debug = true
4 |
5 | -- Разрешаем использовать сессии. Без этого app.session использовать нельзя
6 | cfg.session.active = true
7 |
8 | -- nginx.conf "location /estrela {"
9 | -- Если не указать, то пути маршрутизации должны быть полными: ['/estrela/$'], ['/estrela/do/:action'], etc.
10 | cfg.router.pathPrefix = '/estrela'
11 |
12 | -- Вместо логирования в nginx error_log (стандартное поведение) пишем в отдельный файл
13 | cfg.error_logger = require('estrela.log.file'):new('/tmp/estrela.error.log')
14 | end
15 |
--------------------------------------------------------------------------------
/index.lua:
--------------------------------------------------------------------------------
1 | return xpcall(
2 | function()
3 | local app = require('bootstrap')
4 |
5 | local configurator = require('config')
6 | if type(configurator) == 'function' then
7 | configurator(app.config)
8 | end
9 |
10 | return app:serve()
11 | end,
12 | function(err)
13 | ngx.log(ngx.ERR, err)
14 | ngx.status = 500
15 | ngx.print 'Ooops! Something went wrong'
16 | return ngx.exit(0)
17 | end
18 | )
19 |
--------------------------------------------------------------------------------
/lib/estrela/cli.lua:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/atercattus/estrela/88ac4d6c9c12998c14568c3b5d10f594802f5b20/lib/estrela/cli.lua
--------------------------------------------------------------------------------
/lib/estrela/io/ob.lua:
--------------------------------------------------------------------------------
1 | local ipairs = ipairs
2 | local pairs = pairs
3 | local table_concat = table.concat
4 | local table_insert = table.insert
5 | local tostring = tostring
6 |
7 | local buf = {}
8 |
9 | local _direct_print = ngx and ngx.print or io.write
10 |
11 | local _orig_print, _orig_io_write, _orig_ngx_say, _orig_ngx_print
12 |
13 | local M = {}
14 |
15 | function M.print(...)
16 | if #buf == 0 then
17 | for _, v in pairs({...}) do
18 | _direct_print(tostring(v))
19 | end
20 | else
21 | local len = 0
22 | local _buf = buf[#buf]
23 | for _, v in pairs({...}) do
24 | local chunk = tostring(v)
25 | if _buf['cb'] then
26 | chunk = _buf['cb'](chunk)
27 | end
28 | len = len + #chunk
29 | table_insert(_buf['buf'], chunk)
30 | end
31 | _buf['len'] = _buf['len'] + len
32 | end
33 | end
34 |
35 | function M.println(...)
36 | M.print(...)
37 | M.print('\n')
38 | end
39 |
40 | function M.print_lua(...)
41 | for i, v in pairs({...}) do
42 | if i > 1 then
43 | M.print('\t')
44 | end
45 | M.print(v)
46 | end
47 | M.print('\n')
48 | end
49 |
50 | local function _set_hooks()
51 | _orig_print, print = print, M.print_lua
52 | _orig_io_write, io.write = io.write, M.print
53 |
54 | if ngx then
55 | _orig_ngx_say, ngx.say = ngx.say, M.println
56 | _orig_ngx_print, ngx.print = ngx.print, M.print
57 | end
58 | end
59 |
60 | local function _restore_hooks()
61 | print = _orig_print
62 | io.write = _orig_io_write
63 |
64 | if ngx then
65 | ngx.say = _orig_ngx_say
66 | ngx.print = _orig_ngx_print
67 | end
68 | end
69 |
70 | function M.start(cb)
71 | if #buf == 0 then
72 | _set_hooks()
73 | end
74 |
75 | buf[#buf + 1] = {
76 | buf = {},
77 | len = 0,
78 | cb = cb,
79 | }
80 | end
81 |
82 | function M.finish()
83 | buf[#buf] = nil
84 |
85 | if #buf == 0 then
86 | _restore_hooks()
87 | end
88 | end
89 |
90 | function M.flush()
91 | if #buf > 0 then
92 | local b, cb = table_concat(buf[#buf]['buf']), buf[#buf]['cb']
93 | M.finish()
94 | M.print(b)
95 | M.start(cb)
96 | end
97 | end
98 |
99 | function M.flush_finish()
100 | if #buf > 0 then
101 | local b = table_concat(buf[#buf]['buf'])
102 | M.finish()
103 | M.print(b)
104 | end
105 | end
106 |
107 | function M.get()
108 | return (#buf > 0) and table_concat(buf[#buf]['buf']) or nil
109 | end
110 |
111 | function M.levels()
112 | return #buf
113 | end
114 |
115 | function M.len()
116 | return (#buf > 0) and buf[#buf]['len'] or nil
117 | end
118 |
119 | function M.clean()
120 | if #buf > 0 then
121 | buf[#buf]['buf'], buf[#buf]['len'] = {}, 0
122 | end
123 | end
124 |
125 | function M.status()
126 | if #buf == 0 then
127 | return {}
128 | end
129 |
130 | local res = {}
131 |
132 | for _,lvl in ipairs(buf) do
133 | table_insert(res, {
134 | len = lvl['len'],
135 | cb = lvl['cb'],
136 | })
137 | end
138 |
139 | return res
140 | end
141 |
142 | return M
143 |
--------------------------------------------------------------------------------
/lib/estrela/io/pprint.lua:
--------------------------------------------------------------------------------
1 | local require = require
2 |
3 | local debug = require('debug')
4 | local debug_getinfo = debug.getinfo
5 | local pairs = pairs
6 | local string_format = string.format
7 | local string_gsub = string.gsub
8 | local string_rep = string.rep
9 | local table_concat = table.concat
10 | local table_insert = table.insert
11 | local table_sort = table.sort
12 | local tonumber = tonumber
13 | local tostring = tostring
14 | local type = type
15 |
16 | local M = {}
17 |
18 | local T = require('estrela.util.table')
19 | local T_clone = T.clone
20 |
21 | local PATH = require('estrela.util.path')
22 | local path_rel = PATH.rel
23 |
24 | local function sort_cb(a, b)
25 | if type(a) == 'number' and type(b) == 'number' then
26 | return a < b
27 | else
28 | return tostring(a) < tostring(b)
29 | end
30 | end
31 |
32 | local function prepare_string(s)
33 | -- спец обработка табуляции для сохранения ее в строке в форме \t
34 | s = string_gsub(s, '\t', [[\t]])
35 | s = string_format('%q', s)
36 | s = string_gsub(s, [[\\t]], [[\t]])
37 | return s
38 | end
39 |
40 | function M.sprint(v, opts)
41 | opts = opts or {}
42 |
43 | -- максимальная глубина обхода вложенных структур
44 | local max_depth = tonumber(opts.max_depth) or 100
45 | -- компактный вывод (без табуляций, новых строк)
46 | local compact = opts.compact
47 | -- выводить ли для функций место (файл:строка) их объявления
48 | local func_pathes = opts.func_pathes == nil and true or opts.func_pathes
49 | -- выводить ли сокращенные (относительные) пути при func_pathes
50 | local func_patches_short = opts.func_patches_short == nil and true or opts.func_patches_short
51 | -- на сколько уровней вверх по файловой системе можно подниматься, если функция была объявлена выше корня фреймворка
52 | local func_patches_short_rel = tonumber(opts.func_patches_short_rel) or 4
53 |
54 | local env_root
55 | if func_pathes and func_patches_short then
56 | env_root = require('estrela.util.env').get_root()
57 | end
58 |
59 | local key_opts = T_clone(opts)
60 | key_opts.compact = true
61 |
62 | local result = {}
63 |
64 | local visited = {}
65 |
66 | local function _pretty_print(name, v, path)
67 |
68 | local prefix = compact and '' or string_rep('\t', #path)
69 |
70 | table_insert(result, prefix)
71 | if #name > 0 then
72 | table_insert(result, name .. ' ')
73 | end
74 |
75 | local type_v = type(v)
76 | if type_v == 'string' then
77 | table_insert(result, prepare_string(v))
78 | elseif type_v == 'number' or type_v == 'boolean' then
79 | table_insert(result, tostring(v))
80 | elseif type_v == 'table' then
81 | if #path <= max_depth then
82 | local table_id = tostring(v)
83 | if visited[table_id] then
84 | table_insert(result, 'nil --[[recursion to ' .. visited[table_id] .. ']]')
85 | else
86 | visited[table_id] = table_concat(path, '.')
87 |
88 | local keys = {}
89 | local only_numbers = true
90 | for key, _ in pairs(v) do
91 | table_insert(keys, key)
92 | only_numbers = only_numbers and (type(key) == 'number')
93 | end
94 |
95 | table_sort(keys, only_numbers and nil or sort_cb)
96 |
97 | table_insert(result, compact and '{' or '{\n')
98 | for _, key in pairs(keys) do
99 | local _key
100 | if type(key) == 'string' then
101 | _key = prepare_string(tostring(key))
102 | elseif type(key) == 'number' then
103 | _key = tostring(key)
104 | else
105 | local _max_depth = key_opts.max_depth
106 | key_opts.max_depth = max_depth - #path - 1
107 | _key = M.sprint(key, key_opts)
108 | key_opts.max_depth = _max_depth
109 | end
110 | table_insert(path, _key)
111 | _pretty_print('[' .. _key .. '] =', v[key], path)
112 | path[#path] = nil
113 | end
114 | table_insert(result, prefix .. '}')
115 | end
116 | else
117 | table_insert(result, 'nil --[[max_depth cutted]]')
118 | end
119 | elseif v == nil then
120 | table_insert(result, 'nil')
121 | else
122 | local _v
123 | if type_v == 'function' then
124 | if func_pathes then
125 | local func_info = debug_getinfo(v)
126 | local path = func_info.short_src
127 | if func_patches_short then
128 | if path:find(env_root, 1, true) == 1 then
129 | path = '...' .. path:sub(env_root:len()+1)
130 | elseif func_patches_short_rel > 0 then
131 | local _path, _rel_len = path_rel(env_root, path)
132 | if _rel_len <= func_patches_short_rel then
133 | path = '.../' .. _path
134 | end
135 | end
136 | end
137 | local line = func_info.linedefined
138 | line = (line >= 0) and (':' .. line) or ''
139 |
140 | _v = tostring(v) .. ' ' .. path .. line
141 | else
142 | _v = tostring(v)
143 | end
144 | else
145 | _v = type_v
146 | end
147 | table_insert(result, 'nil --[[' .. _v .. ']]')
148 | end
149 |
150 | if #path > 0 then
151 | table_insert(result, ',')
152 | end
153 |
154 | if not compact then
155 | table_insert(result, '\n')
156 | end
157 | end
158 |
159 | _pretty_print('', v, {})
160 |
161 | return table_concat(result)
162 | end
163 |
164 | function M.print(v, opts)
165 | opts = opts or {}
166 | local writer = opts.writer or (ngx and ngx.print or io.write);
167 | writer(M.sprint(v, opts))
168 | end
169 |
170 | return M
171 |
--------------------------------------------------------------------------------
/lib/estrela/log/common.lua:
--------------------------------------------------------------------------------
1 | local error = error
2 | local debug_traceback = debug.traceback
3 | local table_concat = table.concat
4 | local table_insert = table.insert
5 |
6 | local S = require('estrela.util.string')
7 | local S_split = S.split
8 | local S_trim = S.trim
9 |
10 | local M = {}
11 |
12 | function M:new()
13 | local L = {
14 | DEBUG = 8,
15 | INFO = 7,
16 | NOTICE = 6,
17 | WARN = 5,
18 | ERR = 4,
19 | CRIT = 3,
20 | ALERT = 2,
21 | EMERG = 1,
22 |
23 | DISABLE = 0, -- для выключения логирования
24 | }
25 |
26 | L.level = L.DEBUG -- выводить все ошибки
27 | L.bt_level = L.WARN -- не выводить backtrace для некритичных ошибок
28 |
29 | L.level_names = {
30 | [L.DEBUG] = 'DEBUG',
31 | [L.INFO] = 'INFO',
32 | [L.NOTICE] = 'NOTICE',
33 | [L.WARN] = 'WARN',
34 | [L.ERR] = 'ERR',
35 | [L.CRIT] = 'CRIT',
36 | [L.ALERT] = 'ALERT',
37 | [L.EMERG] = 'EMERG',
38 |
39 | [L.DISABLE] = 'DISABLE',
40 | }
41 |
42 | local function _get_bt(bt_lvl)
43 | if L.bt_level >= bt_lvl then
44 | local _bt = S_split(debug_traceback(), '\n')
45 | local bt = {}
46 | for i = 4, #_bt do
47 | table_insert(bt, S_trim(_bt[i]))
48 | end
49 | return table_concat(bt, '\n')
50 | else
51 | return ''
52 | end
53 | end
54 |
55 | local function _write(lvl, args)
56 | if L.level >= lvl then
57 | return L._write(lvl, table_concat(args), _get_bt(lvl))
58 | end
59 | end
60 |
61 | L._write = function(lvl, msg, bt) return error('Not implemented') end
62 |
63 | L.debug = function(...) return _write(L.DEBUG, {...}) end
64 | L.info = function(...) return _write(L.INFO, {...}) end
65 | L.notice = function(...) return _write(L.NOTICE, {...}) end
66 | L.warn = function(...) return _write(L.WARN, {...}) end
67 | L.err = function(...) return _write(L.ERR, {...}) end
68 | L.crit = function(...) return _write(L.CRIT, {...}) end
69 | L.alert = function(...) return _write(L.ALERT, {...}) end
70 | L.emerg = function(...) return _write(L.EMERG, {...}) end
71 |
72 | L.warning = L.warn
73 | L.error = L.err
74 | L.critical = L.crit
75 | L.emergency = L.emerg
76 |
77 | return L
78 | end
79 |
80 | return M
81 |
--------------------------------------------------------------------------------
/lib/estrela/log/file.lua:
--------------------------------------------------------------------------------
1 | local error = error
2 | local string_gsub = string.gsub
3 | local string_format = string.format
4 | local os_date = os.date
5 | local io_open = io.open
6 | local require = require
7 |
8 | local M = {}
9 |
10 | function M:new(path, fmt)
11 | fmt = fmt or '%Y/%m/%d %H:%M:%S {LVL} [{MSG}] {BT}'
12 |
13 | local f
14 |
15 | local L = require('estrela.log.common'):new()
16 |
17 | L._write = function(lvl, msg, bt)
18 | if not f then
19 | f = io_open(path, 'ab')
20 | if not f then
21 | return error('Cannot open log file ' .. path)
22 | end
23 | end
24 |
25 | local line = os_date(fmt)
26 | local bt = string_gsub(bt, '\n', ' => ')
27 | local lvl_str = string_format('%-7s', L.level_names[lvl])
28 | line = string_gsub(string_gsub(string_gsub(line, '{LVL}', lvl_str), '{MSG}', msg), '{BT}', bt)
29 | f:write(line, '\n')
30 | f:flush()
31 | end
32 |
33 | L.close = function()
34 | if f then
35 | f:close()
36 | f = nil
37 | end
38 | end
39 |
40 | return L
41 | end
42 |
43 | return M
44 |
--------------------------------------------------------------------------------
/lib/estrela/log/ngx.lua:
--------------------------------------------------------------------------------
1 | local require = require
2 | local string_gsub = string.gsub
3 |
4 | local ngx_log = ngx.log
5 |
6 | local M = {}
7 |
8 | function M:new()
9 | local L = require('estrela.log.common'):new()
10 |
11 | L._write = function(lvl, msg, bt)
12 | local bt = string_gsub(bt, '\n', ' => ')
13 | return ngx_log(lvl, msg, ', backtrace: ', bt)
14 | end
15 |
16 | return L
17 | end
18 |
19 | return M
20 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/app.lua:
--------------------------------------------------------------------------------
1 | local require = require
2 |
3 | local OB = require('estrela.io.ob')
4 | local OB_finish = OB.finish
5 | local OB_flush = OB.flush
6 | local OB_levels = OB.levels
7 | local OB_print = OB.print
8 | local OB_start = OB.start
9 |
10 | local assert = assert
11 | local debug = require('debug')
12 | local debug_traceback = debug.traceback
13 | local error = error
14 | local io_open = io.open
15 | local ipairs = ipairs
16 | local loadstring = loadstring
17 | local next = next
18 | local pairs = pairs
19 | local setmetatable = setmetatable
20 | local table_concat = table.concat
21 | local table_insert = table.insert
22 | local tonumber = tonumber
23 | local tostring = tostring
24 | local type = type
25 | local unpack = unpack
26 | local xpcall = xpcall
27 |
28 | local ngx_gsub = ngx.re.gsub
29 | local ngx_match = ngx.re.match
30 |
31 | local Router = require('estrela.ngx.router')
32 | local Request = require('estrela.ngx.request')
33 | local Response = require('estrela.ngx.response')
34 |
35 | local env_get_root = require('estrela.util.env').get_root
36 | local path_join = require('estrela.util.path').join
37 |
38 | local S = require('estrela.util.string')
39 | local S_split = S.split
40 | local S_starts = S.starts
41 | local S_trim = S.trim
42 | local S_htmlencode = S.htmlencode
43 |
44 | local _config_private = {}
45 |
46 | function _config_private:get(path, default)
47 | local r = self
48 | local p, l = 1, #path
49 |
50 | while p <= l do
51 | local c = string.find(path, '.', p, true) or (l + 1)
52 | local key = string.sub(path, p, c - 1)
53 | if r[key] == nil then
54 | return default
55 | end
56 | p = c + 1
57 | r = r[key]
58 | end
59 |
60 | return r
61 | end
62 |
63 | local _error = {}
64 |
65 | function _error:clean()
66 | self.code = 0
67 | self.msg = nil
68 | self.file = nil
69 | self.line = nil
70 | self.stack = nil
71 | self.aborted_code = nil -- используется для передачи кода ошибки из app:abort() в обработчик xpcall
72 | self.redirect = nil -- используется для передачи url и статуса редиректа из app:redirect()
73 | end
74 |
75 | function _error:init(code, msg)
76 | local info = msg and self:_splitErrorLineByWhereAndMsg(msg) or {}
77 | self.code = tonumber(code) or 500
78 | self.msg = info.msg
79 | self.file = info.file
80 | self.line = info.line
81 | self.stack = self:_parseBacktrace(debug_traceback())
82 | return false
83 | end
84 |
85 | function _error:_parseBacktrace(str)
86 | str = S_split(str, '\n')
87 | if not S_starts(str[1], 'stack traceback:') then
88 | return nil
89 | end
90 |
91 | local bt = {}
92 | for i = 3, #str do -- выкидываем "stack traceback:" и вызов самого _parseBacktrace()
93 | table_insert(bt, self:_splitErrorLineByWhereAndMsg(str[i]))
94 | end
95 |
96 | return bt
97 | end
98 |
99 | function _error:_splitErrorLineByWhereAndMsg(line)
100 | line = S_trim(line)
101 | local m = ngx_match(line, [[^(?[^:]+):(?[0-9]+):(?.*)$]], 'jo') or {}
102 | return {
103 | msg = S_trim(m.msg or line),
104 | file = m.path or '',
105 | line = tonumber(m.line) or 0,
106 | }
107 | end
108 |
109 | function _error:place()
110 | local line = self.line and (':' .. self.line) or ''
111 | if self.file and (#self.file > 0) then
112 | return ' in ' .. self.file .. line
113 | else
114 | return ''
115 | end
116 | end
117 |
118 | local App = {}
119 |
120 | function App:serve()
121 | ngx.ctx.estrela = self
122 |
123 | local logger = self.config.error_logger
124 | if logger then
125 | if type(logger) == 'string' then
126 | logger = require(logger):new()
127 | elseif type(logger) == 'function' then
128 | logger = logger()
129 | end
130 | end
131 | if logger then
132 | self.log = logger
133 | end
134 |
135 | self.req = Request:new()
136 | self.resp = Response:new()
137 | self.error:clean()
138 |
139 | self.session = nil
140 |
141 | local pathPrefix = self.config:get('router.pathPrefix')
142 | if pathPrefix then
143 | self.router.path_prefix = pathPrefix
144 | end
145 |
146 | local with_ob = self.config:get('ob.active')
147 | local ob_level = 0
148 |
149 | self.subpath = setmetatable({}, { __mode = 'v', })
150 | self:_protcall(function()
151 | if with_ob then
152 | OB_start()
153 | ob_level = OB_levels()
154 | end
155 |
156 | if self.config:get('session.active') then
157 | local encdec = self.config:get('session.handler.encdec')
158 | if type(encdec) == 'function' then
159 | encdec = encdec()
160 | end
161 | assert(encdec, 'There are no defined encoder/decoder for session')
162 |
163 | local storage = self.config:get('session.storage.handler')
164 | if type(storage) == 'function' then
165 | storage = storage(self)
166 | end
167 | assert(storage, 'There are no defined storage for session')
168 |
169 | local SESSION = require(self.config:get('session.handler.handler'))
170 |
171 | self.session = SESSION:new(storage, encdec.encode, encdec.decode)
172 | end
173 |
174 | return self:_callTriggers(self.trigger.before_req) and self:_callRoutes()
175 | end)
176 |
177 | -- after_req триггеры вызываются даже в случае ошибки внутри before_req или роутах
178 | if next(self.trigger.after_req) then
179 | self:_protcall(function()
180 | return self:_callTriggers(self.trigger.after_req)
181 | end)
182 | end
183 |
184 | if self.error.code > 0 then
185 | if with_ob then
186 | -- при ошибке отбрасываем все ранее выведенное
187 | -- несбалансированность ob при этом не проверяем, т.к. она допустима при ошибках
188 | while OB_levels() >= ob_level do
189 | OB_finish()
190 | end
191 | end
192 | self:_callErrorCb()
193 | else
194 | if with_ob then
195 | if OB_levels() > ob_level then
196 | self.log.warn('OB is not balanced')
197 | end
198 |
199 | while OB_levels() >= ob_level do
200 | if not self.error.redirect then
201 | OB_flush()
202 | end
203 | OB_finish()
204 | end
205 | end
206 | end
207 |
208 | if self.session then
209 | self.session:stop()
210 | end
211 |
212 | if self.error.redirect then
213 | return ngx.redirect(self.error.redirect.url, self.error.redirect.status)
214 | end
215 | end
216 |
217 | function App:defer(func, ...)
218 | if type(func) ~= 'function' then
219 | return nil, 'missing func'
220 | end
221 | table_insert(self.defers, {cb = func, args = {...}})
222 | return true
223 | end
224 |
225 | function App:abort(code, msg)
226 | self.error.aborted_code = code or 500
227 | return error(msg or '', 2)
228 | end
229 |
230 | function App:redirect(url, status)
231 | self.error.redirect = {
232 | url = url,
233 | status = status or ngx.HTTP_MOVED_TEMPORARILY,
234 | }
235 | return self:abort(0)
236 | end
237 |
238 | function App:defaultErrorPage()
239 | local err = self.error
240 |
241 | self.resp.headers.content_type = 'text/html'
242 |
243 | if err.code > 0 then
244 | ngx.status = err.code
245 | end
246 |
247 | local html = {}
248 |
249 | if self.config.debug then
250 | local enc = S_htmlencode
251 |
252 | table_insert(html, table_concat{
253 | 'Error #', err.code, ' ', enc(err.msg or ''), ' ', enc(err:place()), '
'
254 | })
255 |
256 | if type(err.stack) == 'table' then
257 | table_insert(html, 'Stack:')
258 | for _, bt in ipairs(err.stack) do
259 | table_insert(html, '- ' .. enc(bt.file))
260 | if bt.line > 0 then
261 | table_insert(html, ':' .. bt.line)
262 | end
263 | table_insert(html, ' ' .. enc(bt.msg))
264 | table_insert(html, '
')
265 | end
266 | table_insert(html, '
')
267 | end
268 | end
269 |
270 | local res = self:_renderInternalTemplate(tostring(err.code), {
271 | code = err.code,
272 | descr = table_concat(html),
273 | })
274 |
275 | if not res then
276 | -- полный ахтунг. не удалось вывести страницу с описанием ошибки
277 | return nil
278 | end
279 |
280 | return true
281 | end
282 |
283 | function App:forward(url)
284 | return {
285 | forward = url,
286 | }
287 | end
288 |
289 | -- @return bool
290 | function App:_renderInternalTemplate(name, args)
291 | local path = path_join(env_get_root(), 'ngx', 'tmpl', name .. '.html')
292 | local fd = io_open(path, 'rb')
293 | if not fd then
294 | return false
295 | end
296 |
297 | local cont = fd:read('*all')
298 | fd:close()
299 |
300 | ngx.header.content_type = 'text/html'
301 |
302 | cont = ngx_gsub(
303 | cont,
304 | [[{{([.a-z]+)}}]],
305 | function(m)
306 | local val = args[m[1]] or assert(loadstring('local self = ...; return ' .. m[1]))(self) or ''
307 | return tostring(val)
308 | end,
309 | 'jo'
310 | )
311 |
312 | return OB_print(cont)
313 | end
314 |
315 | function App:_protcall(func)
316 | self.error.aborted_code = nil
317 | return xpcall(
318 | func,
319 | function(err)
320 | self.log.err(err)
321 | local code = self.error.aborted_code and self.error.aborted_code or ngx.HTTP_INTERNAL_SERVER_ERROR
322 | return self.error:init(code, err)
323 | end
324 | )
325 | end
326 |
327 | function App:_callTriggers(triggers_list)
328 | for _, cb in ipairs(triggers_list) do
329 | local ok, res = self:_callRoute(cb)
330 | if not ok then
331 | return ok, res
332 | elseif res == true then
333 | break
334 | end
335 | end
336 | return true
337 | end
338 |
339 | function App:_callErrorCb()
340 | local errno = self.error.code
341 |
342 | local route = self.router:getByName(errno)
343 | if route then
344 | local ok, handled = self:_callRoute(route.cb)
345 | if not (ok and handled) then
346 | -- ошибка в обработчике ошибки, либо обработчик не стал сам обрабатывать ошибку
347 | return self:defaultErrorPage()
348 | end
349 | return ok
350 | else
351 | return self:defaultErrorPage()
352 | end
353 | end
354 |
355 | function App:_callDefers()
356 | local ok = true
357 |
358 | for _, defer in ipairs(self.defers) do
359 | local _ok, _ = self:_protcall(function()
360 | return defer.cb(unpack(defer.args))
361 | end)
362 |
363 | ok = ok and _ok
364 | end
365 |
366 | self.defers = {}
367 | return ok
368 | end
369 |
370 | function App:_callRoute(cb)
371 | self.defers = {}
372 |
373 | local ok, res = self:_protcall(function()
374 | return cb(self, self.req, self.resp)
375 | end)
376 |
377 | local def_ok, def_res = self:_callDefers()
378 |
379 | if ok and not def_ok then
380 | ok, res = def_ok, def_res
381 | end
382 |
383 | return ok, res
384 | end
385 |
386 | function App:_callRoutes()
387 | local ok = true
388 |
389 | local found = false
390 |
391 | local path = self.req.path
392 | if #self.subpath > 0 then
393 | path = self.subpath[#self.subpath].url
394 | end
395 |
396 | for route in self.router:route(path) do
397 | -- защита от рекурсии
398 | for _, sp in ipairs(self.subpath) do
399 | if sp.cb == route.cb then
400 | return self.error:init(ngx.HTTP_INTERNAL_SERVER_ERROR, 'Route recursion detected')
401 | end
402 | end
403 |
404 | self.route = route
405 |
406 | local _ok, res = self:_callRoute(route.cb)
407 | ok = ok and _ok
408 |
409 | local extras = _ok and (type(res) == 'table')
410 | local pass = _ok and (res == true)
411 |
412 | if not pass then
413 | found = true
414 |
415 | if extras then
416 | if res.forward then
417 | local url = self.router:getFullUrl(res.forward)
418 |
419 | table_insert(self.subpath, {url=url, cb=route.cb})
420 | ok = self:_callRoutes()
421 | self.subpath[#self.subpath] = nil
422 | end
423 | end
424 |
425 | break
426 | end
427 | end
428 |
429 | if not found then
430 | return self.error:init(ngx.HTTP_NOT_FOUND, 'Route is not found')
431 | end
432 |
433 | return ok
434 | end
435 |
436 | local M = {}
437 |
438 | function M:new(routes)
439 | local A = {
440 | route = nil,
441 | req = nil,
442 | resp = nil,
443 | session = nil,
444 |
445 | subpath = {}, -- для перенаправлений между роутами в пределах одного запроса
446 |
447 | error = _error,
448 |
449 | log = require('estrela.log.ngx'):new(),
450 |
451 | defers = {},
452 | trigger = {
453 | before_req = {add = table_insert,},
454 | after_req = {add = table_insert,},
455 | },
456 |
457 | config = setmetatable(
458 | {
459 | -- Отладочный режим. При ошибках выводится полный stacktrace
460 | debug = false,
461 |
462 | -- Буферизация вывода
463 | ob = {
464 | active = true,
465 | },
466 |
467 | -- Сессии
468 | session = {
469 | -- При неактивный сессиях app.session не инициализируется (app.session.start не доступна)
470 | active = false,
471 | -- Настройки хранилища сессионных данных
472 | storage = {
473 | handler = function()
474 | local SHMEM = require('estrela.storage.engine.shmem')
475 | return SHMEM:new('session_cache')
476 | end,
477 | },
478 | -- Настройки обработчика сессий
479 | handler = {
480 | -- Стандартный обработчик, хранящий сессионный id в куках
481 | handler = 'estrela.ngx.session.engine.common',
482 | -- Имя ключа (чего либо), хранящего сессионый id
483 | key_name = 'estrela_sid',
484 | -- Префикс для всех ключей хранилища, используемых обработчиком сессий
485 | storage_key_prefix = 'estrela_session:',
486 | -- Время жизни блокировки сессионного ключа в хранилище
487 | storage_lock_ttl = 10,
488 | -- Максимальное время ожидания (сек) отпускания блокировки сессионного ключа в хранилище
489 | storage_lock_timeout = 3,
490 | -- Сервис сериализации для хранения сессионный данных в хранилище
491 | encdec = function()
492 | local json = require('cjson')
493 | json.encode_sparse_array(true)
494 | return json
495 | end,
496 | -- Настройки печеньки, в которой хранится сессионый id
497 | -- Используется, если выбранный обработчик сессий работает через куки
498 | cookie = {
499 | params = { -- смотри app.response.COOKIE.empty
500 | --ttl = 86400,
501 | httponly = true,
502 | path = '/',
503 | },
504 | },
505 | },
506 | },
507 |
508 | -- Маршрутизация запросов
509 | router = {
510 | -- Игнорируемый маршрутизатором префикс URL. На случай работы не из корня сайта.
511 | pathPrefix = '/',
512 | },
513 |
514 | -- Альтернативный логер вместо nginx error_log файла.
515 | error_logger = nil,
516 | }, {
517 | __index = _config_private,
518 | }
519 | ),
520 | }
521 |
522 | for k, v in pairs(App) do
523 | A[k] = v
524 | end
525 |
526 | ngx.ctx.estrela = A
527 |
528 | A.error:clean()
529 |
530 | A.router = Router:new(routes)
531 |
532 | return A
533 | end
534 |
535 | setmetatable(M, {
536 | __call = function(self, routes)
537 | return self:new(routes)
538 | end,
539 | })
540 |
541 | return M
542 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/request.lua:
--------------------------------------------------------------------------------
1 | local io_tmpfile = io.tmpfile
2 | local require = require
3 | local setmetatable = setmetatable
4 | local table_concat = table.concat
5 | local table_insert = table.insert
6 | local tonumber = tonumber
7 |
8 | local ngx_get_headers = ngx.req.get_headers
9 | local ngx_get_post_args = ngx.req.get_post_args
10 | local ngx_get_uri_args = ngx.req.get_uri_args
11 | local ngx_read_body = ngx.req.read_body
12 |
13 | local S = require('estrela.util.string')
14 | local S_cmpi = S.cmpi
15 | local S_parse_header_value = S.parse_header_value
16 |
17 | local M = {}
18 |
19 | local function parsePostBody(timeout)
20 | local POST, FILES = {}, {}
21 |
22 | local body_len = ngx.var.http_content_length
23 | if not body_len then
24 | return POST, FILES
25 | end
26 |
27 | local body_len = tonumber(body_len) or 0
28 | if body_len == 0 then
29 | return POST, FILES
30 | end
31 |
32 | local app = ngx.ctx.estrela
33 |
34 | local timeout = tonumber(timeout) or 1000
35 |
36 | local chunk_size = body_len > 102400 and 102400 or 8192
37 |
38 | local UPLOAD = require('resty.upload')
39 | local form, _ = UPLOAD:new(chunk_size)
40 | if form then
41 | form:set_timeout(timeout)
42 |
43 | local field = {}
44 |
45 | while true do
46 | local type_, res, err = form:read()
47 | if not type_ then
48 | app.log.err('failed to read: ' .. err)
49 | break
50 | end
51 |
52 | if type_ == 'header' then
53 | local hdr_key, hdr_body = res[1], S_parse_header_value(res[2])
54 | if S_cmpi(hdr_key, 'Content-Disposition') then
55 | field.name = hdr_body.name
56 | if hdr_body.filename then
57 | field.filename = hdr_body.filename
58 | field.size = 0
59 | else
60 | field.data = {}
61 | end
62 | elseif S_cmpi(hdr_key, 'Content-Type') then
63 | field.type = hdr_body['']
64 | -- else -- игнорируем другие заголовки
65 | end
66 | elseif type_ == 'body' then
67 | local res_len = #res
68 | if res_len > 0 then
69 | if field.data then
70 | table_insert(field.data, res)
71 | else
72 | if not field.tmpfile then
73 | field.fd = io_tmpfile()
74 | end
75 | field.fd:write(res)
76 | field.size = field.size + res_len
77 | end
78 | end
79 | end
80 |
81 | if type_ == 'part_end' then
82 | if field.filename then
83 | if field.fd then
84 | field.fd:seek('set', 0)
85 | end
86 | FILES[field.name] = field
87 | else
88 | field.data = table_concat(field.data)
89 | POST[field.name] = field
90 | end
91 | field = {}
92 | elseif type_ == 'eof' then
93 | break
94 | end
95 | end
96 | else
97 | ngx_read_body()
98 | POST = ngx_get_post_args()
99 | end
100 |
101 | return POST, FILES
102 | end
103 |
104 | function M:new()
105 | local POST, FILES = parsePostBody()
106 |
107 | local BODY = setmetatable(
108 | {
109 | POST = POST,
110 | FILES = FILES,
111 | }, {
112 | __index = function(me, key)
113 | return me.POST[key] or me.FILES[key]
114 | end,
115 | }
116 | )
117 |
118 | local R = {
119 | url = ngx.var.request_uri,
120 | path = ngx.var.uri,
121 | method = ngx.var.request_method,
122 | headers = ngx_get_headers(),
123 |
124 | GET = ngx_get_uri_args(),
125 | COOKIE = S_parse_header_value(ngx.var.http_cookie or ''),
126 |
127 | BODY = BODY,
128 | POST = POST,
129 | FILES = FILES,
130 | }
131 |
132 | local mt = {
133 | __index = function(_, key)
134 | return ngx.var[key]
135 | end,
136 | }
137 |
138 | return setmetatable(R, mt)
139 | end
140 |
141 | return M
142 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/response.lua:
--------------------------------------------------------------------------------
1 | local COOKIE = require('estrela.ngx.response.cookie')
2 |
3 | local OB = require('estrela.io.ob')
4 | local OB_print = OB.print
5 | local OB_println = OB.println
6 |
7 | local M = {}
8 |
9 | function M:new()
10 | return {
11 | headers = ngx.header,
12 | COOKIE = COOKIE:new(),
13 |
14 | finish = ngx.eof,
15 | write = OB_print,
16 | writeln = OB_println,
17 | }
18 | end
19 |
20 | return M
21 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/response/cookie.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local table_concat = table.concat
3 | local table_insert = table.insert
4 | local tostring = tostring
5 | local type = type
6 |
7 | local ngx_cookie_time = ngx.cookie_time
8 |
9 | local M = {}
10 |
11 | local function setCookie(cookie, append)
12 | if not (cookie.name and cookie.value and #cookie.name > 0) then
13 | return nil, 'missing name or value'
14 | end
15 |
16 | if cookie['max-age'] then
17 | cookie['max_age'], cookie['max-age'] = cookie['max-age'], nil
18 | end
19 |
20 | if type(cookie.expires) == 'number' then
21 | cookie.expires = ngx_cookie_time(cookie.expires)
22 | end
23 |
24 | local cookie_str = {cookie.name .. '=' .. cookie.value}
25 | cookie.name, cookie.value = nil, nil
26 |
27 | local attrs = {
28 | max_age = 'max-age',
29 | }
30 | for k, v in pairs(cookie) do
31 | if v and type(v) ~= 'function' then
32 | if type(v) == 'boolean' then
33 | v = v and '' or nil
34 | else
35 | v = '=' .. tostring(v)
36 | end
37 |
38 | if v then
39 | k = tostring(attrs[k] or k)
40 | table_insert(cookie_str, k .. v)
41 | end
42 | end
43 | end
44 |
45 | cookie_str = table_concat(cookie_str, '; ')
46 |
47 | if not append then
48 | ngx.header['Set-Cookie'] = nil
49 | end
50 |
51 | if not ngx.header['Set-Cookie'] then
52 | ngx.header['Set-Cookie'] = {cookie_str}
53 | else
54 | if type(ngx.header['Set-Cookie']) == 'string' then
55 | ngx.header['Set-Cookie'] = {cookie_str, ngx.header['Set-Cookie']}
56 | else
57 | local cookies = ngx.header['Set-Cookie']
58 | table_insert(cookies, cookie_str)
59 | ngx.header['Set-Cookie'] = cookies
60 | end
61 | end
62 | end
63 |
64 | -- http://tools.ietf.org/html/rfc6265
65 |
66 | function M:new()
67 | local C = {}
68 |
69 | function C:set(cookie, value, expires, path, domain, secure, httponly, append)
70 | if type(cookie) ~= 'table' then
71 | cookie = {
72 | name = cookie,
73 | value = value,
74 | expires = expires,
75 | domain = domain,
76 | path = path,
77 | secure = secure,
78 | httponly = httponly,
79 | }
80 | else
81 | -- если передается таблица, то append идет третьим параметром
82 | append = value
83 | end
84 |
85 | return setCookie(cookie, append)
86 | end
87 |
88 | function C:empty(map)
89 | local cookie = {
90 | name = '',
91 | value = '',
92 | expires = nil,
93 | max_age = nil,
94 | domain = nil,
95 | path = nil,
96 | secure = nil,
97 | httponly = nil,
98 |
99 | set = function(me, append)
100 | return self:set(me, append)
101 | end,
102 | }
103 |
104 | if map then
105 | for k, v in pairs(map) do
106 | cookie[k] = v
107 | end
108 | end
109 |
110 | return cookie
111 | end
112 |
113 | return C
114 | end
115 |
116 | return M
117 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/router.lua:
--------------------------------------------------------------------------------
1 | local coroutine_wrap = coroutine.wrap
2 | local coroutine_yield = coroutine.yield
3 | local pairs = pairs
4 | local require = require
5 | local table_insert = table.insert
6 | local table_sort = table.sort
7 | local type = type
8 |
9 | local ngx_gsub = ngx.re.gsub
10 | local ngx_match = ngx.re.match
11 |
12 | local S = require('estrela.util.string')
13 | local S_rtrim = S.rtrim
14 |
15 | local T = require('estrela.util.table')
16 | local T_contains = T.contains
17 |
18 | local name_regexp = ':([_a-zA-Z0-9]+)'
19 |
20 | local function _preprocessRoutes(routes)
21 | local routes_urls, routes_codes = {}, {}
22 |
23 | local function _prefixSimplify(prefix)
24 | local pref = ngx_gsub(prefix, name_regexp, ' ', 'jo')
25 | return pref
26 | end
27 |
28 | local function _prefix2regexp(prefix)
29 | local re = ngx_gsub(prefix, name_regexp, '(?<$1>[^/]+)', 'jo')
30 | -- если в регулярке указывается ограничитель по концу строки, то добавляю опциональный /? перед концом
31 | -- после этого регулярка '/foo$' будет подходить и для '/foo', и для '/foo/'
32 | re = ngx_gsub(re, [[\$$]], [[/?$$]], 'jo')
33 | return '^'..re
34 | end
35 |
36 | local function _addPrefix(prefix, cb, name)
37 | local prefixType = type(prefix)
38 | if prefixType == 'string' then
39 | table_insert(routes_urls, {
40 | cb = cb,
41 | prefixShort = _prefixSimplify(prefix),
42 | prefix = prefix,
43 | name = name or nil,
44 | re = _prefix2regexp(prefix)
45 | })
46 | elseif prefixType == 'number' then
47 | table_insert(routes_codes, {
48 | cb = cb,
49 | prefix = prefix,
50 | name = name or nil,
51 | })
52 | end
53 | end
54 |
55 | for prefix, cb in pairs(routes) do
56 | if type(prefix) == 'table' then
57 | for name, pref in pairs(prefix) do
58 | _addPrefix(pref, cb, name)
59 | end
60 | else
61 | _addPrefix(prefix, cb)
62 | end
63 | end
64 |
65 | table_sort(routes_urls, function(a, b)
66 | return a.prefixShort > b.prefixShort
67 | end)
68 |
69 | return routes_urls, routes_codes
70 | end
71 |
72 | local Router = {}
73 |
74 | function Router:getFullUrl(url)
75 | return self.path_prefix .. url
76 | end
77 |
78 | function Router:mount(prefix, routes)
79 | self.routes_urls, self.routes_codes = nil, nil
80 |
81 | for _prefix, cb in pairs(routes) do
82 | local _prefix_type = type(_prefix)
83 | if _prefix_type == 'string' then
84 | self.routes[prefix .. _prefix] = cb
85 | elseif _prefix_type == 'number' then
86 | self.routes[_prefix] = cb
87 | elseif _prefix_type == 'table' then
88 | local _new_prefix = {}
89 | for name, _sub_prefix in pairs(_prefix) do
90 | _new_prefix[name] = prefix .. _sub_prefix
91 | end
92 | self.routes[_new_prefix] = cb
93 | end
94 | end
95 | end
96 |
97 | function Router:route(pathFull)
98 | local app = ngx.ctx.estrela
99 |
100 | if not self.routes_urls then
101 | self.routes_urls, self.routes_codes = _preprocessRoutes(self.routes)
102 | end
103 |
104 | local path = pathFull
105 | if self.path_prefix then
106 | path = path:sub(self.path_prefix:len() + 1)
107 | end
108 |
109 | local method = app.req.method
110 |
111 | local function check_method(route)
112 | for k, cb in pairs(route) do
113 | if type(k) == 'table' then
114 | if T_contains(k, method) then
115 | return cb
116 | end
117 | elseif method == k:upper() then
118 | return cb
119 | end
120 | end
121 | return nil
122 | end
123 |
124 | return coroutine_wrap(function()
125 | for _, p in ipairs(self.routes_urls) do
126 | local captures = ngx_match(path, p.re, 'jo')
127 | if captures then
128 | local cb = p.cb
129 |
130 | if type(cb) == 'string' then
131 | cb = require(cb)
132 | end
133 |
134 | if type(cb) == 'table' then
135 | cb = check_method(cb)
136 | end
137 |
138 | if cb then
139 | coroutine_yield {
140 | prefix = p.prefix,
141 | cb = cb,
142 | params = captures,
143 | name = p.name,
144 | path = path,
145 | pathFull = pathFull,
146 | }
147 | end
148 | end
149 | end
150 | end)
151 | end
152 |
153 | function Router:getByName(name)
154 | if not self.routes_urls then
155 | self.routes_urls, self.routes_codes = _preprocessRoutes(self.routes)
156 | end
157 |
158 | local name_type = type(name)
159 | if name_type == 'string' then
160 | for _,p in pairs(self.routes_urls) do
161 | if p.name == name then
162 | return p
163 | end
164 | end
165 | elseif name_type == 'number' then
166 | for _,p in pairs(self.routes_codes) do
167 | if (p.prefix == name) or (p.name == name) then
168 | return p
169 | end
170 | end
171 | end
172 | end
173 |
174 | function Router:urlFor(name, params)
175 | params = params or {}
176 |
177 | local route = self:getByName(name)
178 | if not route then
179 | return nil
180 | end
181 |
182 | local url = ngx_gsub(route.prefix, name_regexp, function(m) return params[m[1]] or '' end, 'jo')
183 | return self:getFullUrl(S_rtrim(url, '$'))
184 | end
185 |
186 | local M = {}
187 |
188 | function M:new(routes)
189 | local R = {
190 | routes = routes,
191 | routes_urls = nil,
192 | routes_codes = nil,
193 | path_prefix = '',
194 | }
195 |
196 | for k, v in pairs(Router) do
197 | R[k] = v
198 | end
199 |
200 | return R
201 | end
202 |
203 | return M
204 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/session/engine/common.lua:
--------------------------------------------------------------------------------
1 | local error = error
2 | local math_random = math.random
3 | local math_randomseed = math.randomseed
4 | local type = type
5 |
6 | local ngx_md5 = ngx.md5
7 | local ngx_now = ngx.now
8 | local ngx_sleep = ngx.sleep
9 | local ngx_time = ngx.time
10 |
11 | local T = require('estrela.util.table')
12 | local T_clone = T.clone
13 |
14 | local M = {}
15 |
16 | function M:new(storage, encoder, decoder)
17 | local S = {
18 | data = nil,
19 | storage = storage,
20 | }
21 |
22 | function S:new()
23 | return self
24 | end
25 |
26 | function S:start()
27 | if self.data then
28 | return false -- сессия уже запущена
29 | end
30 |
31 | math_randomseed(ngx_time())
32 |
33 | local app = ngx.ctx.estrela
34 |
35 | self.ttl = app.config:get('session.handler.cookie.params.ttl', 86400)
36 |
37 | self.key_name = app.config:get('session.handler.key_name', 'estrela_sid')
38 | self.storage_key_prefix = app.config:get('session.handler.storage_key_prefix', 'estrela_session:')
39 | self.storage_lock_ttl = app.config:get('session.handler.storage_lock_ttl', 10)
40 | self.storage_lock_timeout = app.config:get('session.handler.storage_lock_timeout', 2)
41 |
42 | self.sessid = app.req.COOKIE[self.key_name]
43 | if type(self.sessid) == 'table' then
44 | self.sessid = self.sessid[1]
45 | end
46 |
47 | if not self.sessid then
48 | self:_create()
49 | end
50 |
51 | if not self.sessid then
52 | return error('Cannot start session', 2)
53 | end
54 |
55 | self:_update_session_cookie()
56 |
57 | self.storage_key_lock = self:_get_storage_key() .. ':lock'
58 |
59 | if not self:_storage_lock() then
60 | return error('Session storage is locked', 2)
61 | end
62 |
63 | return self:_load()
64 | end
65 |
66 | function S:stop()
67 | if not self.sessid then
68 | return
69 | end
70 |
71 | local storage_key = self:_get_storage_key()
72 |
73 | local res = self.storage:set(storage_key, encoder(self.data), self.ttl)
74 | self:_storage_unlock()
75 | self.data, self.sessid = nil, nil
76 | return res
77 | end
78 |
79 | function S:delete()
80 | local storage_key = self:_get_storage_key()
81 | local res = self.storage:delete(storage_key)
82 | self.sessid, self.data = nil, nil
83 | self:_storage_unlock()
84 | return res
85 | end
86 |
87 | function S:gc()
88 | return self.storage:flush_expired(1000)
89 | end
90 |
91 | function S:_create()
92 | self.data = {}
93 | local encoded_data = encoder(self.data)
94 | local tries = 10
95 | while tries > 0 do
96 | self.sessid = self:_gen_sessid()
97 | local storage_key = self:_get_storage_key()
98 | if self.storage:add(storage_key, encoded_data, self.ttl) then
99 | return self.data
100 | end
101 |
102 | tries = tries - 1
103 | end
104 |
105 | self.data, self.sessid = nil, nil
106 | return nil
107 | end
108 |
109 | function S:_load()
110 | local storage_key = self:_get_storage_key()
111 | self.data = self.storage:get(storage_key)
112 | if self.data then
113 | self.data = decoder(self.data)
114 | end
115 | if not self.data then
116 | self.data = {}
117 | end
118 |
119 | return self.data
120 | end
121 |
122 | function S:_gen_sessid()
123 | return ngx_md5(
124 | ngx.var.remote_addr .. ngx.var.pid .. ngx_now() .. ngx.var.connection .. math_random()
125 | ):sub(1, 16)
126 | end
127 |
128 | function S:_get_storage_key()
129 | return self.storage_key_prefix .. self.sessid
130 | end
131 |
132 | function S:_storage_lock()
133 | local lock_ttl = self.storage_lock_ttl
134 | local lock_timeout = self.storage_lock_timeout
135 | local try_until = ngx_now() + lock_timeout
136 | local locked
137 | while true do
138 | locked = self.storage:add(self.storage_key_lock, 1, lock_ttl)
139 | if locked or (try_until < ngx_now()) then
140 | break
141 | end
142 | ngx_sleep(0.01)
143 | end
144 |
145 | return locked
146 | end
147 |
148 | function S:_storage_unlock()
149 | return self.storage:delete(self.storage_key_lock)
150 | end
151 |
152 | function S:_update_session_cookie()
153 | local app = ngx.ctx.estrela
154 | if ngx.headers_sent then
155 | return app.log.err('Error saving the session cookie: headers already sent')
156 | else
157 | local cookie = T_clone(app.config:get('session.handler.cookie.params', {}))
158 | cookie.name = self.key_name
159 | cookie.value = self.sessid
160 | if cookie.ttl then
161 | cookie.expires = ngx_time() + cookie.ttl
162 | cookie.ttl = nil
163 | end
164 |
165 | app.resp.COOKIE:empty(cookie):set()
166 | end
167 | end
168 |
169 | return S:new()
170 | end
171 |
172 | return M
173 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/tmpl/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Error {{code}}
6 |
7 |
8 | Ooops! Page {{self.req.path}} is not found
9 | {{descr}}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/estrela/ngx/tmpl/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Estrelas cadentes {{code}}
6 |
7 |
8 | Ooops! Something went wrong
9 | {{descr}}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/estrela/oop/nano.lua:
--------------------------------------------------------------------------------
1 | local pairs = pairs
2 | local setmetatable = setmetatable
3 |
4 | local M = {}
5 |
6 | function M.class(struct)
7 | struct = struct or {}
8 |
9 | local cls = {}
10 |
11 | setmetatable(cls, {
12 | __call = function(self, ...)
13 | local inst = {}
14 |
15 | for k, v in pairs(struct) do
16 | inst[k] = v
17 | end
18 |
19 | local new = inst.new
20 | if new then
21 | new(inst, ...)
22 | end
23 |
24 | return inst
25 | end,
26 | })
27 |
28 | return cls
29 | end
30 |
31 | return M
32 |
--------------------------------------------------------------------------------
/lib/estrela/oop/single.lua:
--------------------------------------------------------------------------------
1 | local debug = require('debug')
2 | local debug_getinfo = debug.getinfo
3 | local getmetatable = getmetatable
4 | local pairs = pairs
5 | local rawget = rawget
6 | local rawset = rawset
7 | local require = require
8 | local setmetatable = setmetatable
9 | local tostring = tostring
10 | local type = type
11 |
12 | local M = {}
13 |
14 | local function super_func(self, ...)
15 | local frame = debug_getinfo(2)
16 | local mt = getmetatable(self)
17 | assert(mt and mt.__base, 'There are no super method')
18 |
19 | local func = mt.__base[frame.name]
20 | return func and func(self, ...) or nil
21 | end
22 |
23 | function M.class(name, struct, parent)
24 | if type(name) ~= 'string' then
25 | name, struct, parent = nil, name, struct
26 | end
27 |
28 | if type(parent) == 'string' then
29 | parent = require(parent)
30 | end
31 |
32 | local cls = {}
33 |
34 | name = name or ('Cls' .. tostring(cls):sub(10))
35 |
36 | struct = struct or {}
37 |
38 | -- создание новой инстанции класса без вызова конструкторов
39 | local function _create_inst()
40 | local base = parent and parent:create() or nil
41 |
42 | local inst = {}
43 |
44 | setmetatable(inst, {
45 | __base = base,
46 | __index = setmetatable(
47 | {
48 | super = super_func,
49 | }, {
50 | __index = function(_, key)
51 | local idx_func = rawget(inst, '__index__')
52 | if idx_func then
53 | return idx_func(inst, key)
54 | end
55 | end,
56 | }
57 | ),
58 | })
59 |
60 | if base then
61 | for k, v in pairs(base) do
62 | inst[k] = v
63 | end
64 | end
65 |
66 | for k, v in pairs(struct) do
67 | inst[k] = v
68 | end
69 |
70 | inst.__class = cls
71 |
72 | return inst
73 | end
74 |
75 | setmetatable(cls, {
76 | __index = setmetatable(
77 | {
78 | __name = name,
79 | name = M.name,
80 | subclass = M.subclass,
81 | create = _create_inst,
82 | }, {
83 | __index = function(_, key)
84 | if parent then
85 | return parent[key]
86 | end
87 | end,
88 | }
89 | ),
90 | __newindex = function(tbl, key, val)
91 | if key ~= 'name' then
92 | rawset(tbl, key, val)
93 | end
94 | end,
95 | __call = function(cls, ...)
96 | local inst = cls:create()
97 |
98 | local new = inst.new
99 | if new then
100 | new(inst, ...)
101 | end
102 |
103 | return inst
104 | end,
105 | })
106 |
107 | return cls
108 | end
109 |
110 | function M.subclass(parent, name)
111 | return function(struct)
112 | return name and M.class(name, struct, parent) or M.class(struct, parent)
113 | end
114 | end
115 |
116 | function M.name(name)
117 | return {
118 | class = function(struct, parent)
119 | return M.class(name, struct)
120 | end,
121 |
122 | subclass = function(parent)
123 | return function(struct)
124 | return M.class(name, struct, parent)
125 | end
126 | end,
127 | }
128 | end
129 |
130 | return M
131 |
--------------------------------------------------------------------------------
/lib/estrela/storage/engine/common.lua:
--------------------------------------------------------------------------------
1 | local error = error
2 |
3 | local M = {}
4 |
5 | function M:new()
6 | local E = {}
7 |
8 | function E:new()
9 | return self
10 | end
11 |
12 | function E:get(key, stale)
13 | return error('Not implemented')
14 | end
15 |
16 | function E:set(key, val, ttl)
17 | return error('Not implemented')
18 | end
19 |
20 | function E:add(key, val, ttl)
21 | return error('Not implemented')
22 | end
23 |
24 | function E:replace(key, val, ttl)
25 | return error('Not implemented')
26 | end
27 |
28 | function E:incr(key, by)
29 | return error('Not implemented')
30 | end
31 |
32 | function E:delete(key)
33 | return error('Not implemented')
34 | end
35 |
36 | function E:exists(key)
37 | return error('Not implemented')
38 | end
39 |
40 | return E:new()
41 | end
42 |
43 | return M
44 |
--------------------------------------------------------------------------------
/lib/estrela/storage/engine/shmem.lua:
--------------------------------------------------------------------------------
1 | local error = error
2 | local require = require
3 | local tostring = tostring
4 |
5 | local M = {}
6 |
7 | function M:new(shared_var)
8 | if not shared_var or not ngx.shared[shared_var] then
9 | return error('There are no defined ngx.shared[' .. tostring(shared_var) .. ']')
10 | end
11 |
12 | local E = require('estrela.storage.engine.common'):new()
13 |
14 | function E:new()
15 | self.shmem = ngx.shared[shared_var]
16 | return self
17 | end
18 |
19 | function E:get(key, stale)
20 | local func = stale and self.shmem.get_stale or self.shmem.get
21 | return func(self.shmem, key)
22 | end
23 |
24 | function E:set(key, val, ttl)
25 | return self.shmem:set(key, val, ttl or 0)
26 | end
27 |
28 | function E:add(key, val, ttl)
29 | return self.shmem:add(key, val, ttl or 0)
30 | end
31 |
32 | function E:replace(key, val, ttl)
33 | return self.shmem:replace(key, val, ttl or 0)
34 | end
35 |
36 | function E:incr(key, by)
37 | return self.shmem:incr(key, by or 1)
38 | end
39 |
40 | function E:delete(key)
41 | return self.shmem:delete(key)
42 | end
43 |
44 | function E:exists(key)
45 | return self:get(key) ~= nil
46 | end
47 |
48 | return E:new()
49 | end
50 |
51 | return M
52 |
--------------------------------------------------------------------------------
/lib/estrela/util/env.lua:
--------------------------------------------------------------------------------
1 | local debug = require('debug')
2 | local debug_getinfo = debug.getinfo
3 | local string_len = string.len
4 |
5 | local S = require('estrela.util.string')
6 | local S_ltrim = S.ltrim
7 | local S_rtrim = S.rtrim
8 |
9 | local path_split = require('estrela.util.path').split
10 |
11 | local M = {}
12 |
13 | local _lib_root
14 |
15 | function M.get_root()
16 | return _lib_root
17 | end
18 |
19 | local function _setup_env()
20 | local _path = S_ltrim(debug_getinfo(1).source, '@')
21 | _path = S_rtrim(path_split(_path).path, [[/]])
22 | _lib_root = _path:sub(1, -string_len('/util')-1)
23 | end
24 |
25 | _setup_env()
26 |
27 | return M
28 |
--------------------------------------------------------------------------------
/lib/estrela/util/path.lua:
--------------------------------------------------------------------------------
1 | local ipairs = ipairs
2 | local math_min = math.min
3 | local table_concat = table.concat
4 | local table_insert = table.insert
5 |
6 | local S = require('estrela.util.string')
7 | local S_find_last = S.find_last
8 | local S_rtrim = S.rtrim
9 | local S_trim = S.trim
10 | local S_split = S.split
11 |
12 | local M = {}
13 |
14 | -- *nix only
15 |
16 | function M.split(path)
17 | local name, ext
18 |
19 | local slash_pos = S_find_last(path, '/')
20 | if slash_pos then
21 | name = path:sub(slash_pos + 1)
22 | path = path:sub(1, slash_pos)
23 | else
24 | path, name = '', path
25 | end
26 |
27 | local dot_pos = S_find_last(name, '.')
28 | ext = dot_pos and name:sub(dot_pos + 1) or ''
29 |
30 | return {
31 | path = path,
32 | name = name,
33 | ext = ext,
34 | }
35 | end
36 |
37 | function M.rel(base, path)
38 | local base = S_trim(base, '/')
39 | local path = S_trim(path, '/')
40 | local _base = (#base > 0) and S_split(base, '/') or {}
41 | local _path = (#path > 0) and S_split(path, '/') or {}
42 |
43 | local rel = {}
44 | local common_len = 0
45 | for p = 1, math_min(#_path, #_base) do
46 | if _base[p] == _path[p] then
47 | common_len = p
48 | else
49 | break
50 | end
51 | end
52 |
53 | for _ = 1, (#_base - common_len) do
54 | table_insert(rel, '..')
55 | end
56 |
57 | for p = common_len + 1, #_path do
58 | table_insert(rel, _path[p])
59 | end
60 |
61 | rel = table_concat(rel, '/')
62 |
63 | return rel, (#_base - common_len)
64 | end
65 |
66 | function M.join(path, ...)
67 | local dirs = {...}
68 | if #dirs == 0 then
69 | return path
70 | end
71 |
72 | local path = {S_rtrim(path, '/')}
73 |
74 | for _, dir in ipairs(dirs) do
75 | table_insert(path, S_trim(dir, '/'))
76 | end
77 |
78 | return table_concat(path, '/')
79 | end
80 |
81 | return M
82 |
--------------------------------------------------------------------------------
/lib/estrela/util/string.lua:
--------------------------------------------------------------------------------
1 | local getmetatable = getmetatable
2 | local loadstring = loadstring
3 | local pairs = pairs
4 | local pcall = pcall
5 | local string_byte = string.byte
6 | local string_find = string.find
7 | local string_format = string.format
8 | local table_insert = table.insert
9 | local type = type
10 | local unpack = unpack
11 |
12 | local T_list = require('estrela.util.table').list
13 |
14 | local M = {}
15 |
16 | function M.find_first(str, sub, start, stop)
17 | stop = stop or #str
18 | start = start or 1
19 | local p = string_find(str, sub, start, true)
20 | return (p and p <= stop) and p or nil
21 | end
22 |
23 | function M.find_last(str, sub, start, stop)
24 | stop = stop or #str
25 | start = start or 1
26 |
27 | local last_pos
28 | while true do
29 | local f, t = string_find(str, sub, start, true)
30 | if not f or f > stop then
31 | break
32 | end
33 |
34 | last_pos = f
35 | start = t + 1
36 | end
37 |
38 | return last_pos
39 | end
40 |
41 | function M.split(str, sep, maxsplit, sep_regex)
42 | if type(sep) ~= 'string' or #sep < 1 then
43 | return T_list(str:gmatch('.'))
44 | end
45 |
46 | local plain = not sep_regex
47 |
48 | if not string_find(str, sep, 1, plain) then
49 | return {str}
50 | end
51 |
52 | if not maxsplit or maxsplit < 1 then
53 | maxsplit = #str
54 | end
55 |
56 | local res = {}
57 |
58 | local prev_pos = 1
59 | while #res < maxsplit - 1 do
60 | local pos, pos_t = string_find(str, sep, prev_pos, plain)
61 | if not pos then
62 | res[#res + 1] = str:sub(prev_pos)
63 | prev_pos = #str + 1
64 | break
65 | end
66 |
67 | res[#res + 1] = str:sub(prev_pos, pos - 1)
68 | prev_pos = pos_t + 1
69 | end
70 |
71 | if prev_pos <= #str then
72 | res[#res + 1] = str:sub(prev_pos)
73 | end
74 |
75 | return res
76 | end
77 |
78 | function M.trim(str, chars)
79 | if type(chars) == 'string' and #chars == 0 then
80 | return str
81 | end
82 |
83 | chars = chars or '%s'
84 | return M.ltrim(M.rtrim(str, chars), chars)
85 | end
86 |
87 | function M.rtrim(str, chars)
88 | if type(chars) == 'string' and #chars == 0 then
89 | return str
90 | end
91 |
92 | chars = chars or '%s'
93 | local res = str:gsub('['..chars..']+$', '')
94 | return res
95 | end
96 |
97 | function M.ltrim(str, chars)
98 | if type(chars) == 'string' and #chars == 0 then
99 | return str
100 | end
101 |
102 | chars = chars or '%s'
103 | local res = str:gsub('^['..chars..']+', '')
104 | return res
105 | end
106 |
107 | function M.htmlencode(str, withQuotes)
108 | str = str
109 | :gsub([[&]], [[&]])
110 | :gsub([[<]], [[<]])
111 | :gsub([[>]], [[>]])
112 |
113 | if withQuotes then
114 | str = str
115 | :gsub([["]], [["]])
116 | :gsub([[']], [[']])
117 | end
118 |
119 | return str
120 | end
121 |
122 | function M.starts(str, prefix)
123 | return prefix:len() == 0 or string_find(str, prefix, 1, true) == 1
124 | end
125 |
126 | function M.ends(str, suffix)
127 | local sl = suffix:len()
128 | return sl == 0 or string_find(str, suffix, -sl, true) == (str:len() - sl + 1)
129 | end
130 |
131 | --[[ Разбор значений HTTP заголовков на компоненты
132 | ]]
133 | function M.parse_header_value(str)
134 | if #str == 0 then
135 | return {}
136 | end
137 |
138 | local trim = M.trim
139 |
140 | local ST_KEY = 1
141 | local ST_VAL = 2
142 | local ST_VAL_WAIT_QUOTE = 3
143 | local ST_VAL_SLASHED = 4
144 |
145 | local SEMI = string_byte(';')
146 | local EQUAL = string_byte('=')
147 | local QUOTE = string_byte('"')
148 | local SLASH = string_byte([[\]])
149 |
150 | local map = {}
151 |
152 | local function to_map(k, v)
153 | k = trim(k)
154 |
155 | if not v then
156 | k, v = '', k
157 | else
158 | v = trim(v)
159 | end
160 |
161 | if string_byte(v) == QUOTE then
162 | v = loadstring('return '..v)
163 | if v then
164 | local ok, res = pcall(v)
165 | v = ok and res or nil
166 | end
167 | v = v or ''
168 | end
169 |
170 | if map[k] then
171 | if type(map[k]) ~= 'table' then
172 | map[k] = {map[k]}
173 | end
174 | table_insert(map[k], v)
175 | else
176 | map[k] = trim(v)
177 | end
178 | end
179 |
180 | local state = ST_KEY
181 | local sl = #str
182 | local pos = 1
183 | local last_pos = 1
184 | local key
185 |
186 | while pos <= sl do
187 | local ch = string_byte(str, pos)
188 |
189 | if state == ST_KEY then
190 | if ch == SEMI then
191 | if pos > last_pos then
192 | to_map(str:sub(last_pos, pos - 1), nil)
193 | end
194 | last_pos = pos + 1
195 | elseif ch == EQUAL then
196 | key = str:sub(last_pos, pos - 1)
197 | last_pos = pos + 1
198 | state = ST_VAL
199 | end
200 | elseif state == ST_VAL then
201 | if ch == SEMI then
202 | to_map(key, str:sub(last_pos, pos - 1))
203 | key = nil
204 | last_pos = pos + 1
205 | state = ST_KEY
206 | elseif ch == QUOTE then
207 | last_pos = pos
208 | state = ST_VAL_WAIT_QUOTE
209 | end
210 | elseif state == ST_VAL_WAIT_QUOTE then
211 | if ch == QUOTE then
212 | to_map(key, str:sub(last_pos, pos))
213 | key = nil
214 | last_pos = pos + 1
215 | state = ST_KEY
216 | elseif ch == SLASH then
217 | state = ST_VAL_SLASHED
218 | end
219 | elseif state == ST_VAL_SLASHED then
220 | state = ST_VAL_WAIT_QUOTE
221 | end
222 |
223 | pos = pos + 1
224 | end
225 |
226 | local val = last_pos <= sl and str:sub(last_pos, sl) or nil
227 | if not key then
228 | key, val = val, nil
229 | end
230 |
231 | if key then
232 | to_map(key, val)
233 | end
234 |
235 | return map
236 | end
237 |
238 | function M.cmpi(str, str2)
239 | return str:lower() == str2:lower()
240 | end
241 |
242 | function M.format(fmt, vars)
243 | if type(vars) == 'table' then
244 | return string_format(fmt, unpack(vars))
245 | else
246 | return string_format(fmt, vars)
247 | end
248 | end
249 |
250 | function M.apply_patch()
251 | local mt = getmetatable('')
252 | for k, v in pairs(M) do
253 | mt.__index[k] = v
254 | end
255 | mt.__mod = M.format
256 | end
257 |
258 | return M
259 |
--------------------------------------------------------------------------------
/lib/estrela/util/table.lua:
--------------------------------------------------------------------------------
1 | local coroutine_wrap = coroutine.wrap
2 | local coroutine_yield = coroutine.yield
3 | local getmetatable = getmetatable
4 | local ipairs = ipairs
5 | local pairs = pairs
6 | local setmetatable = setmetatable
7 | local table_concat = table.concat
8 | local table_insert = table.insert
9 | local type = type
10 |
11 | local M = {}
12 |
13 | local function wrap_gen_func(gen)
14 | local gen = coroutine_wrap(gen)
15 |
16 | return setmetatable({}, {
17 | __index = function(self, key)
18 | return (key == 'list') and M.list or nil
19 | end,
20 | __call = function(...)
21 | return gen(...)
22 | end,
23 | })
24 | end
25 |
26 | function M.push(tbl, ...)
27 | for _, v in ipairs{...} do
28 | table_insert(tbl, v)
29 | end
30 |
31 | return tbl
32 | end
33 |
34 | function M.clone(tbl)
35 | if type(tbl) ~= 'table' then
36 | return tbl
37 | end
38 |
39 | local copy = {}
40 | for k, v in pairs(tbl) do
41 | copy[M.clone(k)] = M.clone(v)
42 | end
43 |
44 | setmetatable(copy, M.clone(getmetatable(tbl)))
45 |
46 | return copy
47 | end
48 |
49 | function M.rep(tbl, times)
50 | local t = {}
51 | while times > 0 do
52 | table_insert(t, M.clone(tbl))
53 | times = times - 1
54 | end
55 | return t
56 | end
57 |
58 | function M.range(start, stop, step)
59 | if not stop then
60 | start, stop = 1, start
61 | end
62 |
63 | if not step then
64 | step = start <= stop and 1 or -1
65 | end
66 |
67 | return wrap_gen_func(function()
68 | for i = start, stop, step do
69 | coroutine_yield(i)
70 | end
71 | end)
72 | end
73 |
74 | function M.join(tbl, sep)
75 | return table_concat(tbl, sep)
76 | end
77 |
78 | function M.len(tbl)
79 | local cnt = 0
80 | for _, _ in pairs(tbl) do
81 | cnt = cnt + 1
82 | end
83 | return cnt
84 | end
85 |
86 | function M.list(smth)
87 | local l = {}
88 | for v in smth do
89 | table_insert(l, v)
90 | end
91 | return l
92 | end
93 |
94 | function M.contains(tbl, item)
95 | for _, v in pairs(tbl) do
96 | if v == item then
97 | return true
98 | end
99 | end
100 | return false
101 | end
102 |
103 | local MT = {
104 | __index = M,
105 | __add = M.push,
106 | __mul = M.rep,
107 | }
108 |
109 | setmetatable(M, {
110 | __call = function(cls, ...)
111 | return setmetatable({}, MT):push(...)
112 | end,
113 | })
114 |
115 | return M
116 |
--------------------------------------------------------------------------------
/lib/estrela/web.lua:
--------------------------------------------------------------------------------
1 | if not ngx then
2 | return error('[lua-nginx-module](https://github.com/openresty/lua-nginx-module) is required')
3 | end
4 |
5 | local app = require('estrela.ngx.app')
6 |
7 | return {
8 | App = app,
9 | }
10 |
--------------------------------------------------------------------------------
/lib/resty/README.md:
--------------------------------------------------------------------------------
1 | COPYRIGHT
2 | ============================================================
3 |
4 | The sources files in this directory are from:
5 | * upload.lua from [lua-resty-upload](https://github.com/openresty/lua-resty-upload)
6 |
--------------------------------------------------------------------------------
/lib/resty/upload.lua:
--------------------------------------------------------------------------------
1 | -- Copyright (C) Yichun Zhang (agentzh)
2 |
3 |
4 | local sub = string.sub
5 | local req_socket = ngx.req.socket
6 | local null = ngx.null
7 | local match = string.match
8 | local setmetatable = setmetatable
9 | local error = error
10 | local get_headers = ngx.req.get_headers
11 | local type = type
12 | -- local print = print
13 |
14 |
15 | local _M = { _VERSION = '0.09' }
16 |
17 |
18 | local MAX_LINE_SIZE = 512
19 |
20 | local STATE_BEGIN = 1
21 | local STATE_READING_HEADER = 2
22 | local STATE_READING_BODY = 3
23 | local STATE_EOF = 4
24 |
25 |
26 | local mt = { __index = _M }
27 |
28 | local state_handlers
29 |
30 |
31 | local function get_boundary()
32 | local header = get_headers()["content-type"]
33 | if not header then
34 | return nil
35 | end
36 |
37 | if type(header) == "table" then
38 | header = header[1]
39 | end
40 |
41 | local m = match(header, ";%s*boundary=\"([^\"]+)\"")
42 | if m then
43 | return m
44 | end
45 |
46 | return match(header, ";%s*boundary=([^\",;]+)")
47 | end
48 |
49 |
50 | function _M.new(self, chunk_size)
51 | local boundary = get_boundary()
52 |
53 | -- print("boundary: ", boundary)
54 |
55 | if not boundary then
56 | return nil, "no boundary defined in Content-Type"
57 | end
58 |
59 | -- print('boundary: "', boundary, '"')
60 |
61 | local sock, err = req_socket()
62 | if not sock then
63 | return nil, err
64 | end
65 |
66 | local read2boundary, err = sock:receiveuntil("--" .. boundary)
67 | if not read2boundary then
68 | return nil, err
69 | end
70 |
71 | local read_line, err = sock:receiveuntil("\r\n")
72 | if not read_line then
73 | return nil, err
74 | end
75 |
76 | return setmetatable({
77 | sock = sock,
78 | size = chunk_size or 4096,
79 | read2boundary = read2boundary,
80 | read_line = read_line,
81 | boundary = boundary,
82 | state = STATE_BEGIN
83 | }, mt)
84 | end
85 |
86 |
87 | function _M.set_timeout(self, timeout)
88 | local sock = self.sock
89 | if not sock then
90 | return nil, "not initialized"
91 | end
92 |
93 | return sock:settimeout(timeout)
94 | end
95 |
96 |
97 | local function discard_line(self)
98 | local read_line = self.read_line
99 |
100 | local line, err = self.read_line(MAX_LINE_SIZE)
101 | if not line then
102 | return nil, err
103 | end
104 |
105 | local dummy, err = self.read_line(1)
106 | if dummy then
107 | return nil, "line too long: " .. line .. dummy .. "..."
108 | end
109 |
110 | if err then
111 | return nil, err
112 | end
113 |
114 | return 1
115 | end
116 |
117 |
118 | local function discard_rest(self)
119 | local sock = self.sock
120 | local size = self.size
121 |
122 | while true do
123 | local dummy, err = sock:receive(size)
124 | if err and err ~= 'closed' then
125 | return nil, err
126 | end
127 |
128 | if not dummy then
129 | return 1
130 | end
131 | end
132 | end
133 |
134 |
135 | local function read_body_part(self)
136 | local read2boundary = self.read2boundary
137 |
138 | local chunk, err = read2boundary(self.size)
139 | if err then
140 | return nil, nil, err
141 | end
142 |
143 | if not chunk then
144 | local sock = self.sock
145 |
146 | local data = sock:receive(2)
147 | if data == "--" then
148 | local ok, err = discard_rest(self)
149 | if not ok then
150 | return nil, nil, err
151 | end
152 |
153 | self.state = STATE_EOF
154 | return "part_end"
155 | end
156 |
157 | if data ~= "\r\n" then
158 | local ok, err = discard_line(self)
159 | if not ok then
160 | return nil, nil, err
161 | end
162 | end
163 |
164 | self.state = STATE_READING_HEADER
165 | return "part_end"
166 | end
167 |
168 | return "body", chunk
169 | end
170 |
171 |
172 | local function read_header(self)
173 | local read_line = self.read_line
174 |
175 | local line, err = read_line(MAX_LINE_SIZE)
176 | if err then
177 | return nil, nil, err
178 | end
179 |
180 | local dummy, err = read_line(1)
181 | if dummy then
182 | return nil, nil, "line too long: " .. line .. dummy .. "..."
183 | end
184 |
185 | if err then
186 | return nil, nil, err
187 | end
188 |
189 | -- print("read line: ", line)
190 |
191 | if line == "" then
192 | -- after the last header
193 | self.state = STATE_READING_BODY
194 | return read_body_part(self)
195 | end
196 |
197 | local key, value = match(line, "([^: \t]+)%s*:%s*(.+)")
198 | if not key then
199 | return 'header', line
200 | end
201 |
202 | return 'header', {key, value, line}
203 | end
204 |
205 |
206 | local function eof()
207 | return "eof", nil
208 | end
209 |
210 |
211 | function _M.read(self)
212 | local size = self.size
213 |
214 | local handler = state_handlers[self.state]
215 | if handler then
216 | return handler(self)
217 | end
218 |
219 | return nil, nil, "bad state: " .. self.state
220 | end
221 |
222 |
223 | local function read_preamble(self)
224 | local sock = self.sock
225 | if not sock then
226 | return nil, nil, "not initialized"
227 | end
228 |
229 | local size = self.size
230 | local read2boundary = self.read2boundary
231 |
232 | while true do
233 | local preamble, err = read2boundary(size)
234 | if not preamble then
235 | break
236 | end
237 |
238 | -- discard the preamble data chunk
239 | -- print("read preamble: ", preamble)
240 | end
241 |
242 | local ok, err = discard_line(self)
243 | if not ok then
244 | return nil, nil, err
245 | end
246 |
247 | local read2boundary, err = sock:receiveuntil("\r\n--" .. self.boundary)
248 | if not read2boundary then
249 | return nil, nil, err
250 | end
251 |
252 | self.read2boundary = read2boundary
253 |
254 | self.state = STATE_READING_HEADER
255 | return read_header(self)
256 | end
257 |
258 |
259 | state_handlers = {
260 | read_preamble,
261 | read_header,
262 | read_body_part,
263 | eof
264 | }
265 |
266 |
267 | return _M
268 |
--------------------------------------------------------------------------------
/nginx.conf.example:
--------------------------------------------------------------------------------
1 | http {
2 | #lua_code_cache off; # раскомментировать для отладки без постоянных nginx reload
3 |
4 | lua_package_path '/path/to/lua/?.lua;/path/to/lua/lib/?.lua;;';
5 | #lua_shared_dict session_cache 100m; # для хранения сессий в оперативке
6 |
7 | server {
8 | location /estrela {
9 | content_by_lua_file /path/to/lua/index.lua;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tester.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | location /estrela_test {
3 | content_by_lua_file $document_root/../lua/tester.lua;
4 | }
5 |
6 | $ luajit tester.lua 'http://ater.me/estrela_test' ./tests
7 | ]]
8 |
9 | local function load_test(name)
10 | return require('tests.' .. name)
11 | end
12 |
13 | if ngx then
14 | return xpcall(
15 | function()
16 | local test_name = ngx.var.arg_test
17 | if not test_name then
18 | return error('Name of test is not defined')
19 | end
20 |
21 | local tester = require('tests.tester.ngx')
22 |
23 | return load_test(test_name)(tester)
24 | end,
25 |
26 | function(err)
27 | local error_header_name = require('tests.tester.common').error_header_name
28 |
29 | ngx.status = ngx.HTTP_BAD_REQUEST
30 | if #err > 1024 then
31 | err = err:sub(1, 1021) .. '...'
32 | end
33 | ngx.header[error_header_name] = err
34 | return ngx.exit(0)
35 | end
36 | )
37 | else
38 | package.path = './lib/?.lua;' .. package.path
39 |
40 | if #arg < 2 then
41 | print('Usage: ' .. arg[0] .. ' ')
42 | return os.exit(1)
43 | end
44 |
45 | local url = arg[1]
46 | local dir = arg[2]
47 |
48 | local tester = require('tests.tester.cli')
49 |
50 | local tests, err = tester.ls_tests(dir)
51 | if not tests then
52 | return error(err)
53 | end
54 |
55 | local stats = {
56 | ok = 0,
57 | fail = 0,
58 | }
59 |
60 | local max_test_name_len = 0
61 | for _, test_name in ipairs(tests) do
62 | max_test_name_len = math.max(max_test_name_len, #test_name)
63 | end
64 |
65 | local max_test_idx_digits = math.ceil(math.log10(#tests + 1e-5))
66 |
67 | local test_header_format = '#### Test #%-' .. max_test_idx_digits .. 'd %-' .. max_test_name_len .. 's - '
68 |
69 | for idx, test_name in ipairs(tests) do
70 | io.write(string.format(test_header_format, idx, test_name))
71 |
72 | tester.url = url .. '?test=' .. test_name
73 |
74 | tester.remove_cookies()
75 |
76 | local ok, res, descr = xpcall(
77 | function()
78 | local test = load_test(test_name)
79 | if type(test) ~= 'function' then
80 | -- если нет реализации теста, то используем базовую заготовку
81 | test = function(tester)
82 | local body, status, headers = tester.req(tester.url)
83 |
84 | local ok = status and (status.code == 200)
85 | local err = headers and headers[tester.error_header_name] or nil
86 |
87 | return ok, err
88 | end
89 | end
90 | return test(tester)
91 | end,
92 | function(err)
93 | return err
94 | end
95 | )
96 |
97 | if ok and res then
98 | stats.ok = stats.ok + 1
99 | print('OK')
100 | else
101 | stats.fail = stats.fail + 1
102 | print('ERROR', descr and descr or res)
103 | end
104 | end
105 |
106 | local format = [[Results:
107 | Total: %d
108 | Success: %d
109 | Failed: %d
110 | ]]
111 |
112 | print(string.format(
113 | format,
114 | stats.ok + stats.fail,
115 | stats.ok,
116 | stats.fail
117 | ))
118 |
119 | tester.remove_cookies()
120 |
121 | return os.exit(stats.fail == 0 and 0 or 1)
122 | end
123 |
--------------------------------------------------------------------------------
/tests/00_check.lua:
--------------------------------------------------------------------------------
1 | --[[ Проверка работоспособности nginx и lua_nginx в целом
2 | ]]
3 | if ngx then
4 | return function()
5 | return ngx.print(ngx.var.arg_check)
6 | end
7 | else
8 | return function(tester)
9 | math.randomseed(os.time())
10 | local check = tostring(os.time()) .. tostring(math.random())
11 | local body, status, headers = tester.req(tester.url, {check=check})
12 | return assert(body == check, 'lua_nginx check')
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/tests/01_util_table.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local T = require('estrela.util.table');
4 |
5 | -- push
6 | (function()
7 | local tbl = {}
8 | T.push(tbl, 42)
9 | assert(
10 | tester.tbl_cmpi(
11 | tbl,
12 | {42}
13 | ),
14 | 'push single'
15 | )
16 |
17 | T.push(tbl, 1, 2, 3)
18 | assert(
19 | tester.tbl_cmpi(
20 | tbl,
21 | {42, 1, 2, 3}
22 | ),
23 | 'push multiple'
24 | )
25 |
26 | T.push(tbl, {4, {5}, 6})
27 | assert(
28 | tester.tbl_cmpi(
29 | tbl,
30 | {42, 1, 2, 3, {4, {5}, 6}}
31 | ),
32 | 'push table'
33 | )
34 | end)();
35 |
36 | -- clone
37 | (function()
38 | assert(
39 | T.clone(42) == 42,
40 | 'clone non table'
41 | )
42 |
43 | local tbl = {}
44 | assert(
45 | tester.tbl_cmp(
46 | T.clone(tbl),
47 | tbl
48 | ),
49 | 'clone empty table'
50 | )
51 |
52 | local tbl = {1, 2, setmetatable({3}, {1, 2, 3})}
53 | assert(
54 | tester.tbl_cmpi(
55 | T.clone(tbl),
56 | tbl
57 | ),
58 | 'clone table with metatable'
59 | )
60 | end)();
61 |
62 | -- rep
63 | (function()
64 | local tbl = {}
65 | assert(
66 | tester.tbl_cmpi(
67 | T.rep(tbl, 3),
68 | {tbl, tbl, tbl}
69 | ),
70 | 'rep empty table'
71 | )
72 |
73 | local tbl = {42, {7}}
74 | assert(
75 | tester.tbl_cmpi(
76 | T.rep(tbl, 5),
77 | {tbl, tbl, tbl, tbl, tbl}
78 | ),
79 | 'rep table'
80 | )
81 |
82 | assert(
83 | tester.tbl_cmpi(
84 | T.rep({}, 0),
85 | {}
86 | ),
87 | 'rep zero times'
88 | )
89 | end)();
90 |
91 | -- join
92 | (function()
93 | assert(
94 | T.join({}) == '',
95 | 'join empty table w/o sep'
96 | )
97 |
98 | assert(
99 | T.join({}, '|') == '',
100 | 'join empty table w/ sep'
101 | )
102 |
103 | assert(
104 | T.join({1, 2, 3}) == '123',
105 | 'join w/o sep'
106 | )
107 |
108 | assert(
109 | T.join({1, 2, 3}, '|') == '1|2|3',
110 | 'join w/ sep'
111 | )
112 |
113 | assert(
114 | T.join({1, 2, '4'}, '|') == '1|2|4',
115 | 'join mix item types'
116 | )
117 | end)();
118 |
119 | -- len
120 | (function()
121 | assert(
122 | T.len({}) == 0,
123 | 'len empty table'
124 | )
125 |
126 | assert(
127 | T.len({1, 2, 42, 100500}) == 4,
128 | 'len table'
129 | )
130 |
131 | assert(
132 | T.len({1, 2, nil, 3}) == 3,
133 | 'len table w/ nil item'
134 | )
135 |
136 | assert(
137 | T.len({[0]=1, [7]=2, [42]=100500}) == 3,
138 | 'len separate table'
139 | )
140 | end)();
141 |
142 | -- list
143 | (function()
144 | local tbl = {1, 2, 17}
145 | local iter = coroutine.wrap(function()
146 | for _, v in ipairs(tbl) do
147 | coroutine.yield(v)
148 | end
149 | end)
150 |
151 | assert(
152 | tester.tbl_cmpi(
153 | T.list(iter),
154 | tbl
155 | ),
156 | 'list'
157 | )
158 | end)();
159 |
160 | -- range
161 | (function()
162 | assert(
163 | tester.tbl_cmpi(
164 | T.list(T.range(1, 5)),
165 | {1, 2, 3, 4, 5}
166 | ),
167 | 'range from..to'
168 | )
169 |
170 | assert(
171 | tester.tbl_cmpi(
172 | T.range(1, 5):list(),
173 | {1, 2, 3, 4, 5}
174 | ),
175 | 'range from..to to list'
176 | )
177 |
178 | assert(
179 | tester.tbl_cmpi(
180 | T.range(5):list(),
181 | {1, 2, 3, 4, 5}
182 | ),
183 | 'range to'
184 | )
185 |
186 | assert(
187 | tester.tbl_cmpi(
188 | T.range(1, 10, 2):list(),
189 | {1, 3, 5, 7, 9}
190 | ),
191 | 'range w/ step'
192 | )
193 |
194 | assert(
195 | tester.tbl_cmpi(
196 | T.range(5, 1, -2):list(),
197 | {5, 3, 1}
198 | ),
199 | 'range w/ neg step'
200 | )
201 |
202 | assert(
203 | tester.tbl_cmpi(
204 | T.range(5, 1):list(),
205 | {5, 4, 3, 2, 1}
206 | ),
207 | 'range to..from'
208 | )
209 |
210 | local idx, res = 0, 0
211 | for v in T.range(2, 10, 3) do
212 | idx = idx + 1
213 | res = res + idx * v
214 | end
215 | assert(
216 | res == (1*2 + 2*5 + 3*8),
217 | 'range for in'
218 | )
219 |
220 | assert(
221 | T.range(1, math.huge),
222 | 'range huge'
223 | )
224 |
225 | end)();
226 |
227 | -- contains
228 | (function()
229 | local tbl = {1, 2, 3, {4, 5}}
230 | assert(
231 | T.contains(
232 | tbl,
233 | 3
234 | ),
235 | 'contains exists'
236 | )
237 |
238 | assert(
239 | not T.contains(
240 | tbl,
241 | 7
242 | ),
243 | 'contains non exists'
244 | )
245 |
246 | assert(
247 | not T.contains(
248 | tbl,
249 | nil
250 | ),
251 | 'contains nil'
252 | )
253 |
254 | assert(
255 | not T.contains(
256 | tbl,
257 | {4, 5}
258 | ),
259 | 'contains table'
260 | )
261 |
262 | local sub_tbl = {4, 5}
263 | local tbl = {1, 2, 3, sub_tbl}
264 |
265 | assert(
266 | T.contains(
267 | tbl,
268 | sub_tbl
269 | ),
270 | 'contains table by ref'
271 | )
272 |
273 | end)();
274 |
275 | -- __call
276 | (function()
277 | assert(
278 | tester.tbl_cmpi(
279 | T(1, 2, 3),
280 | {1, 2, 3},
281 | true
282 | ),
283 | '__call'
284 | )
285 | end)();
286 |
287 | -- __add
288 | (function()
289 | assert(
290 | tester.tbl_cmpi(
291 | T(1, 2, 3) + 4,
292 | {1, 2, 3, 4},
293 | true
294 | ),
295 | '__add'
296 | )
297 | end)();
298 |
299 | -- __mul
300 | (function()
301 | assert(
302 | tester.tbl_cmpi(
303 | T(1, 2) * 3,
304 | {{1, 2}, {1, 2}, {1, 2}},
305 | true
306 | ),
307 | '__mul'
308 | )
309 | end)();
310 | end
311 | end
312 |
--------------------------------------------------------------------------------
/tests/02_util_string.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local S = require('estrela.util.string');
4 |
5 | local str = 'hello world yep'
6 |
7 | -- find_first
8 | (function()
9 | assert(
10 | S.find_first(str, 'w') == 7,
11 | 'find_first uniq [sub]'
12 | )
13 |
14 | assert(
15 | S.find_first(str, 'W') == nil,
16 | 'find_first case sensitivity'
17 | )
18 |
19 | assert(
20 | S.find_first(str, 'l') == 3,
21 | 'find_first ordinary [sub]'
22 | )
23 |
24 | assert(
25 | S.find_first(str, 'l', 3) == 3,
26 | 'find_first with [start]'
27 | )
28 |
29 | assert(
30 | S.find_first(str, 'l', 6) == 10,
31 | 'find_first with [start] after first appearance'
32 | )
33 |
34 | assert(
35 | S.find_first(str, 'l', 1, 3) == 3,
36 | 'find_first with [stop]'
37 | )
38 |
39 | assert(
40 | S.find_first(str, 'l', 1, 2) == nil,
41 | 'find_first with [stop] before first appearance'
42 | )
43 |
44 | assert(
45 | S.find_first(str, 'yep') == 13,
46 | 'find_first multichar [sub]'
47 | )
48 |
49 | assert(
50 | S.find_first(str, 'yep!') == nil,
51 | 'find_first over-tail check'
52 | )
53 | end)();
54 |
55 | -- find_last
56 | (function()
57 | assert(
58 | S.find_last(str, 'w') == 7,
59 | 'find_last uniq [sub]'
60 | )
61 |
62 | assert(
63 | S.find_last(str, 'W') == nil,
64 | 'find_last case sensitivity'
65 | )
66 |
67 | assert(
68 | S.find_last(str, 'l') == 10,
69 | 'find_last ordinary [sub]'
70 | )
71 |
72 | assert(
73 | S.find_last(str, 'l', 3) == 10,
74 | 'find_last with [start]'
75 | )
76 |
77 | assert(
78 | S.find_last(str, 'l', 1, 9) == 4,
79 | 'find_last with [stop] before last appearance'
80 | )
81 |
82 | assert(
83 | S.find_last(str, 'l', 1, 3) == 3,
84 | 'find_last with [stop]'
85 | )
86 |
87 | assert(
88 | S.find_last(str, 'l', 12) == nil,
89 | 'find_last with [start] after last appearance'
90 | )
91 |
92 | assert(
93 | S.find_last(str, 'yep') == 13,
94 | 'find_last multichar [sub]'
95 | )
96 |
97 | assert(
98 | S.find_last(str, 'yep!') == nil,
99 | 'find_last over-tail check'
100 | )
101 | end)();
102 |
103 | -- split
104 | (function()
105 | assert(
106 | tester.tbl_cmpi(
107 | S.split('hello'),
108 | {'h', 'e', 'l', 'l', 'o'}
109 | ),
110 | 'split w/o [sep]'
111 | )
112 |
113 | assert(
114 | tester.tbl_cmpi(
115 | S.split('hello', ''),
116 | {'h', 'e', 'l', 'l', 'o'}
117 | ),
118 | 'split with empty [sep]'
119 | )
120 |
121 | assert(
122 | tester.tbl_cmpi(
123 | S.split(str, '|'),
124 | {str}
125 | ),
126 | 'split with nonexistent [sep]'
127 | )
128 |
129 | assert(
130 | tester.tbl_cmpi(
131 | S.split(str, ' '),
132 | {'hello', 'world', 'yep'}
133 | ),
134 | [[split with [sep] = ' ']]
135 | )
136 |
137 | assert(
138 | tester.tbl_cmpi(
139 | S.split(str, ' ', 1),
140 | {str}
141 | ),
142 | [[split with [maxsplit] = 1]]
143 | )
144 |
145 | assert(
146 | tester.tbl_cmpi(
147 | S.split(str, ' ', 2),
148 | {'hello', 'world yep'}
149 | ),
150 | [[split with [maxsplit] = 2]]
151 | )
152 |
153 | assert(
154 | tester.tbl_cmpi(
155 | S.split(str, ' ', 42),
156 | {'hello', 'world', 'yep'}
157 | ),
158 | [[split with [maxsplit] = 42]]
159 | )
160 |
161 | assert(
162 | tester.tbl_cmpi(
163 | S.split(str .. ' ', ' '),
164 | {'hello', 'world', 'yep', ''}
165 | ),
166 | [[split with empty section]]
167 | )
168 |
169 | assert(
170 | tester.tbl_cmpi(
171 | S.split(str, '%s', nil, true),
172 | {'hello', 'world', 'yep'}
173 | ),
174 | [[split with regex [sep] = '%s']]
175 | )
176 |
177 | assert(
178 | tester.tbl_cmpi(
179 | S.split(str, '%s', nil, true),
180 | {'hello', 'world', 'yep'}
181 | ),
182 | [[split with regex [sep] = '%s']]
183 | )
184 |
185 | assert(
186 | tester.tbl_cmpi(
187 | S.split('hello world! yep:)', '%W+', nil, true),
188 | {'hello', 'world', 'yep', ''}
189 | ),
190 | [[split with regex [sep] = '%W+']]
191 | )
192 | end)();
193 |
194 | -- ltrim
195 | (function()
196 | assert(
197 | S.ltrim(str) == str,
198 | 'ltrim w/o spaces'
199 | )
200 |
201 | assert(
202 | S.ltrim(' ' .. str) == str,
203 | 'ltrim w/ left spaces'
204 | )
205 |
206 | assert(
207 | S.ltrim(str .. ' ') == str .. ' ',
208 | 'ltrim w/ right spaces'
209 | )
210 |
211 | assert(
212 | S.ltrim(' ' .. str .. ' ') == str .. ' ',
213 | 'ltrim w/ left and right spaces'
214 | )
215 |
216 | assert(
217 | S.ltrim(' \r\n\v' .. str .. ' \r\n\v') == str .. ' \r\n\v',
218 | 'ltrim w/ special chars'
219 | )
220 |
221 | assert(
222 | S.ltrim(str, 'helo ') == 'world yep',
223 | 'ltrim w/ [chars]'
224 | )
225 |
226 | assert(
227 | S.ltrim(str, '') == str,
228 | 'ltrim w/ empty [chars]'
229 | )
230 | end)();
231 |
232 | -- rtrim
233 | (function()
234 | assert(
235 | S.rtrim(str) == str,
236 | 'rtrim w/o spaces'
237 | )
238 |
239 | assert(
240 | S.rtrim(' ' .. str) == ' ' .. str,
241 | 'rtrim w/ left spaces'
242 | )
243 |
244 | assert(
245 | S.rtrim(str .. ' ') == str,
246 | 'rtrim w/ right spaces'
247 | )
248 |
249 | assert(
250 | S.rtrim(' ' .. str .. ' ') == ' ' .. str,
251 | 'rtrim w/ left and right spaces'
252 | )
253 |
254 | assert(
255 | S.rtrim(' \r\n\v' .. str .. ' \r\n\v') == ' \r\n\v' .. str,
256 | 'rtrim w/ special chars'
257 | )
258 |
259 | assert(
260 | S.rtrim(str, 'yepdlrow ') == 'h',
261 | 'rtrim w/ [chars]'
262 | )
263 |
264 | assert(
265 | S.rtrim(str, '') == str,
266 | 'rtrim w/ empty [chars]'
267 | )
268 | end)();
269 |
270 | -- trim
271 | (function()
272 | assert(
273 | S.trim(str) == str,
274 | 'trim w/o spaces'
275 | )
276 |
277 | assert(
278 | S.trim(' ' .. str) == str,
279 | 'trim w/ left spaces'
280 | )
281 |
282 | assert(
283 | S.trim(str .. ' ') == str,
284 | 'trim w/ right spaces'
285 | )
286 |
287 | assert(
288 | S.trim(' ' .. str .. ' ') == str,
289 | 'trim w/ left and right spaces'
290 | )
291 |
292 | assert(
293 | S.trim(' \r\n\v' .. str .. ' \r\n\v') == str,
294 | 'trim w/ special chars'
295 | )
296 |
297 | assert(
298 | S.trim(str, 'yepdhelo ') == 'wor',
299 | 'trim w/ [chars]'
300 | )
301 |
302 | assert(
303 | S.trim(str, '') == str,
304 | 'trim w/ empty [chars]'
305 | )
306 | end)();
307 |
308 | -- htmlencode
309 | (function()
310 | assert(
311 | S.htmlencode('') == '',
312 | 'htmlencode empty [str]'
313 | )
314 |
315 | assert(
316 | S.htmlencode(str) == str,
317 | 'htmlencode w/o special chars'
318 | )
319 |
320 | assert(
321 | S.htmlencode([[A'C'&"D"&E]]) == [[A<B>'C'&"D"&E</F>]],
322 | 'htmlencode w/o quotes'
323 | )
324 |
325 | local res = [[A<B>'C'&"D"&E</F>]]
326 | assert(
327 | S.htmlencode([[A'C'&"D"&E]], true) == res,
328 | 'htmlencode w/ quotes'
329 | )
330 | end)();
331 |
332 | -- starts
333 | (function()
334 | assert(
335 | S.starts(str, 'hello'),
336 | 'starts'
337 | )
338 |
339 | assert(
340 | not S.starts(str, 'HELLO'),
341 | 'starts case sensitivity'
342 | )
343 |
344 | assert(
345 | S.starts(str, ''),
346 | 'starts w/ empty [prefix]'
347 | )
348 |
349 | assert(
350 | not S.starts(str, 'world'),
351 | 'starts [prefix] inside [str]'
352 | )
353 |
354 | assert(
355 | not S.starts(str, 'helloworld'),
356 | 'starts over-prefix'
357 | )
358 |
359 | assert(
360 | S.starts(str .. ' fooBar', str),
361 | 'starts over-str'
362 | )
363 |
364 | assert(
365 | not S.starts(str, str .. ' over-tail'),
366 | 'starts over-tail'
367 | )
368 | end)();
369 |
370 | -- ends
371 | (function()
372 | assert(
373 | S.ends(str, 'yep'),
374 | 'ends'
375 | )
376 |
377 | assert(
378 | not S.ends(str, 'YEP'),
379 | 'ends case sensitivity'
380 | )
381 |
382 | assert(
383 | S.ends(str, ''),
384 | 'ends w/ empty [suffix]'
385 | )
386 |
387 | assert(
388 | not S.ends(str, 'world'),
389 | 'ends [suffix] inside [str]'
390 | )
391 |
392 | assert(
393 | not S.ends(str, 'worldyep'),
394 | 'ends over-suffix'
395 | )
396 |
397 | assert(
398 | S.ends('fooBar ' .. str, str),
399 | 'ends over-str'
400 | )
401 |
402 | assert(
403 | not S.ends(str, 'over-head ' .. str),
404 | 'ends over-head'
405 | )
406 | end)();
407 |
408 | -- parse_header_value
409 | (function()
410 | local _headers = {
411 | 'simple-scalar-string',
412 | 'string;key=value1;key2=value2',
413 | 'string;key=value1;key=value2',
414 | 'na/me; key=value; '
415 | .. '__utmz=1.111.11.11.utmcsr=foo|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); '
416 | .. 'tz=Europe%2FMoscow; user_session=d1K_fuBk11; filename="file.txt"',
417 | }
418 |
419 | local headers = {}
420 |
421 | for idx, line in ipairs(_headers) do
422 | local parsed = S.parse_header_value(line)
423 | assert(
424 | type(parsed) == 'table',
425 | 'parse_header_value'
426 | )
427 | headers[idx] = parsed
428 | end
429 |
430 | assert(
431 | headers[1][''] == _headers[1],
432 | 'parse_header_value scalar'
433 | )
434 |
435 | assert(
436 | headers[2][''] == 'string'
437 | and headers[2].key
438 | and headers[2].key == 'value1'
439 | and headers[2].key2
440 | and headers[2].key2 == 'value2',
441 | 'parse_header_value table w/ scalar key values'
442 | )
443 |
444 | assert(
445 | headers[3][''] == 'string'
446 | and type(headers[3].key) == 'table'
447 | and tester.tbl_cmpi(headers[3].key, {'value1', 'value2'}),
448 | 'parse_header_value table w/ table key values'
449 | )
450 |
451 | assert(
452 | tester.tbl_cmp(headers[4], {
453 | [''] = 'na/me',
454 | key = 'value',
455 | __utmz = '1.111.11.11.utmcsr=foo|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided)',
456 | tz = 'Europe%2FMoscow',
457 | user_session = 'd1K_fuBk11',
458 | filename = 'file.txt',
459 | }),
460 | 'parse_header_value mix'
461 | )
462 | end)();
463 |
464 | -- cmpi
465 | (function()
466 | assert(
467 | S.cmpi(str, str),
468 | 'cmpi'
469 | )
470 |
471 | assert(
472 | S.cmpi(str, str:upper()),
473 | 'cmpi w/ upper self'
474 | )
475 |
476 | assert(
477 | S.cmpi(str, str:lower()),
478 | 'cmpi w/ lower self'
479 | )
480 |
481 | assert(
482 | not S.cmpi(str, ''),
483 | 'cmpi empty [str2]'
484 | )
485 |
486 | assert(
487 | not S.cmpi('', str),
488 | 'cmpi empty [str1]'
489 | )
490 |
491 | assert(
492 | not S.cmpi(str, 'chpachryamba'),
493 | 'cmpi diffs'
494 | )
495 | end)();
496 |
497 | -- format
498 | (function()
499 | local _str = 'hello %s'
500 | assert(
501 | S.format(_str) == 'hello nil',
502 | 'format w/o [vars]'
503 | )
504 |
505 | assert(
506 | S.format(_str, 'world') == 'hello world',
507 | 'format w/ scalar [vars]'
508 | )
509 |
510 | assert(
511 | S.format(_str, {'world'}) == 'hello world',
512 | 'format w/ table [vars]'
513 | )
514 | end)();
515 |
516 | -- apply_patch
517 | (function()
518 | S.apply_patch()
519 |
520 | local _str = 'foo'
521 |
522 | assert(
523 | string.starts and _str.trim,
524 | 'apply_patch mapping'
525 | )
526 |
527 | assert(
528 | ('hello %s' % 'world') == 'hello world',
529 | 'apply_patch % scalar'
530 | )
531 |
532 | assert(
533 | ('%s %s' % {'hello', 'world'}) == 'hello world',
534 | 'apply_patch % table'
535 | )
536 | end)();
537 | end
538 | end
539 |
--------------------------------------------------------------------------------
/tests/03_util_path.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local P = require('estrela.util.path');
4 |
5 | -- split
6 | (function()
7 | local info = P.split('/path/to/file.ext')
8 | assert(
9 | info.path == '/path/to/'
10 | and info.name == 'file.ext'
11 | and info.ext == 'ext',
12 | 'split'
13 | )
14 |
15 | local info = P.split('/path/to/')
16 | assert(
17 | info.path == '/path/to/'
18 | and info.name == ''
19 | and info.ext == '',
20 | 'split only path'
21 | )
22 |
23 | local info = P.split('/path/to')
24 | assert(
25 | info.path == '/path/'
26 | and info.name == 'to'
27 | and info.ext == '',
28 | 'split w/o ext'
29 | )
30 |
31 | local info = P.split('/')
32 | assert(
33 | info.path == '/'
34 | and info.name == ''
35 | and info.ext == '',
36 | 'split root'
37 | )
38 |
39 | local info = P.split('filename')
40 | assert(
41 | info.path == ''
42 | and info.name == 'filename'
43 | and info.ext == '',
44 | 'split only filename'
45 | )
46 | end)();
47 |
48 | -- rel
49 | (function()
50 |
51 | assert(
52 | tester.tbl_cmpi(
53 | {P.rel('/path/to/estrela', '/path/to/estrela/bootstrap.lua')},
54 | {'bootstrap.lua', 0}
55 | ),
56 | 'rel in workdir'
57 | )
58 |
59 | assert(
60 | tester.tbl_cmpi(
61 | {P.rel('/path/to/estrela', '/path/to/estrela/foo/bar/bootstrap.lua')},
62 | {'foo/bar/bootstrap.lua', 0}
63 | ),
64 | 'rel downward path'
65 | )
66 |
67 | assert(
68 | tester.tbl_cmpi(
69 | {P.rel('/path/to/estrela', '/path/foo/bar/bootstrap.lua')},
70 | {'../../foo/bar/bootstrap.lua', 2}
71 | ),
72 | 'rel upward+downward path'
73 | )
74 |
75 | assert(
76 | tester.tbl_cmpi(
77 | {P.rel('/path/to/lua/lib/estrela', '/bootstrap.lua')},
78 | {'../../../../../bootstrap.lua', 5}
79 | ),
80 | 'rel upward path'
81 | )
82 |
83 | assert(
84 | tester.tbl_cmpi(
85 | {P.rel('/path/to/lua/lib/estrela', '/foo/bar/bootstrap.lua')},
86 | {'../../../../../foo/bar/bootstrap.lua', 5}
87 | ),
88 | 'rel upward to root+downward path'
89 | )
90 |
91 | assert(
92 | tester.tbl_cmpi(
93 | {P.rel('/', '/path/to/lua/bootstrap.lua')},
94 | {'path/to/lua/bootstrap.lua', 0}
95 | ),
96 | 'rel downward from root path'
97 | )
98 | end)();
99 |
100 | -- join
101 | (function()
102 | assert(
103 | P.join('/') == '/',
104 | [[join('/')]]
105 | )
106 |
107 | assert(
108 | P.join('/', 'foo', 'bar') == '/foo/bar',
109 | [[join('/', 'foo', 'bar')]]
110 | )
111 |
112 | assert(
113 | P.join('/path', 'to', 'file') == '/path/to/file',
114 | [[join('/path', 'to', 'file')]]
115 | )
116 |
117 | assert(
118 | P.join('/path/', 'to', 'file') == '/path/to/file',
119 | [[join('/path/', 'to', 'file')]]
120 | )
121 |
122 | assert(
123 | P.join('/path/', 'to/', 'file/') == '/path/to/file',
124 | [[join('/path/', 'to/', 'file/')]]
125 | )
126 | end)();
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/tests/04_util_env.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 |
4 | -- get_root
5 | (function()
6 | -- monkey patching :)
7 | local debug = require('debug')
8 | debug.getinfo = function()
9 | return {
10 | source = '@/path/to/util/env.lua',
11 | }
12 | end
13 |
14 | local E = require('estrela.util.env')
15 |
16 | assert(
17 | E.get_root() == '/path/to',
18 | 'get_root'
19 | )
20 | end)();
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/tests/05_io_ob.lua:
--------------------------------------------------------------------------------
1 | local tests = {
2 | {
3 | name = 'without_ob',
4 | code = function(tester)
5 | ngx.print 'ok'
6 | end,
7 | checker = function(tester, test, body, status, headers)
8 | return assert(body == 'ok', test.name)
9 | end,
10 | },
11 |
12 | -- print
13 | {
14 | name = 'print_wo_buff',
15 | code = function(tester, test)
16 | local OB = require('estrela.io.ob')
17 | OB.print('hello', 'world')
18 | OB.print(nil)
19 | end,
20 | checker = function(tester, test, body, status, headers)
21 | return assert(body == 'helloworld', test.name)
22 | end,
23 | },
24 |
25 | -- println
26 | {
27 | name = 'println_wo_buff',
28 | code = function(tester, test)
29 | local OB = require('estrela.io.ob')
30 | OB.println('hello', 'world')
31 | OB.println('traveler!')
32 | OB.println()
33 | end,
34 | checker = function(tester, test, body, status, headers)
35 | return assert(body == 'helloworld\ntraveler!\n\n', test.name)
36 | end,
37 | },
38 |
39 | -- print_lua
40 | {
41 | name = 'print_lua_wo_buff',
42 | code = function(tester, test)
43 | local OB = require('estrela.io.ob')
44 | OB.print_lua('hello', 'world')
45 | OB.print_lua()
46 | end,
47 | checker = function(tester, test, body, status, headers)
48 | return assert(body == 'hello\tworld\n\n', test.name)
49 | end,
50 | },
51 |
52 | -- start
53 | {
54 | name = 'start_wo_finish',
55 | code = function(tester, test)
56 | local OB = require('estrela.io.ob')
57 | ngx.print 'hello'
58 | OB.start()
59 | ngx.print 'world'
60 | -- without OB.finish()
61 | end,
62 | checker = function(tester, test, body, status, headers)
63 | return assert(body == 'hello', test.name)
64 | end,
65 | },
66 |
67 | -- finish
68 | {
69 | name = 'start_with_finish',
70 | code = function(tester, test)
71 | local OB = require('estrela.io.ob')
72 | ngx.print 'hello'
73 | OB.start()
74 | ngx.print 'world'
75 | OB.finish()
76 | end,
77 | checker = function(tester, test, body, status, headers)
78 | return assert(body == 'hello', test.name)
79 | end,
80 | },
81 |
82 | -- flush_finish
83 | {
84 | name = 'start_with_flush_finish',
85 | code = function(tester, test)
86 | local OB = require('estrela.io.ob')
87 | ngx.print 'hello'
88 | OB.start()
89 | ngx.print 'world'
90 | OB.flush_finish()
91 | end,
92 | checker = function(tester, test, body, status, headers)
93 | return assert(body == 'helloworld', test.name)
94 | end,
95 | },
96 |
97 | {
98 | name = 'start_with_cb_with_flush_finish',
99 | code = function(tester, test)
100 | local OB = require('estrela.io.ob')
101 | ngx.say 'hello'
102 | OB.start(function(chunk) return string.upper(chunk) end)
103 | ngx.print 'world'
104 | OB.flush_finish()
105 | end,
106 | checker = function(tester, test, body, status, headers)
107 | return assert(body == 'hello\nWORLD', test.name)
108 | end,
109 | },
110 |
111 | {
112 | name = 'double_start_with_cb_with_flush_finish',
113 | code = function(tester, test)
114 | local OB = require('estrela.io.ob')
115 | ngx.print 'hello'
116 | OB.start(function(chunk) return string.upper(chunk) end)
117 | ngx.print 'world'
118 | OB.start(function(chunk) return string.sub(chunk, 1, 2) end)
119 | ngx.print 'traveler'
120 | OB.flush_finish()
121 | OB.flush_finish()
122 | end,
123 | checker = function(tester, test, body, status, headers)
124 | return assert(body == 'helloWORLDTR', test.name)
125 | end,
126 | },
127 |
128 | {
129 | name = 'double_start_with_cb_with_only_one_flush_finish',
130 | code = function(tester, test)
131 | local OB = require('estrela.io.ob')
132 | ngx.print 'hello'
133 | OB.start(function(chunk) return string.upper(chunk) end)
134 | ngx.print 'world'
135 | OB.start(function(chunk) return string.sub(chunk, 1, 2) end)
136 | ngx.print 'traveler'
137 | OB.flush_finish()
138 | -- without OB.flush_finish()
139 | end,
140 | checker = function(tester, test, body, status, headers)
141 | return assert(body == 'hello', test.name)
142 | end,
143 | },
144 |
145 | {
146 | name = 'start_with_double_flush_finish',
147 | code = function(tester, test)
148 | local OB = require('estrela.io.ob')
149 | ngx.print 'hello'
150 | OB.start()
151 | ngx.print 'world'
152 | OB.flush_finish()
153 |
154 | OB.flush_finish()
155 | end,
156 | checker = function(tester, test, body, status, headers)
157 | return assert(body == 'helloworld', test.name)
158 | end,
159 | },
160 |
161 | -- flush
162 | {
163 | name = 'start_with_flush',
164 | code = function(tester, test)
165 | local OB = require('estrela.io.ob')
166 | ngx.print 'hello'
167 | OB.start()
168 | ngx.print 'world'
169 | OB.flush()
170 | ngx.print 'traveler'
171 | -- without OB.finish()
172 | end,
173 | checker = function(tester, test, body, status, headers)
174 | return assert(body == 'helloworld', test.name)
175 | end,
176 | },
177 |
178 | -- get
179 | {
180 | name = 'get_after_start',
181 | code = function(tester, test)
182 | local OB = require('estrela.io.ob')
183 | ngx.print 'hello'
184 | OB.start()
185 | print('world', 'traveler')
186 | io.write('!')
187 | local buf = OB.get()
188 | OB.flush_finish()
189 |
190 | ngx.print(buf)
191 | end,
192 | checker = function(tester, test, body, status, headers)
193 | return assert(body == 'helloworld\ttraveler\n!world\ttraveler\n!', test.name)
194 | end,
195 | },
196 |
197 | {
198 | name = 'get_before_start',
199 | code = function(tester, test)
200 | local OB = require('estrela.io.ob')
201 | ngx.print(type(OB.get()))
202 | end,
203 | checker = function(tester, test, body, status, headers)
204 | return assert(body == 'nil', test.name)
205 | end,
206 | },
207 |
208 | -- levels
209 | {
210 | name = 'levels',
211 | code = function(tester, test)
212 | local OB = require('estrela.io.ob')
213 | local levels = {}
214 | table.insert(levels, OB.levels())
215 | OB.start()
216 | table.insert(levels, OB.levels())
217 | OB.start()
218 | table.insert(levels, OB.levels())
219 | OB.flush()
220 | table.insert(levels, OB.levels())
221 | OB.finish()
222 | table.insert(levels, OB.levels())
223 | OB.flush_finish()
224 | table.insert(levels, OB.levels())
225 |
226 | ngx.print(table.concat(levels, ','))
227 | end,
228 | checker = function(tester, test, body, status, headers)
229 | return assert(body == '0,1,2,2,1,0', test.name)
230 | end,
231 | },
232 |
233 | -- len
234 | {
235 | name = 'len',
236 | code = function(tester, test)
237 | local OB = require('estrela.io.ob')
238 | local len = {}
239 | table.insert(len, OB.len())
240 | ngx.say(test.__prefix)
241 | table.insert(len, OB.len())
242 | OB.start()
243 | ngx.print 'a'
244 | table.insert(len, OB.len())
245 | ngx.print 'b'
246 | OB.start()
247 | ngx.print 'c'
248 | table.insert(len, OB.len())
249 | ngx.print 'd'
250 | OB.flush()
251 | ngx.print 'e'
252 | table.insert(len, OB.len())
253 | ngx.print 'f'
254 | OB.finish()
255 | ngx.print 'g'
256 | table.insert(len, OB.len())
257 | ngx.print 'h'
258 | OB.flush_finish()
259 | ngx.print 'i'
260 | table.insert(len, OB.len())
261 |
262 | ngx.print(table.concat(len, ','))
263 | end,
264 | checker = function(tester, test, body, status, headers)
265 | return assert(body == test.__prefix .. '\nabcdghi1,1,1,5', test.name)
266 | end,
267 | __prefix = 'ufheforgofrygforgf7r9fyrurhyeguyrqeg7e',
268 | },
269 |
270 | -- clean
271 | {
272 | name = 'clean',
273 | code = function(tester, test)
274 | local OB = require('estrela.io.ob')
275 | ngx.print 'hello'
276 | OB.start()
277 | ngx.print 'world'
278 | OB.clean()
279 | OB.flush_finish()
280 | end,
281 | checker = function(tester, test, body, status, headers)
282 | return assert(body == 'hello', test.name)
283 | end,
284 | },
285 |
286 | -- status
287 | {
288 | name = 'status',
289 | code = function(tester, test)
290 | local OB = require('estrela.io.ob')
291 |
292 | local cb = function(chunk) return chunk end
293 |
294 | OB.start()
295 | ngx.print 'hello'
296 | OB.start(cb)
297 | ngx.print 'world!'
298 | local status = OB.status()
299 | OB.flush_finish()
300 | OB.flush_finish()
301 |
302 | assert(
303 | tester.tbl_cmp(
304 | status,
305 | {
306 | {
307 | len = 5,
308 | cb = nil,
309 | },
310 | {
311 | len = 6,
312 | cb = cb,
313 | },
314 | }
315 | ),
316 | test.name
317 | )
318 | ngx.say 'hello'
319 | end,
320 | checker = function(tester, test, body, status, headers)
321 | return assert(body == 'helloworld!hello\n', test.name)
322 | end,
323 | },
324 | }
325 |
326 | if ngx then
327 | return function(tester)
328 | return tester:multitest_srv(tests)
329 | end
330 | else
331 | return function(tester)
332 | return tester:multitest_cli(tests)
333 | end
334 | end
335 |
--------------------------------------------------------------------------------
/tests/06_io_pprint.lua:
--------------------------------------------------------------------------------
1 | local tests = {
2 | -- sprint
3 | {
4 | name = 'sprint_basic',
5 | code = function(tester, test)
6 | local sprint = require('estrela.io.pprint').sprint
7 |
8 | assert(
9 | sprint() == 'nil\n',
10 | test.name .. ' w/o params'
11 | )
12 |
13 | assert(
14 | sprint(nil) == 'nil\n',
15 | test.name .. ' nil'
16 | )
17 |
18 | assert(
19 | sprint('') == '""\n',
20 | test.name .. ' empty string'
21 | )
22 |
23 | assert(
24 | sprint(0) == '0\n',
25 | test.name .. ' number'
26 | )
27 |
28 | assert(
29 | sprint('hello') == '"hello"\n',
30 | test.name .. ' string'
31 | )
32 |
33 | assert(
34 | sprint({}) == '{\n}\n',
35 | test.name .. ' empty table'
36 | )
37 |
38 | assert(
39 | sprint({'hello', 42, 'world'}) == '{\n\t[1] = "hello",\n\t[2] = 42,\n\t[3] = "world",\n}\n',
40 | test.name .. ' table'
41 | )
42 |
43 | assert(
44 | sprint(true) == 'true\n',
45 | test.name .. ' boolean'
46 | )
47 |
48 | assert(
49 | sprint(ngx.null) == 'nil --[[userdata]]\n',
50 | test.name .. ' userdata'
51 | )
52 |
53 | -- nil --[[function: 0x41209210 .../../../tests/6_io_pprint.lua:10]]
54 | local res = [[^nil --\[\[function: 0x[0-9a-f]+ .*io_pprint\.lua:[0-9]+\]\]\s$]]
55 |
56 | assert(
57 | ngx.re.match(sprint(function() end), res),
58 | test.name .. ' function'
59 | )
60 | end,
61 | checker = function(tester, test, body, status, headers)
62 | end,
63 | },
64 |
65 | {
66 | name = 'sprint_compact',
67 | code = function(tester, test)
68 | local sprint = require('estrela.io.pprint').sprint
69 |
70 | assert(
71 | sprint(0, {compact=true}) == '0',
72 | test.name .. ' number'
73 | )
74 |
75 | assert(
76 | sprint({}, {compact=true}) == '{}',
77 | test.name .. ' empty table'
78 | )
79 |
80 | assert(
81 | sprint({'hello', 42, 'world'}, {compact=true}) == '{[1] = "hello",[2] = 42,[3] = "world",}',
82 | test.name .. ' table'
83 | )
84 | end,
85 | checker = function(tester, test, body, status, headers)
86 | end,
87 | },
88 |
89 | {
90 | name = 'sprint_max_depth',
91 | code = function(tester, test)
92 | local sprint = require('estrela.io.pprint').sprint
93 |
94 | local res = '{[1] = 1,[2] = {[1] = 2,[2] = {[1] = 3,[2] = 4,},[3] = 5,},[3] = 6,}'
95 | assert(
96 | sprint({1, {2, {3, 4}, 5}, 6}, {compact=true, max_depth=5}) == res,
97 | test.name .. ' max_depth > table depth'
98 | )
99 |
100 | local res = '{[1] = 1,[2] = {[1] = 2,[2] = nil --[[max_depth cutted]],[3] = 5,},[3] = 6,}'
101 | assert(
102 | sprint({1, {2, {3, 4}, 5}, 6}, {compact=true, max_depth=1}) == res,
103 | test.name .. ' max_depth < table depth'
104 | )
105 |
106 | local res = '{[1] = "a",[2] = {[{[1] = "b",[2] = nil --[[max_depth cutted]],}] = {[nil --[[max_depth cutted]]] = nil --[[max_depth cutted]],},},}'
107 | assert(
108 | sprint({'a', {[{'b', {'c'}}] = {[{'d'}] = {'e', 'f'}}}}, {compact=true, max_depth=2}) == res,
109 | test.name .. ' max_depth < table depth w/ tables as keys'
110 | )
111 | end,
112 | checker = function(tester, test, body, status, headers)
113 | end,
114 | },
115 |
116 | {
117 | name = 'sprint_func_pathes',
118 | code = function(tester, test)
119 | local sprint = require('estrela.io.pprint').sprint
120 |
121 | -- nil --[[function: 0x41209210]]
122 | local res = [[^nil --\[\[function: 0x[0-9a-f]+\]\]\s$]]
123 |
124 | assert(
125 | ngx.re.match(sprint(function() end, {func_pathes=false}), res),
126 | test.name
127 | )
128 | end,
129 | checker = function(tester, test, body, status, headers)
130 | end,
131 | },
132 |
133 | {
134 | name = 'sprint_func_patches_short',
135 | code = function(tester, test)
136 | local sprint = require('estrela.io.pprint').sprint
137 |
138 | local f = function() end
139 |
140 | assert(
141 | sprint(f, {func_patches_short=false}) ~= sprint(f, {func_patches_short=true}),
142 | test.name
143 | )
144 | end,
145 | checker = function(tester, test, body, status, headers)
146 | end,
147 | },
148 |
149 | {
150 | name = 'sprint_func_patches_short_rel',
151 | code = function(tester, test)
152 | local sprint = require('estrela.io.pprint').sprint
153 |
154 | local f = function() end
155 |
156 | assert(
157 | sprint(f, {func_patches_short_rel=100500}) == sprint(f, {func_patches_short=true}),
158 | test.name .. ' rel 100500'
159 | )
160 |
161 | assert(
162 | sprint(f, {func_patches_short_rel=0}) == sprint(f, {func_patches_short=false}),
163 | test.name .. ' rel 1'
164 | )
165 | end,
166 | checker = function(tester, test, body, status, headers)
167 | end,
168 | },
169 |
170 | -- print
171 | {
172 | name = 'print',
173 | code = function(tester, test)
174 | local pprint = require('estrela.io.pprint').print;
175 |
176 | -- с подменой writer'а
177 | (function()
178 | local data = {}
179 | local function writer(chunk)
180 | table.insert(data, chunk)
181 | end
182 |
183 | pprint({'hello', 42, 'world'}, {compact=true, writer=writer})
184 |
185 | data = table.concat(data)
186 |
187 | assert(
188 | data == '{[1] = "hello",[2] = 42,[3] = "world",}',
189 | test.name .. ' w/ writer'
190 | )
191 | end)();
192 |
193 | -- стандартный вывод через ngx.print
194 | (function()
195 | local data = {}
196 | local _ngx_print = ngx.print
197 | ngx.print = function(chunk)
198 | table.insert(data, chunk)
199 | end
200 |
201 | pprint({'hello', 42, 'world'}, {compact=true})
202 |
203 | ngx.print = _ngx_print
204 |
205 | data = table.concat(data)
206 |
207 | assert(
208 | data == '{[1] = "hello",[2] = 42,[3] = "world",}',
209 | test.name .. ' w/o writer'
210 | )
211 | end)();
212 | end,
213 | checker = function(tester, test, body, status, headers)
214 | end,
215 | },
216 | }
217 |
218 | if ngx then
219 | return function(tester)
220 | return tester:multitest_srv(tests)
221 | end
222 | else
223 | return function(tester)
224 | return tester:multitest_cli(tests)
225 | end
226 | end
227 |
--------------------------------------------------------------------------------
/tests/07_log_common.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local L = require('estrela.log.common'):new();
4 |
5 | -- проверка наличия методов
6 | local methods = {'debug', 'info', 'notice', 'warn', 'err', 'crit', 'alert', 'emerg',}
7 |
8 | for _, method in ipairs(methods) do
9 | assert(L[method], method)
10 |
11 | local error_triggered = false
12 | xpcall(L[method], function() error_triggered = true end)
13 | assert(error_triggered, method .. ' error() call')
14 | end
15 |
16 | -- проверка наличия алиасов
17 | local methods = {warning='warn', error='err', critical='crit', emergency='emerg',}
18 | for alias, method in pairs(methods) do
19 | assert(L[alias] == L[method], alias)
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/tests/08_log_file.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local levels = require('estrela.log.common'):new().level_names
4 |
5 | math.randomseed(os.time())
6 |
7 | for level_code, level_name in pairs(levels) do
8 | local tmpname = os.tmpname()
9 |
10 | local check = tostring(os.time()) .. tostring(math.random())
11 |
12 | local F = require('estrela.log.file'):new(tmpname, 'TESTER %Y/%m/%d %H:%M:%S {LVL} [{MSG}] {BT}')
13 | F._write(level_code, check, '')
14 | F.close()
15 |
16 | local fd = io.open(tmpname, 'rb')
17 | assert(fd, 'logfile exists')
18 |
19 | local cont = fd:read('*a')
20 | fd:close()
21 |
22 | os.remove(tmpname)
23 |
24 | -- TESTER 2014/07/03 18:17:08 EMERG [/tmp/lua_FU3WuM] \n
25 | local re = string.format(
26 | [=[^TESTER \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} %s\s+\[%s\]\s{2}$]=],
27 | level_name,
28 | check
29 | )
30 |
31 | assert(
32 | ngx.re.match(cont, re),
33 | 'format ' .. level_name
34 | )
35 | end
36 |
37 | local tmpname = os.tmpname()
38 |
39 | local F = require('estrela.log.file'):new(tmpname);
40 | F.level = F.EMERG
41 | F.debug('hello')
42 | F.close()
43 |
44 | local fd = io.open(tmpname, 'rb')
45 | assert(fd, 'logfile exists#2')
46 |
47 | local cont = fd:read('*a')
48 | fd:close()
49 |
50 | os.remove(tmpname)
51 |
52 | assert(
53 | #cont == 0,
54 | 'format error levels'
55 | )
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/tests/09_log_ngx.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local _ngx_log = ngx.log
4 |
5 | local called = false
6 | ngx.log = function(...)
7 | called = true
8 | end
9 |
10 | local N = require('estrela.log.ngx'):new()
11 | N.emerg('hello world#1')
12 |
13 | local called2 = called
14 |
15 | N.level = N.DISABLE
16 | called = false
17 | N.emerg('hello world#2')
18 |
19 | ngx.log = _ngx_log
20 |
21 | assert(
22 | called2 and (not called),
23 | 'was called'
24 | )
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/tests/10_storage_engine_common.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | return function(tester)
3 | local S = require('estrela.storage.engine.common'):new()
4 |
5 | local error_str
6 |
7 | xpcall(S.get, function(err) error_str = err end)
8 |
9 | assert(error_str == 'Not implemented')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/tests/11_storage_engine_shmem.lua:
--------------------------------------------------------------------------------
1 | if ngx then
2 | local function get_mock()
3 | local mock = {
4 | _data = {},
5 |
6 | _mem_enough = true,
7 | }
8 |
9 | function mock:get(key)
10 | local data = self._data[key]
11 | if not data or ((data.expire > 0) and (data.expire < ngx.now())) then
12 | return nil
13 | end
14 |
15 | return data.data
16 | end
17 |
18 | function mock:get_stale(key)
19 | local data = self._data[key]
20 | return data and data.data or nil
21 | end
22 |
23 | function mock:set(key, value, ttl)
24 | if not self._mem_enough then
25 | return false, 'no memory'
26 | end
27 |
28 | ttl = ttl or 0
29 |
30 | self._data[key] = {
31 | data = value,
32 | expire = (ttl > 0) and (ngx.now() + ttl) or 0,
33 | }
34 |
35 | return true
36 | end
37 |
38 | function mock:add(key, value, ttl)
39 | if self:get(key) then
40 | return false, 'exists'
41 | end
42 |
43 | return self:set(key, value, ttl)
44 | end
45 |
46 | function mock:replace(key, value, ttl)
47 | if not self:get(key) then
48 | return false, 'not found'
49 | end
50 |
51 | return self:set(key, value, ttl)
52 | end
53 |
54 | function mock:incr(key, value)
55 | local val = self:get(key)
56 | if not val then
57 | return false, 'not found'
58 | end
59 |
60 | val = tonumber(val)
61 | if not val then
62 | return nil, 'not a number'
63 | end
64 |
65 | val = val + value
66 |
67 | self._data[key].data = val
68 | -- expire не обновляется
69 |
70 | return val
71 | end
72 |
73 | function mock:delete(key)
74 | self._data[key] = nil
75 | end
76 |
77 | return mock
78 | end
79 |
80 | return function(tester)
81 | local shmem_name = tostring(os.time()) .. tostring(math.random())
82 |
83 | ngx.shared[shmem_name] = get_mock()
84 |
85 | local S = require('estrela.storage.engine.shmem'):new(shmem_name)
86 |
87 | local key = 'foo'
88 | local val = 42
89 |
90 | -- basic
91 |
92 | assert(
93 | S:get(key) == nil,
94 | 'get nonexistent key'
95 | )
96 |
97 | assert(
98 | S:set(key, val) == true,
99 | 'set w/o ttl'
100 | )
101 |
102 | assert(
103 | S:incr(key, 7) == 49,
104 | 'incr +'
105 | )
106 |
107 | assert(
108 | S:incr(key, -7) == val,
109 | 'incr -'
110 | )
111 |
112 | assert(
113 | S:incr(ngx.now(), 7) == false,
114 | 'incr nonexistent key'
115 | )
116 |
117 | assert(
118 | S:get(key) == val,
119 | 'get existent key'
120 | )
121 |
122 | assert(
123 | S:add(key, val .. val) == false,
124 | 'add w/ existent key'
125 | )
126 |
127 | assert(
128 | S:replace(key, val .. val) == true,
129 | 'replace w/ existent key'
130 | )
131 |
132 | assert(
133 | S:get(key) == val .. val,
134 | 'get replaced key'
135 | )
136 |
137 | assert(
138 | S:set(key, val) == true,
139 | 'set existent key'
140 | )
141 |
142 | assert(
143 | S:exists(key) == true,
144 | 'exists existent key'
145 | )
146 |
147 | S:delete(key)
148 |
149 | assert(
150 | S:get(key) == nil,
151 | 'get deleted key'
152 | )
153 |
154 | assert(
155 | S:exists(key) == false,
156 | 'exists nonexistent key'
157 | )
158 |
159 | -- expires
160 |
161 | assert(
162 | S:set(key, val, 0.5) == true,
163 | 'set w/ ttl'
164 | )
165 |
166 | assert(
167 | S:exists(key) == true,
168 | 'exists existent key w/ ttl'
169 | )
170 |
171 | ngx.sleep(0.5)
172 |
173 | assert(
174 | S:get(key) == nil,
175 | 'get existent key w/ ttl after ttl expires'
176 | )
177 |
178 | assert(
179 | S:get(key, true) == val,
180 | 'get stale existent key w/ ttl after ttl expires'
181 | )
182 |
183 | -- no enough of memory
184 | ngx.shared[shmem_name]._mem_enough = false
185 |
186 | assert(
187 | S:set(key, val) == false,
188 | 'set w/ no enough of memory'
189 | )
190 |
191 | ngx.shared[shmem_name] = nil
192 | end
193 | end
194 |
--------------------------------------------------------------------------------
/tests/12_ngx_response_cookie.lua:
--------------------------------------------------------------------------------
1 | local demo_cookie = {
2 | name = 'noname',
3 | value = 'fake_value',
4 | path = '/',
5 | httponly = true,
6 | secure = true,
7 | }
8 |
9 | local function checker_demo_cookie(tester, test, body, status, headers)
10 | local parse_header_value = require('estrela.util.string').parse_header_value
11 |
12 | local cookie = headers['set_cookie']
13 | cookie = cookie and parse_header_value(cookie) or nil
14 | assert(cookie, test.name)
15 |
16 | table.sort(cookie['']) -- чтобы тест tester.tbl_cmpi ниже не зависел от порядка перечисления флагов
17 |
18 | assert(
19 | cookie[demo_cookie.name] == demo_cookie.value
20 | and cookie.path == demo_cookie.path
21 | and tester.tbl_cmpi(cookie[''], {'httponly', 'secure'}),
22 | test.name
23 | )
24 | end
25 |
26 | local tests = {
27 | -- empty
28 | {
29 | name = 'empty_wo_map',
30 | code = function(tester, test)
31 | local C = require('estrela.ngx.response.cookie'):new()
32 |
33 | local cookie = C:empty()
34 |
35 | assert(
36 | cookie.name and cookie.value and (type(cookie.set) == 'function'),
37 | test.name
38 | )
39 | end,
40 | checker = function(tester, test, body, status, headers)
41 | end,
42 | },
43 |
44 | {
45 | name = 'empty_with_map',
46 | code = function(tester, test)
47 | local C = require('estrela.ngx.response.cookie'):new()
48 |
49 | local cookie = C:empty(demo_cookie)
50 | cookie.set = nil -- удаляю метод, чтобы tbl_cmp не ругалась
51 |
52 | assert(
53 | tester.tbl_cmp(
54 | cookie,
55 | demo_cookie
56 | ),
57 | test.name
58 | )
59 | end,
60 | checker = function(tester, test, body, status, headers)
61 | end,
62 | },
63 |
64 | -- set
65 | {
66 | name = 'set_via_multi_params',
67 | code = function(tester, test)
68 | local C = require('estrela.ngx.response.cookie'):new()
69 | C:set(demo_cookie.name, demo_cookie.value, demo_cookie.expires, demo_cookie.path, demo_cookie.domain,
70 | demo_cookie.secure, demo_cookie.httponly
71 | )
72 | end,
73 | checker = checker_demo_cookie,
74 | },
75 |
76 | {
77 | name = 'set_wo_name',
78 | code = function(tester, test)
79 | local C = require('estrela.ngx.response.cookie'):new()
80 |
81 | local await_err = 'missing name or value'
82 |
83 | local ok, err = C:set()
84 | assert(
85 | (not ok) and (err == await_err),
86 | test.name .. 'w/o name'
87 | )
88 |
89 | local ok, err = C:set('')
90 | assert(
91 | (not ok) and (err == await_err),
92 | test.name .. 'empty name'
93 | )
94 |
95 | local ok, err = C:set('name')
96 | assert(
97 | (not ok) and (err == await_err),
98 | test.name .. 'name w/o value'
99 | )
100 | end,
101 | checker = function(tester, test, body, status, headers)
102 | end,
103 | },
104 |
105 | {
106 | name = 'set_via_map',
107 | code = function(tester, test)
108 | local C = require('estrela.ngx.response.cookie'):new()
109 | C:set(demo_cookie)
110 | end,
111 | checker = checker_demo_cookie,
112 | },
113 |
114 | {
115 | name = 'set_via_empty_set',
116 | code = function(tester, test)
117 | local C = require('estrela.ngx.response.cookie'):new()
118 |
119 | local cookie = C:empty()
120 |
121 | for k, v in pairs(demo_cookie) do
122 | cookie[k] = v
123 | end
124 |
125 | cookie:set()
126 | end,
127 | checker = checker_demo_cookie,
128 | },
129 | }
130 |
131 | if ngx then
132 | return function(tester)
133 | return tester:multitest_srv(tests)
134 | end
135 | else
136 | return function(tester)
137 | return tester:multitest_cli(tests)
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/tests/tester/cli.lua:
--------------------------------------------------------------------------------
1 | local M = require('tests.tester.common')
2 |
3 | local cookie_file_name = os.tmpname()
4 |
5 | function M.exec(cmd, ignore_code)
6 | local stdout_name, stderr_name = os.tmpname(), os.tmpname()
7 | local cmd = string.format('%s >%s 2>%s', cmd, stdout_name, stderr_name)
8 | local code = os.execute(cmd)
9 |
10 | local stdout_fd, stderr_fd = io.open(stdout_name, 'rb'), io.open(stderr_name, 'rb')
11 | local stdout_buf, stderr_buf
12 |
13 | if (ignore_code or (code == 0)) and stdout_fd then
14 | stdout_buf = stdout_fd:read('*a')
15 | end
16 |
17 | if stderr_fd then
18 | stderr_buf = stderr_fd:read('*a')
19 | end
20 |
21 | os.remove(stdout_name)
22 | os.remove(stderr_name)
23 |
24 | return stdout_buf, stderr_buf or ''
25 | end
26 |
27 | function M.exec_by_lines(cmd)
28 | local out, err = M.exec(cmd)
29 | if out then
30 | local lines = M.split(M.trim(out), '\n')
31 | out = {}
32 | for _, line in ipairs(lines) do
33 | table.insert(out, M.trim(line))
34 | end
35 | end
36 | return out, err
37 | end
38 |
39 | function M.ls_tests(path)
40 | local lst, err = M.exec_by_lines('ls -1 ' .. path)
41 | if not lst then
42 | return lst, err
43 | end
44 |
45 | local tests = {}
46 |
47 | for _, file in ipairs(lst) do
48 | local m = string.match(file, [[^(.+).lua$]])
49 | if m then
50 | table.insert(tests, m)
51 | end
52 | end
53 |
54 | table.sort(tests)
55 |
56 | return tests
57 | end
58 |
59 | function M.http_req(url, args)
60 | local url = url
61 |
62 | if args then
63 | for k, v in pairs(args) do
64 | url = url .. '&' .. tostring(k) .. '=' .. tostring(v)
65 | end
66 | end
67 |
68 | local req = table.concat(
69 | {
70 | 'wget -S -q -O /dev/stdout -T 5 -w 1 --no-cache --keep-session-cookies',
71 | '--load-cookies', cookie_file_name,
72 | '--save-cookies', cookie_file_name,
73 | '"' .. url .. '"'
74 | },
75 | ' '
76 | )
77 |
78 | local body, err = M.exec(req, true)
79 | if not body then
80 | return nil, err
81 | end
82 |
83 | local status, headers_or_err = M.parse_response_header(err)
84 | if not status then
85 | return nil, headers_or_err
86 | end
87 |
88 | return body, status, headers_or_err
89 | end
90 |
91 | function M.req(...)
92 | local body, status, headers = M.http_req(...)
93 | if headers[M.error_header_name] then
94 | return error(headers[M.error_header_name])
95 | else
96 | return body, status, headers
97 | end
98 | end
99 |
100 | function M.check(orig, got)
101 | if orig == got then
102 | return true
103 | else
104 | return false, string.format([[Expected: %s, got: %s]], M.prepare_string(orig), M.prepare_string(got))
105 | end
106 | end
107 |
108 | function M.remove_cookies()
109 | os.remove(cookie_file_name)
110 | end
111 |
112 | function M.multitest_cli(self, tests)
113 | for _, test in ipairs(tests) do
114 | local body, status, headers = self.req(self.url, {name = test.name})
115 | local ok = test.checker(self, test, body, status, headers)
116 | if ok == nil then
117 | assert(status and (status.code == 200), test.name)
118 | end
119 | end
120 | return true
121 | end
122 |
123 | return M
124 |
--------------------------------------------------------------------------------
/tests/tester/common.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | M.error_header_name = 'x_tester_error'
4 |
5 | local json
6 |
7 | function M.rtrim(str, chars)
8 | chars = chars or '%s'
9 | local res = str:gsub('['..chars..']+$', '')
10 | return res
11 | end
12 |
13 | function M.ltrim(str, chars)
14 | chars = chars or '%s'
15 | local res = str:gsub('^['..chars..']+', '')
16 | return res
17 | end
18 |
19 | function M.trim(str, chars)
20 | chars = chars or '%s'
21 | return M.ltrim(M.rtrim(str, chars), chars)
22 | end
23 |
24 | function M.split(str, sep)
25 | if not string.find(str, sep, 1, true) then
26 | return {str}
27 | end
28 |
29 | local res = {}
30 |
31 | local prev_pos = 1
32 | while #res < #str - 1 do
33 | local pos, pos_t = string.find(str, sep, prev_pos, true)
34 | if not pos then
35 | res[#res + 1] = str:sub(prev_pos)
36 | prev_pos = #str + 1
37 | break
38 | end
39 |
40 | res[#res + 1] = str:sub(prev_pos, pos - 1)
41 | prev_pos = pos_t + 1
42 | end
43 |
44 | if prev_pos <= #str then
45 | res[#res + 1] = str:sub(prev_pos)
46 | end
47 |
48 | return res
49 | end
50 |
51 | function M.parse_response_header(header)
52 | local lines = M.split(M.trim(header), '\n')
53 |
54 | local ver, code, descr = string.match(lines[1], [[^HTTP/(%d.%d)%s(%d+)%s(.+)$]])
55 | if not ver or not code or not descr then
56 | return nil, 'Wrong response status line format'
57 | end
58 |
59 | local status = {
60 | version = ver,
61 | code = tonumber(code),
62 | descr = descr,
63 | }
64 |
65 | local headers = {}
66 |
67 | for i = 2, #lines do
68 | local line = M.trim(lines[i])
69 | local k, v = string.match(line, [[^([^:]+):%s*(.+)$]])
70 | if k and v then
71 | k = k:lower():gsub('-', '_')
72 | headers[k] = v
73 | else
74 | return nil, 'Wrong response header line format: ' .. line
75 | end
76 | end
77 |
78 | return status, headers
79 | end
80 |
81 | function M.prepare_string(s)
82 | local s_type = type(s)
83 | if s_type == 'nil' then
84 | return 'nil'
85 | elseif s_type == 'number' then
86 | return tostring(s)
87 | elseif s_type == 'string' then
88 | -- спец обработка \t и \n для сохранения ее в строке в текстовом формате
89 | s = string.gsub(s, '\t', [[\t]])
90 | s = string.gsub(s, '\n', [[\n]])
91 | s = string.format('%q', s)
92 | s = string.gsub(s, [[\\t]], [[\t]])
93 | s = string.gsub(s, [[\\n]], [[\n]])
94 | return s
95 | else
96 | return error('Unsupported type ' .. s_type)
97 | end
98 | end
99 |
100 | function M.tbl_cmpx(t1, t2, iter, cmp, ignore_mt)
101 | if type(t1) ~= 'table' or type(t2) ~= 'table' then
102 | return false
103 | end
104 |
105 | if not ignore_mt then
106 | local mt1 = getmetatable(t1)
107 | if type(mt1) == 'table' then
108 | local mt2 = getmetatable(t2)
109 | if type(mt2) ~= 'table' then
110 | return false
111 | end
112 |
113 | if not M.tbl_cmp(mt1, mt2) then
114 | return false
115 | end
116 | end
117 | end
118 |
119 | for k, v in iter(t1) do
120 | if type(v) == 'table' then
121 | if not cmp(t2[k], v, ignore_mt) then
122 | return false
123 | end
124 | elseif t2[k] ~= v then
125 | return false
126 | end
127 | end
128 | return true
129 | end
130 |
131 | function M.tbl_cmp(t1, t2, ignore_mt)
132 | return M.tbl_cmpx(t1, t2, pairs, M.tbl_cmp, ignore_mt)
133 | and M.tbl_cmpx(t2, t1, pairs, M.tbl_cmp, ignore_mt)
134 | end
135 |
136 | function M.tbl_cmpi(t1, t2, ignore_mt)
137 | return M.tbl_cmpx(t1, t2, ipairs, M.tbl_cmpi, ignore_mt)
138 | and M.tbl_cmpx(t2, t1, ipairs, M.tbl_cmpi, ignore_mt)
139 | end
140 |
141 | function M.je(...)
142 | if not json then
143 | json = require('cjson')
144 | json.encode_sparse_array(true)
145 | end
146 | return json.encode{...}
147 | end
148 |
149 | return M
150 |
--------------------------------------------------------------------------------
/tests/tester/ngx.lua:
--------------------------------------------------------------------------------
1 | local M = require('tests.tester.common')
2 |
3 | function M.multitest_srv(self, tests)
4 | local name = ngx.var.arg_name
5 | assert(name, 'Test name is not specified')
6 |
7 | local test
8 | for _, t in ipairs(tests) do
9 | if t.name == name then
10 | test = t
11 | break
12 | end
13 | end
14 | assert(test, 'Test with name [' .. tostring(name) .. '] is not found')
15 |
16 | return test.code(self, test)
17 | end
18 |
19 | return M
20 |
--------------------------------------------------------------------------------