Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions jsonpath-pico.1.1.4.rockspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package = 'jsonpath'
version = 'pico.1.1.4-1'
source = {
url = 'git+https://github.com/picodata/lua-jsonpath',
tag = '1.1.4'
}
description = {
summary = 'Query Lua data structures with JsonPath expressions. Robust and safe JsonPath engine for Lua.',
detailed = [[
This library implements Stefan Goessner's JsonPath syntax (http://goessner.net/articles/JsonPath/) in Lua.

Lua JsonPath is compatible with David Chester's Javascript implementation (https://github.com/dchester/jsonpath).

The Lua JsonPath library was written from scratch by Frank Edelhaeuser. It's a pure Lua implementation based on a PEG grammer handled by LulPeg pattern-matching library (https://github.com/pygy/LuLPeg.git).
]],
homepage = 'https://github.com/tarantool/lua-jsonpath',
license = 'MIT'
}
dependencies = {
'lua >= 5.1',
'lulpeg ~> pico.0.1.3-1'
}
build = {
type = 'builtin',
modules = {
jsonpath = 'jsonpath.lua'
}
}
145 changes: 112 additions & 33 deletions jsonpath.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@
]]--
local M = {}

local codes = {
BAD_REQUEST = 400,
NOT_FOUND = 404,
INTERNAL_ERR = 500,
}

local errors = require('errors')

local JsonPathError = errors.new_class("JsonPathError")
local JsonPathNotFoundError = errors.new_class("JsonPathNotFoundError")

local ffi = require('ffi')

-- Use Roberto Ierusalimschy's fabulous LulPeg pattern-matching library
Expand Down Expand Up @@ -286,6 +297,24 @@ local jsonpath_grammer = (function()
return jsonpath
end)()

local function bad_request_error(err)
local err = JsonPathError:new(err)
err.rc = codes.BAD_REQUEST
return err
end

local function not_found_error(err)
local err = JsonPathNotFoundError:new(err)
err.rc = codes.NOT_FOUND
return err
end

local function internal_error(err)
local err = JsonPathNotFoundError:new(err)
err.rc = codes.INTERNAL_ERR
return err
end

--- @alias Operator 1|2|3|4|5|6|7|8|9|10|11|12|13
--- @alias OperatorType 1|2|3|4

Expand Down Expand Up @@ -440,18 +469,19 @@ local function exec_binary_op(op, lval, rval, op_str)
if l_type == "string" then
lval = tonumber(lval)
if lval == nil then
return nil, ("can not parse string lvalue as number for operation %s"):format(op_str)
return nil, bad_request_error(("can not parse string lvalue as number for operation %s"):format(op_str))
end
elseif l_type ~= "number" then
return nil, ("lvalue is not a number for operation %s"):format(op_str)
return nil, bad_request_error(("lvalue is not a number for operation %s"):format(op_str))
end
if r_type == "string" then
rval = tonumber(rval)
if rval == nil then
return nil, ("can not parse string rvalue as number for operation %s"):format(op_str)
return nil,
bad_request_error(("can not parse string rvalue as number for operation %s"):format(op_str))
end
elseif r_type ~= "number" then
return nil, ("rvalue is not a number for operation %s"):format(op_str)
return nil, bad_request_error(("rvalue is not a number for operation %s"):format(op_str))
end
elseif op_type == OPERATOR_TYPES.LOGICAL then
-- everything which is not null is a true boolean
Expand Down Expand Up @@ -539,10 +569,10 @@ local function exec_binary_op(op, lval, rval, op_str)

-- must be the same type
if l_type ~= r_type then
return nil, ("can not apply %s on types %s and %s"):format(op_str, l_type, r_type)
return nil, bad_request_error(("can not apply %s on types %s and %s"):format(op_str, l_type, r_type))
end
else
return nil, ("unknown operator %s"):format(op_str)
return nil, bad_request_error(("unknown operator %s"):format(op_str))
end

return OPERATORS_FN[op](lval, rval), nil
Expand All @@ -554,10 +584,10 @@ local function eval_ast(ast, obj)
-- Helper helper: evaluate variable expression inside abstract syntax tree
local function eval_var(expr, obj)
if obj == nil then
return nil, 'object is not set'
return nil, bad_request_error('object is not set')
end
if type(obj) ~= "table" then
return nil, 'object is primitive'
return nil, not_found_error('object is primitive')
end
for i = 2, #expr do
-- [1] is "var"
Expand All @@ -568,7 +598,7 @@ local function eval_ast(ast, obj)
member = type(member) == 'number' and member + 1 or member
obj = obj[member]
if is_nil(obj) then
return nil, 'object doesn\'t contain an object or attribute "' .. member .. '"'
return nil, not_found_error('object doesn\'t contain an object or attribute "'.. member ..'"')
end
end
return obj
Expand All @@ -585,7 +615,10 @@ local function eval_ast(ast, obj)
local function eval_union(expr, obj)
local matches = {} -- [1] is "union"
for i = 2, #expr do
local result = eval_ast(expr[i], obj)
local result, err = eval_ast(expr[i], obj)
if err then
return nil, err
end
if type(result) == 'table' then
for _, j in ipairs(result) do
table.insert(matches, j)
Expand All @@ -599,16 +632,31 @@ local function eval_ast(ast, obj)

-- Helper helper: evaluate 'filter' expression inside abstract syntax tree
local function eval_filter(expr, obj)
return eval_ast(expr[2], obj) and true or false
local result, err = eval_ast(expr[2], obj)
if err then
if err.rc == codes.NOT_FOUND then
return false
end
return nil, err
end
return result and true or false
end

-- Helper helper: evaluate 'slice' expression inside abstract syntax tree
local function eval_slice(expr, obj)
local matches = {} -- [1] is "slice"
if #expr == 4 then
local from = tonumber(eval_ast(expr[2], obj))
local to = tonumber(eval_ast(expr[3], obj))
local step = tonumber(eval_ast(expr[4], obj))
local from_result, err = eval_ast(expr[2], obj)
if err then return nil, err end
local to_result, err = eval_ast(expr[3], obj)
if err then return nil, err end
local step_result, err = eval_ast(expr[4], obj)
if err then return nil, err end

local from = tonumber(from_result)
local to = tonumber(to_result)
local step = tonumber(step_result)

if (from == nil) or (from < 0) or (to == nil) or (to < 0) then
local len = eval_var_length(obj)
if from == nil then
Expand Down Expand Up @@ -638,15 +686,15 @@ local function eval_ast(ast, obj)
for i = 3, #expr, 2 do
local op_str = expr[i]
if op_str == nil then
return nil, 'missing expression operator'
return nil, bad_request_error('missing expression operator')
end
local op2, eval_err = eval_ast(expr[i + 1], obj)
if is_nil(op2) then
return nil, eval_err
end
local op = parse_operator(op_str)
if op == 0 then
return nil, "unknown operator"
return nil, bad_request_error("unknown operator")
end
--- @cast op Operator
local result, cast_err = exec_binary_op(op, op1, op2, op_str)
Expand All @@ -672,8 +720,7 @@ local function eval_ast(ast, obj)
elseif ast[1] == 'filter' then
return eval_filter(ast, obj)
elseif ast[1] == 'slice' then
local result = eval_slice(ast, obj)
return result
return eval_slice(ast, obj)
end

return 0
Expand Down Expand Up @@ -705,7 +752,10 @@ local function match_path(ast, path, parent, obj)
end
elseif ast_spec[1] == 'union' or ast_spec[1] == 'slice' then
-- match union or slice expression (on parent object)
local matches = eval_ast(ast_spec, parent)
local matches, err = eval_ast(ast_spec, parent)
if err then
return nil, err
end
--- @cast matches table[]
for _, i in pairs(matches) do
match_component = tostring(i) == tostring(component)
Expand All @@ -715,7 +765,16 @@ local function match_path(ast, path, parent, obj)
end
elseif ast_spec[1] == 'filter' then
-- match filter expression
match_component = eval_ast(ast_spec, obj) and true or false
local filter_result, err = eval_ast(ast_spec, obj)
if err then
if err.rc == codes.NOT_FOUND then
match_component = false
else
return nil, err
end
else
match_component = filter_result and true or false
end
end
else
if ast_spec == '*' then
Expand All @@ -734,7 +793,16 @@ local function match_path(ast, path, parent, obj)
if path_index == #path and ast_spec ~= "array" and match_component then
local _, next_ast_spec = next(ast, ast_key)
if next_ast_spec ~= nil and next_ast_spec[1] == 'filter' then
match_component = eval_ast(next_ast_spec, obj) and true or false
local filter_result, err = eval_ast(next_ast_spec, obj)
if err then
if err.rc == codes.NOT_FOUND then
match_component = false
else
return nil, err
end
else
match_component = filter_result and true or false
end
ast_key, ast_spec = ast_iter(ast, ast_key)
end
end
Expand Down Expand Up @@ -769,7 +837,10 @@ end

local function match_tree(nodes, ast, path, parent, obj, count)
-- Try to match every node against AST
local match = match_path(ast, path, parent, obj)
local match, err = match_path(ast, path, parent, obj)
if err then
return err
end
if match == MATCH_ONE or match == MATCH_DESCENDANTS then
-- This node matches. Add path and value to result
-- (if max result count not yet reached)
Expand All @@ -792,7 +863,10 @@ local function match_tree(nodes, ast, path, parent, obj, count)
table.insert(path1, p)
end
table.insert(path1, type(key) == 'string' and key or (key - 1))
match_tree(nodes, ast, path1, obj, child, count)
local err = match_tree(nodes, ast, path1, obj, child, count)
if err then
return err
end
end
end
end
Expand All @@ -818,15 +892,15 @@ end
--
function M.parse(expr)
if expr == nil or type(expr) ~= 'string' then
return nil, "missing or invalid 'expr' argument"
return nil, bad_request_error("missing or invalid 'expr' argument")
end

local ast = Ct(jsonpath_grammer * Cp()):match(expr)
if ast == nil or #ast ~= 2 then
return nil, 'invalid expression "' .. expr .. '"'
return nil, bad_request_error('invalid expression "' .. expr .. '"')
end
if ast[2] ~= #expr + 1 then
return nil, 'invalid expression "' .. expr .. '" near "' .. expr:sub(ast[2]) .. '"'
return nil, bad_request_error('invalid expression "' .. expr .. '" near "' .. expr:sub(ast[2]) .. '"')
end
return ast[1]
end
Expand All @@ -850,13 +924,13 @@ end
--
function M.nodes(obj, expr, count)
if obj == nil or type(obj) ~= 'table' then
return nil, "missing or invalid 'obj' argument"
return nil, bad_request_error("missing or invalid 'obj' argument")
end
if expr == nil or (type(expr) ~= 'string' and type(expr) ~= 'table') then
return nil, "missing or invalid 'expr' argument"
return nil, bad_request_error("missing or invalid 'expr' argument")
end
if count ~= nil and type(count) ~= 'number' then
return nil, "invalid 'count' argument"
return nil, bad_request_error("invalid 'count' argument")
end

local ast, err
Expand All @@ -868,7 +942,10 @@ function M.nodes(obj, expr, count)
ast = expr
end
if ast == nil then
return nil, err or 'internal error'
if not err then
err = internal_error("internal error")
end
return nil, err
end

if count ~= nil and count == 0 then
Expand All @@ -885,8 +962,10 @@ function M.nodes(obj, expr, count)
end

local matches = {}
match_tree(matches, ast, { '$' }, {}, obj, count)

local err = match_tree(matches, ast, { '$' }, {}, obj, count)
if err then
return nil, err
end
-- Sort results by path
local sorted = {}
for p, v in pairs(matches) do
Expand Down Expand Up @@ -938,7 +1017,7 @@ function M.value(obj, expr, count)
return nodes[1].value
end

return nil, 'no element matching expression'
return nil, bad_request_error('no element matching expression')
end


Expand Down
Loading