Skip to content

Instantly share code, notes, and snippets.

@bodik
Created May 9, 2019 10:09
Show Gist options
  • Save bodik/bc764ad2b73317b3936019b1e40e9908 to your computer and use it in GitHub Desktop.
Save bodik/bc764ad2b73317b3936019b1e40e9908 to your computer and use it in GitHub Desktop.
parse simple search expression to sqlalchemy-filters
#!/usr/bin/env python3
import logging
from pprint import pprint
from lark import Lark, Transformer
"""
Boolean expression definition widely recognizes basic building blocks as
'terms' and 'factors', do not confuse them with parser's terminals. Also note
that EBNF expresses logic operators precedence.
https://unnikked.ga/how-to-build-a-boolean-expression-evaluator-518e9e068a65
https://cs.stackexchange.com/questions/10558/grammar-for-describing-boolean-expressions-with-and-or-and-not/10622#10622
"""
search_grammar = r"""
?start: expression
expression: term ("OR" term)*
term: _factor ("AND" _factor)*
_factor: criteria | "(" expression ")"
criteria: FIELD OP VALUE
FIELD: /[a-z]+/
OP: "==" | "!="
VALUE: ESCAPED_STRING
%import common.ESCAPED_STRING
%import common.WS
%ignore WS
"""
class TreeToSAFilter(Transformer):
def expression(self, args):
if len(args) > 1:
return {"or": args}
else:
return args[0]
def term(self, args):
if len(args) > 1:
return {"and": args}
else:
return args[0]
def criteria(self, args):
return {'field': args[0].value, 'op': args[1].value, 'value': args[2].value}
search_parser = Lark(search_grammar, parser='lalr', lexer='standard', transformer=TreeToSAFilter())
def test(testcase, expected):
output = search_parser.parse(testcase)
print('testcase: %s outputs %s' % (testcase, output))
assert output == expected
if __name__ == '__main__':
test('a=="a"', {'field': 'a', 'op': '==', 'value': '"a"'})
test('a=="a" AND b=="b"', {'and': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'field': 'b', 'op': '==', 'value': '"b"'}]})
test('a=="a" AND b=="b" AND c=="c"', {'and': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'field': 'b', 'op': '==', 'value': '"b"'}, {'field': 'c', 'op': '==', 'value': '"c"'}]})
test('a=="a" OR b=="b"', {'or': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'field': 'b', 'op': '==', 'value': '"b"'}]})
test('a=="a" OR b=="b" OR c=="c"', {'or': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'field': 'b', 'op': '==', 'value': '"b"'}, {'field': 'c', 'op': '==', 'value': '"c"'}]})
test('a=="a" OR b=="b" AND c!="c"', {'or': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'and': [{'field': 'b', 'op': '==', 'value': '"b"'}, {'field': 'c', 'op': '!=', 'value': '"c"'}]}]})
test('a=="a" OR b=="b" OR c!="c" AND d=="d"', {'or': [{'field': 'a', 'op': '==', 'value': '"a"'}, {'field': 'b', 'op': '==', 'value': '"b"'}, {'and': [{'field': 'c', 'op': '!=', 'value': '"c"'}, {'field': 'd', 'op': '==', 'value': '"d"'}]}]})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment