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

Commit 4beadc9

Browse files
committed
graphql: implement expressions parser and executor
Implement c-style filter expressions for graphql objects. Introduced module containing parser and executor for them. Needed for #13
1 parent 5b9dca8 commit 4beadc9

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

graphql/expressions.lua

+239
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
local lpeg = require('lulpeg')
2+
local utils = require('graphql.utils')
3+
local rex, is_pcre2 = utils.optional_require_rex()
4+
local bit = require('bit')
5+
6+
--- NOTE: Functions that worth moving out into other modules:
7+
--- 1) Regexp implementation (also lies inside of
8+
--- accessor_general.lua)
9+
--- 2) Vararg iterator.
10+
11+
--- TODO:
12+
--- 1) Validation.
13+
--- 2) Big numbers.
14+
--- 3) Absence of variable in context.variables is equal
15+
--- to the situation when context.variables.var == nil.
16+
--- 4) In case you change string inside of an expression
17+
--- object you need to manually change ast in object.
218

319
local expressions = {}
420
local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V
@@ -220,6 +236,26 @@ local expression_grammar = P {
220236
value_terminal = (_literal + _variable + _field_path) / identical_node
221237
}
222238

239+
-- Add one number to another.
240+
--
241+
-- It may be changed after introduction of "big ints".
242+
--
243+
-- @param operand_1
244+
-- @param operand_2
245+
--
246+
local function sum(operand_1, operand_2)
247+
return operand_1 + operand_2
248+
end
249+
250+
-- Subtract one number from another.
251+
--
252+
-- It may be changed after introduction of "big ints".
253+
-- @param operand_1
254+
-- @param operand_2
255+
local function subtract(operand_1, operand_2)
256+
return operand_1 - operand_2
257+
end
258+
223259
--- Parse given string which supposed to be a c-style expression.
224260
---
225261
--- @tparam str string representation of expression.
@@ -230,4 +266,207 @@ function expressions.parse(str)
230266
return expression_grammar:match(str) or error('syntax error')
231267
end
232268

269+
--function expressions.validate(context)
270+
--
271+
--end
272+
273+
--- Recursively execute the syntax subtree. Of course it can be
274+
--- syntax tree itself.
275+
---
276+
--- @tparam node node to be executed.
277+
---
278+
--- @tparam context table containing information useful for
279+
--- execution (see @{expressions.new}).
280+
---
281+
--- @treturn subtree value.
282+
function expressions.execute(node, context)
283+
if node.kind == 'const' then
284+
if node.value_class == 'string' then
285+
return node.value
286+
end
287+
288+
if node.value_class == 'bool' then
289+
if node.value == 'false' then
290+
return false
291+
end
292+
return true
293+
end
294+
295+
if node.value_class == 'number' then
296+
return tonumber(node.value)
297+
end
298+
end
299+
300+
if node.kind == 'variable' then
301+
local name = node.name
302+
return context.variables[name]
303+
end
304+
305+
if node.kind == 'object_field' then
306+
local path = node.path
307+
local field = context.object
308+
local table_path = (path:split('.'))
309+
for i = 1, #table_path do
310+
field = field[table_path[i]]
311+
end
312+
return field
313+
end
314+
315+
if node.kind == 'func' then
316+
-- regexp() implementation.
317+
if node.name == 'regexp' then
318+
local flags = rex.flags()
319+
local cfg = 0
320+
local pattern = expressions.execute(node.args[1], context)
321+
local str = expressions.execute(node.args[2], context)
322+
if not is_pcre2 then
323+
local cnt
324+
pattern, cnt = pattern:gsub('^%(%?i%)', '')
325+
if cnt > 0 then
326+
cfg = bit.bor(cfg, flags.CASELESS)
327+
end
328+
end
329+
if is_pcre2 then
330+
cfg = bit.bor(cfg, flags.UTF)
331+
cfg = bit.bor(cfg, flags.UCP)
332+
else
333+
cfg = bit.bor(cfg, flags.UTF8)
334+
cfg = bit.bor(cfg, flags.UCP)
335+
end
336+
local pattern = rex.new(pattern, cfg)
337+
if not pattern:match(str) then
338+
return false
339+
end
340+
return true
341+
end
342+
343+
-- is_null() implementation.
344+
if node.name == 'is_null' then
345+
return expressions.execute(node.args[1], context) == nil
346+
end
347+
348+
-- not_null() implementation.
349+
if node.name == 'not_null' then
350+
return expressions.execute(node.args[1], context) ~= nil
351+
end
352+
end
353+
354+
if node.kind == 'unary_operation' then
355+
-- Negation.
356+
if node.op == '!' then
357+
return not expressions.execute(node.node, context)
358+
end
359+
360+
-- Unary '+'.
361+
if node.op == '+' then
362+
return expressions.execute(node.node, context)
363+
end
364+
365+
-- Unary '-'.
366+
if node.op == '-' then
367+
return -expressions.execute(node.node, context)
368+
end
369+
end
370+
371+
if node.kind == 'binary_operations' then
372+
local prev = expressions.execute(node.operands[1], context)
373+
for i, op in ipairs(node.operators) do
374+
local second_operand = expressions.execute(node.operands[i + 1],
375+
context)
376+
-- Sum.
377+
if op == '+' then
378+
prev = sum(prev, second_operand)
379+
end
380+
381+
-- Subtraction.
382+
if op == '-' then
383+
prev = subtract(prev, second_operand)
384+
end
385+
386+
-- Logical and.
387+
if op == '&&' then
388+
prev = prev and second_operand
389+
end
390+
391+
-- Logical or.
392+
if op == '||' then
393+
prev = prev or second_operand
394+
end
395+
396+
-- Equal.
397+
if op == '==' then
398+
prev = prev == second_operand
399+
end
400+
401+
-- Not equal.
402+
if op == '!=' then
403+
prev = prev ~= second_operand
404+
end
405+
406+
-- Greater than.
407+
if op == '>' then
408+
prev = prev > second_operand
409+
end
410+
411+
-- Greater or equal.
412+
if op == '>=' then
413+
prev = prev >= second_operand
414+
end
415+
416+
-- Lower than.
417+
if op == '<' then
418+
prev = prev < second_operand
419+
end
420+
421+
-- Lower or equal.
422+
if op == '&&' then
423+
prev = prev and second_operand
424+
end
425+
end
426+
return prev
427+
end
428+
429+
if node.kind == 'root_expression' then
430+
return expressions.execute(node.expr, context)
431+
end
432+
end
433+
434+
435+
--- Compile and execute given string that represents a c-style
436+
--- expression.
437+
---
438+
--- @tparam str string representation of expression.
439+
--- @tparam object object considered inside of an expression.
440+
--- @tparam variables list of variables.
441+
---
442+
--- @treturn expression value.
443+
function expressions.compile_and_execute(str, object, variables)
444+
local context = expressions.new(str, object, variables)
445+
return expressions.execute(context.ast, context)
446+
end
447+
448+
--- Create a new c-style expression object.
449+
---
450+
--- @tparam str string representation of expression.
451+
--- @tparam object object considered inside of an expression.
452+
--- @tparam variables list of variables.
453+
---
454+
--- @treturn expression object.
455+
function expressions.new(str, object, variables)
456+
return setmetatable({
457+
string = str,
458+
object = object,
459+
variables = variables,
460+
}, {
461+
__index = function(self, key)
462+
if key == 'ast' then
463+
return expressions.parse(self.string)
464+
end
465+
if key == 'execute' then
466+
return expressions.execute(self.ast, self)
467+
end
468+
end
469+
})
470+
end
471+
233472
return expressions

test/unit/expr_execute.test.lua

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env tarantool
2+
3+
local expressions = require('graphql.expressions')
4+
local tap = require('tap')
5+
local test = tap.test('expr_tree')
6+
7+
test:plan(8)
8+
9+
local str = '2 + 2'
10+
local expr = expressions.new(str)
11+
test:is(expr.execute, 4)
12+
13+
test:is(expressions.compile_and_execute(str), 4)
14+
15+
str = 'true && ($variable + 7 == field.path_1 && !(2399>23941)) && !false'
16+
expr = expressions.new(str, {node_1 = {node_2 = 21},
17+
field = {path_1 = 28}}, {variable = 21})
18+
test:is(expr.execute, true)
19+
20+
test:is(expressions.compile_and_execute(str, {node_1 = {node_2 = 21},
21+
field = {path_1 = 28}},
22+
{variable = 21}), true)
23+
24+
str = 'false || (regexp("abc", $var))&(is_null(path.ddf))|| not_null(path.ddf)'
25+
expr = expressions.new(str, {path = {ddf = 'hi_there'}}, {var = 'abc'})
26+
test:is(expr.execute, true)
27+
28+
test:is(expressions.compile_and_execute(str, {path = {ddf = 'hi_there'}},
29+
{var = 'abc'}), true)
30+
31+
expr = expressions.new(str, {path = {ddf = 'hi_there'}}, {var = 'abc'})
32+
test:is(expr.execute, false)
33+
34+
str = 'false || (regexp("abc", $var)) & (is_null(path.ddf))|| is_null(path.ddf)'
35+
test:is(expressions.compile_and_execute(str, {path = {ddf = 'hi_there'}},
36+
{var = 'abc'}), false)
37+
38+

0 commit comments

Comments
 (0)