Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 0895398

Browse files
committed
graphql: implement expressions executor
Implement c-style filter expressions for graphql objects. Introduced executor into expressions module. Moved regexp implementation into utils.lua. Needed for #13
1 parent 5b9dca8 commit 0895398

File tree

6 files changed

+341
-74
lines changed

6 files changed

+341
-74
lines changed

graphql/accessor_general.lua

+3-26
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ local json = require('json')
99
local avro_schema = require('avro_schema')
1010
local utils = require('graphql.utils')
1111
local clock = require('clock')
12-
local bit = require('bit')
13-
local rex, is_pcre2 = utils.optional_require_rex()
12+
local rex = utils.optional_require_rex()
1413
local avro_helpers = require('graphql.avro_helpers')
1514
local db_schema_helpers = require('graphql.db_schema_helpers')
1615
local error_codes = require('graphql.error_codes')
@@ -787,30 +786,8 @@ local function match_using_re(obj, pcre)
787786
if type(re) == 'table' then
788787
local match = match_using_re(obj[field_name], re)
789788
if not match then return false end
790-
else
791-
local flags = rex.flags()
792-
-- emulate behaviour of (?i) on libpcre (libpcre2 supports it)
793-
local cfg = 0
794-
if not is_pcre2 then
795-
local cnt
796-
re, cnt = re:gsub('^%(%?i%)', '')
797-
if cnt > 0 then
798-
cfg = bit.bor(cfg, flags.CASELESS)
799-
end
800-
end
801-
-- enable UTF-8
802-
if is_pcre2 then
803-
cfg = bit.bor(cfg, flags.UTF)
804-
cfg = bit.bor(cfg, flags.UCP)
805-
else
806-
cfg = bit.bor(cfg, flags.UTF8)
807-
cfg = bit.bor(cfg, flags.UCP)
808-
end
809-
-- XXX: compile re once
810-
local re = rex.new(re, cfg)
811-
if not re:match(obj[field_name]) then
812-
return false
813-
end
789+
elseif not utils.regexp(re, obj[field_name]) then
790+
return false
814791
end
815792
end
816793

graphql/expressions.lua

+224-32
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
local lpeg = require('lulpeg')
2+
local utils = require('graphql.utils')
3+
4+
--- NOTE: Functions that worth moving out into other modules:
5+
--- 1) Regexp implementation (also lies inside of
6+
--- accessor_general.lua)
7+
--- 2) Vararg iterator.
8+
9+
--- TODO:
10+
--- 1) Validation.
11+
--- 2) Big numbers.
12+
--- 3) Absence of variable in context.variables is equal
13+
--- to the situation when context.variables.var == nil.
214

315
local expressions = {}
416
local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V
@@ -28,8 +40,8 @@ local field_path = identifier * ('.' * identifier) ^ 0
2840
-- Possible logical function patterns:
2941
-- 1) is_null.
3042
local is_null = P('is_null')
31-
-- 2) not_null.
32-
local not_null = P('not_null')
43+
-- 2) is_not_null.
44+
local is_not_null = P('is_not_null')
3345
-- 3) regexp.
3446
local regexp = P('regexp')
3547

@@ -63,31 +75,6 @@ local lt = P('<')
6375
-- 10) <=
6476
local le = P('<=')
6577

66-
--- Utility functions:
67-
--- 1) Vararg iterator.
68-
69-
70-
local function vararg(...)
71-
local i = 0
72-
local t = {}
73-
local limit
74-
local function iter(...)
75-
i = i + 1
76-
if i > limit then return end
77-
return i, t[i]
78-
end
79-
80-
i = 0
81-
limit = select("#", ...)
82-
for n = 1, limit do
83-
t[n] = select(n, ...)
84-
end
85-
for n = limit + 1, #t do
86-
t[n] = nil
87-
end
88-
return iter
89-
end
90-
9178
-- AST nodes generating functions.
9279
local function identical(arg)
9380
return arg
@@ -110,7 +97,8 @@ local function bin_op_node(...)
11097
end
11198
local operators = {}
11299
local operands = {}
113-
for i, v in vararg(...) do
100+
for i = 1, select('#', ...) do
101+
local v = select(i, ...)
114102
if i % 2 == 0 then
115103
table.insert(operators, v)
116104
else
@@ -193,7 +181,7 @@ local _logic_and = logic_and / op_name
193181
local _comparison_op = (eq + not_eq + ge + gt + le + lt) / op_name
194182
local _arithmetic_op = (addition + subtraction) / op_name
195183
local _unary_op = (negation + unary_minus + unary_plus) / op_name
196-
local _functions = (is_null + not_null + regexp) / identical
184+
local _functions = (is_null + is_not_null + regexp) / identical
197185

198186
-- Grammar rules for C-style expressions positioned ascending in
199187
-- terms of priority.
@@ -220,14 +208,218 @@ local expression_grammar = P {
220208
value_terminal = (_literal + _variable + _field_path) / identical_node
221209
}
222210

211+
-- Add one number to another.
212+
--
213+
-- It may be changed after introduction of "big ints".
214+
--
215+
-- @param operand_1
216+
-- @param operand_2
217+
local function sum(operand_1, operand_2)
218+
return operand_1 + operand_2
219+
end
220+
221+
-- Subtract one number from another.
222+
--
223+
-- It may be changed after introduction of "big ints".
224+
--
225+
-- @param operand_1
226+
-- @param operand_2
227+
local function subtract(operand_1, operand_2)
228+
return operand_1 - operand_2
229+
end
230+
223231
--- Parse given string which supposed to be a c-style expression.
224232
---
225-
--- @tparam str string representation of expression.
233+
--- @tparam string str string representation of expression
226234
---
227-
--- @treturn syntax tree.
228-
function expressions.parse(str)
235+
--- @treturn table syntax tree
236+
local function parse(str)
229237
assert(type(str) == 'string', 'parser expects a string')
230238
return expression_grammar:match(str) or error('syntax error')
231239
end
232240

241+
--local function validate(context)
242+
--
243+
--end
244+
245+
--- Recursively execute the syntax subtree. Of course it can be
246+
--- syntax tree itself.
247+
---
248+
--- @tparam table node node to be executed
249+
---
250+
--- @tparam table context table containing information useful for
251+
--- execution (see @{expressions.new})
252+
---
253+
--- @return subtree value
254+
local function execute_node(node, context)
255+
if node.kind == 'const' then
256+
if node.value_class == 'string' then
257+
return node.value
258+
end
259+
260+
if node.value_class == 'bool' then
261+
if node.value == 'false' then
262+
return false
263+
end
264+
return true
265+
end
266+
267+
if node.value_class == 'number' then
268+
return tonumber(node.value)
269+
end
270+
end
271+
272+
if node.kind == 'variable' then
273+
local name = node.name
274+
return context.variables[name]
275+
end
276+
277+
if node.kind == 'object_field' then
278+
local path = node.path
279+
local field = context.object
280+
local table_path = (path:split('.'))
281+
for i = 1, #table_path do
282+
field = field[table_path[i]]
283+
end
284+
return field
285+
end
286+
287+
if node.kind == 'func' then
288+
-- regexp() implementation.
289+
if node.name == 'regexp' then
290+
return utils.regexp(execute_node(node.args[1], context),
291+
execute_node(node.args[2], context))
292+
end
293+
294+
-- is_null() implementation.
295+
if node.name == 'is_null' then
296+
return execute_node(node.args[1], context) == nil
297+
end
298+
299+
-- is_not_null() implementation.
300+
if node.name == 'is_not_null' then
301+
return execute_node(node.args[1], context) ~= nil
302+
end
303+
end
304+
305+
if node.kind == 'unary_operation' then
306+
-- Negation.
307+
if node.op == '!' then
308+
return not execute_node(node.node, context)
309+
end
310+
311+
-- Unary '+'.
312+
if node.op == '+' then
313+
return execute_node(node.node, context)
314+
end
315+
316+
-- Unary '-'.
317+
if node.op == '-' then
318+
return -execute_node(node.node, context)
319+
end
320+
end
321+
322+
if node.kind == 'binary_operations' then
323+
local prev = execute_node(node.operands[1], context)
324+
for i, op in ipairs(node.operators) do
325+
local second_operand = execute_node(node.operands[i + 1],
326+
context)
327+
-- Sum.
328+
if op == '+' then
329+
prev = sum(prev, second_operand)
330+
end
331+
332+
-- Subtraction.
333+
if op == '-' then
334+
prev = subtract(prev, second_operand)
335+
end
336+
337+
-- Logical and.
338+
if op == '&&' then
339+
prev = prev and second_operand
340+
end
341+
342+
-- Logical or.
343+
if op == '||' then
344+
prev = prev or second_operand
345+
end
346+
347+
-- Equal.
348+
if op == '==' then
349+
prev = prev == second_operand
350+
end
351+
352+
-- Not equal.
353+
if op == '!=' then
354+
prev = prev ~= second_operand
355+
end
356+
357+
-- Greater than.
358+
if op == '>' then
359+
prev = prev > second_operand
360+
end
361+
362+
-- Greater or equal.
363+
if op == '>=' then
364+
prev = prev >= second_operand
365+
end
366+
367+
-- Lower than.
368+
if op == '<' then
369+
prev = prev < second_operand
370+
end
371+
372+
-- Lower or equal.
373+
if op == '&&' then
374+
prev = prev and second_operand
375+
end
376+
end
377+
return prev
378+
end
379+
380+
if node.kind == 'root_expression' then
381+
return execute_node(node.expr, context)
382+
end
383+
end
384+
385+
local function expr_execute(self, object, variables)
386+
local context = {
387+
object = object,
388+
variables = variables,
389+
}
390+
return execute_node(self.ast, context)
391+
end
392+
393+
--- Compile and execute given string that represents a c-style
394+
--- expression.
395+
---
396+
--- @tparam string str string representation of expression
397+
--- @tparam table object object considered inside of an
398+
--- expression
399+
--- @tparam table variables list of variables
400+
---
401+
--- @return expression value
402+
function expressions.execute(str, object, variables)
403+
local expr = expressions.new(str)
404+
return expr:execute(object, variables)
405+
end
406+
407+
--- Create a new c-style expression object.
408+
---
409+
--- @tparam string str string representation of expression
410+
---
411+
--- @treturn table expression object
412+
function expressions.new(str)
413+
local ast = parse(str)
414+
415+
return setmetatable({
416+
raw = str,
417+
ast = ast,
418+
}, {
419+
__index = {
420+
execute = expr_execute,
421+
}
422+
})
423+
end
424+
233425
return expressions

graphql/utils.lua

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
local json = require('json')
44
local log = require('log')
55
local ffi = require('ffi')
6+
local bit = require('bit')
67

78
local utils = {}
89

@@ -306,4 +307,36 @@ function utils.debug(data_or_func, ...)
306307
end
307308
end
308309

310+
--- Compare pattern with a string using pcre.
311+
---
312+
--- @param pattern matching pattern
313+
---
314+
--- @param string string to match
315+
---
316+
--- @return true or false
317+
function utils.regexp(pattern, string)
318+
local rex, is_pcre2 = utils.optional_require_rex()
319+
local flags = rex.flags()
320+
local cfg = 0
321+
if not is_pcre2 then
322+
local cnt
323+
pattern, cnt = pattern:gsub('^%(%?i%)', '')
324+
if cnt > 0 then
325+
cfg = bit.bor(cfg, flags.CASELESS)
326+
end
327+
end
328+
if is_pcre2 then
329+
cfg = bit.bor(cfg, flags.UTF)
330+
cfg = bit.bor(cfg, flags.UCP)
331+
else
332+
cfg = bit.bor(cfg, flags.UTF8)
333+
cfg = bit.bor(cfg, flags.UCP)
334+
end
335+
local pattern = rex.new(pattern, cfg)
336+
if not pattern:match(string) then
337+
return false
338+
end
339+
return true
340+
end
341+
309342
return utils

0 commit comments

Comments
 (0)