-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from petsagouris/lpeg_routes
Introducing the LPEG only version of orbit/routes.lua
- Loading branch information
Showing
1 changed file
with
142 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,134 +1,181 @@ | ||
local setmetatable, type, ipairs, table, string = setmetatable, type, ipairs, table, string | ||
|
||
local lpeg = require "lpeg" | ||
local re = require "re" | ||
local util = require "wsapi.util" | ||
|
||
local _M = {} | ||
|
||
local function foldr(t, f, acc) | ||
for i = #t, 1, -1 do | ||
acc = f(t[i], acc) | ||
end | ||
return acc | ||
end | ||
local alpha = lpeg.R('AZ', 'az') | ||
local number = lpeg.R('09') | ||
local asterisk = lpeg.P('*') | ||
local question_mark = lpeg.P('?') | ||
local at_sign = lpeg.P('@') | ||
local colon = lpeg.P(':') | ||
local the_dot = lpeg.P('.') | ||
local underscore = lpeg.P('_') | ||
local forward_slash = lpeg.P('/') | ||
local slash_or_dot = forward_slash + the_dot | ||
|
||
local function foldl(t, f, acc) | ||
for i = 1, #t do | ||
acc = f(acc, t[i]) | ||
end | ||
return acc | ||
end | ||
local param = lpeg.C(slash_or_dot) * colon * lpeg.C((alpha + number + underscore)^1) * #(slash_or_dot + -1) / | ||
function (prefix, name, dot) | ||
local inner = (1 - lpeg.S('/' .. (dot or '')))^1 | ||
local close = lpeg.P'/' + (dot or -1) + -1 | ||
return { | ||
cap = lpeg.Carg(1) * slash_or_dot * lpeg.C(inner^1) * #close / function (params, item, delim) params[name] = util.url_decode(item) end, | ||
clean = slash_or_dot * inner^1 * #close, | ||
tag = "param", | ||
name = name, | ||
prefix = prefix | ||
} | ||
end | ||
|
||
local opt_param = lpeg.C(slash_or_dot) * question_mark * colon * lpeg.C((alpha + number + underscore)^1) * question_mark * #(forward_slash + -1) / | ||
function (prefix, name, dot) | ||
local inner = (1 - lpeg.S('/' .. (dot or '')))^1 | ||
local close = lpeg.P('/') + lpeg.P(dot or -1) + -1 | ||
return { | ||
cap = (lpeg.Carg(1) * slash_or_dot * lpeg.C(inner) * #close / function (params, item, delim) params[name] = util.url_decode(item) end)^-1, | ||
clean = (slash_or_dot * inner * #lpeg.C(close))^-1, | ||
tag = "opt", | ||
name = name, | ||
prefix = prefix | ||
} | ||
end | ||
|
||
local splat = lpeg.P(lpeg.C(forward_slash + the_dot) * asterisk * #(forward_slash + the_dot + -1)) / | ||
function (prefix) | ||
return { | ||
cap = "*", | ||
tag = "splat", | ||
prefix = prefix | ||
} | ||
end | ||
|
||
local param = re.compile[[ {[/%.]} ':' {[_%w]+} &('/' / {'.'} / !.) ]] / | ||
function (prefix, name, dot) | ||
local extra = { inner = (lpeg.P(1) - lpeg.S("/" .. (dot or "")))^1, | ||
close = lpeg.P"/" + lpeg.P(dot or -1) + lpeg.P(-1) } | ||
return { cap = lpeg.Carg(1) * re.compile([[ [/%.] {%inner+} &(%close) ]], extra) / | ||
function (params, item, delim) | ||
params[name] = util.url_decode(item) | ||
end, | ||
clean = re.compile([[ [/%.] %inner &(%close) ]], extra), | ||
tag = "param", name = name, prefix = prefix } | ||
end | ||
|
||
local opt_param = re.compile[[ {[/%.]} '?:' {[_%w]+} '?' &('/' / {'.'} / !.) ]] / | ||
function (prefix, name, dot) | ||
local extra = { inner = (lpeg.P(1) - lpeg.S("/" .. (dot or "")))^1, | ||
close = lpeg.P"/" + lpeg.P(dot or -1) + lpeg.P(-1) } | ||
return { cap = (lpeg.Carg(1) * re.compile([[ [/%.] {%inner+} &(%close) ]], extra) / | ||
function (params, item, delim) | ||
params[name] = util.url_decode(item) | ||
end)^-1, | ||
clean = re.compile([[ [/%.] %inner &(%close) ]], extra)^-1, | ||
tag = "opt", name = name, prefix = prefix } | ||
end | ||
|
||
local splat = re.compile[[ {[/%.]} {'*'} &('/' / '.' / !.) ]] / | ||
function (prefix) | ||
return { cap = "*", tag = "splat", prefix = prefix } | ||
end | ||
|
||
local rest = lpeg.C((lpeg.P(1) - param - opt_param - splat)^1) | ||
|
||
local function fold_caps(cap, acc) | ||
local rest = lpeg.C((1 - param - opt_param - splat)^1) | ||
|
||
local function fold_captures(cap, acc) | ||
if type(cap) == "string" then | ||
return { cap = lpeg.P(cap) * acc.cap, clean = lpeg.P(cap) * acc.clean } | ||
elseif cap.cap == "*" then | ||
return { cap = (lpeg.Carg(1) * (lpeg.P(cap.prefix) * lpeg.C((lpeg.P(1) - acc.clean)^0))^-1 / | ||
function (params, splat) | ||
if not params.splat then params.splat = {} end | ||
if splat and splat ~= "" then | ||
params.splat[#params.splat+1] = util.url_decode(splat) | ||
end | ||
end) * acc.cap, | ||
clean = (lpeg.P(cap.prefix) * (lpeg.P(1) - acc.clean)^0)^-1 * acc.clean } | ||
else | ||
return { cap = cap.cap * acc.cap, clean = cap.clean * acc.clean } | ||
return { | ||
cap = lpeg.P(cap) * acc.cap, | ||
clean = lpeg.P(cap) * acc.clean | ||
} | ||
end | ||
|
||
-- if we have a star match (match everything) | ||
if cap.cap == "*" then | ||
return { | ||
cap = (lpeg.Carg(1) * (cap.prefix * lpeg.C((1 - acc.clean)^0))^-1 / | ||
function (params, splat) | ||
params.splat = params.splat or {} | ||
if splat and splat ~= "" then | ||
params.splat[#params.splat+1] = util.url_decode(splat) | ||
end | ||
end) * acc.cap, | ||
clean = (cap.prefix * (1 - acc.clean)^0)^-1 * acc.clean | ||
} | ||
end | ||
|
||
return { | ||
cap = cap.cap * acc.cap, | ||
clean = cap.clean * acc.clean | ||
} | ||
end | ||
|
||
local function fold_parts(parts, cap) | ||
if type(cap) == "string" then | ||
parts[#parts+1] = { tag = "text", text = cap } | ||
else | ||
parts[#parts+1] = { tag = cap.tag, prefix = cap.prefix, name = cap.name } | ||
|
||
if type(cap) == "string" then -- if the capture is a string | ||
parts[#parts+1] = { | ||
tag = "text", | ||
text = cap | ||
} | ||
else -- it must be a table capture | ||
parts[#parts+1] = { | ||
tag = cap.tag, | ||
prefix = cap.prefix, | ||
name = cap.name | ||
} | ||
end | ||
|
||
return parts | ||
end | ||
|
||
local route = lpeg.Ct((param + opt_param + splat + rest)^1 * lpeg.P(-1)) / | ||
function (caps) | ||
return foldr(caps, fold_caps, { cap = lpeg.P("/")^-1 * lpeg.P(-1), clean = lpeg.P("/")^-1 * lpeg.P(-1) }), | ||
foldl(caps, fold_parts, {}) | ||
end | ||
-- the right part (a bottom to top loop) | ||
local function fold_right(t, f, acc) | ||
for i = #t, 1, -1 do | ||
acc = f(t[i], acc) | ||
end | ||
return acc | ||
end | ||
|
||
-- the left part (a top to bottom loop) | ||
local function fold_left(t, f, acc) | ||
for i = 1, #t do | ||
acc = f(acc, t[i]) | ||
end | ||
return acc | ||
end | ||
|
||
local route = lpeg.Ct((param + opt_param + splat + rest)^0 * -1) / function (caps) | ||
local forward_slash_at_end = lpeg.P('/')^-1 * -1 | ||
return fold_right(caps, fold_captures, { cap = forward_slash_at_end, clean = forward_slash_at_end }), fold_left(caps, fold_parts, {}) | ||
end | ||
|
||
local function build(parts, params) | ||
local res = {} | ||
local i = 1 | ||
local res, i = {}, 1 | ||
params = params or {} | ||
params.splat = params.splat or {} | ||
for _, part in ipairs(parts) do | ||
if part.tag == "param" then | ||
if not params[part.name] then | ||
error("route parameter " .. part.name .. " does not exist") | ||
end | ||
local s = string.gsub (params[part.name], "([^%.@]+)", | ||
function (s) return util.url_encode(s) end) | ||
if part.tag == 'param' then | ||
if not params[part.name] then error('route parameter ' .. part.name .. ' does not exist') end | ||
local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end) | ||
res[#res+1] = part.prefix .. s | ||
elseif part.tag == "splat" then | ||
local s = string.gsub (params.splat[i] or "", "([^/%.@]+)", | ||
function (s) return util.url_encode(s) end) | ||
local s = string.gsub (params.splat[i] or '', '([^/%.@]+)', function (s) return util.url_encode(s) end) | ||
res[#res+1] = part.prefix .. s | ||
i = i + 1 | ||
elseif part.tag == "opt" then | ||
if params and params[part.name] then | ||
local s = string.gsub (params[part.name], "([^%.@]+)", | ||
function (s) return util.url_encode(s) end) | ||
res[#res+1] = part.prefix .. s | ||
local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end) | ||
res[#res+1] = part.prefix .. s | ||
end | ||
else | ||
res[#res+1] = part.text | ||
end | ||
end | ||
if #res > 0 then return table.concat(res) else return "/" end | ||
|
||
if #res > 0 then | ||
return table.concat(res) | ||
end | ||
|
||
return '/' | ||
end | ||
|
||
function _M.R(path) | ||
local p, b = route:match(path) | ||
return setmetatable({ parser = p.cap, parts = b }, | ||
{ __index = { | ||
match = function (t, s) | ||
local params = {} | ||
if t.parser:match(s, 1, params) then | ||
return params | ||
else | ||
return nil | ||
end | ||
end, | ||
build = function (t, params) | ||
return build(t.parts, params) | ||
end | ||
} }) | ||
local p, b = route:match(path) | ||
|
||
return setmetatable({ | ||
parser = p.cap, | ||
parts = b | ||
}, { | ||
__index = { | ||
match = function (t, s) | ||
local params = {} | ||
if t.parser:match(s, 1, params) then | ||
return params | ||
end | ||
return nil | ||
end, | ||
build = function (t, params) | ||
return build(t.parts, params) | ||
end | ||
} | ||
}) | ||
|
||
end | ||
|
||
return setmetatable(_M, { __call = function (_, path) return _M.R(path) end }) | ||
return setmetatable(_M, { | ||
__call = function (_, path) | ||
return _M.R(path) | ||
end | ||
}) |