mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
|
import ast
|
||
|
import json
|
||
|
|
||
|
def parse_acl_formula(acl_formula):
|
||
|
"""
|
||
|
Parse an ACL formula expression into a parse tree that we can interpret in JS, e.g.
|
||
|
"rec.office == 'Seattle' and user.email in ['sally@', 'xie@']".
|
||
|
|
||
|
The idea is to support enough to express ACL rules flexibly, but we don't need to support too
|
||
|
much, since rules should be reasonably simple.
|
||
|
|
||
|
The returned tree has the form [NODE_TYPE, arguments...], with these NODE_TYPEs supported:
|
||
|
And|Or ...values
|
||
|
Add|Sub|Mult|Div|Mod left, right
|
||
|
Not operand
|
||
|
Eq|NotEq|Lt|LtE|Gt|GtE left, right
|
||
|
Is|IsNot|In|NotIn left, right
|
||
|
List ...elements
|
||
|
Const value (number, string, bool)
|
||
|
Name name (string)
|
||
|
Attr node, attr_name
|
||
|
"""
|
||
|
try:
|
||
|
tree = ast.parse(acl_formula, mode='eval')
|
||
|
return _TreeConverter().visit(tree)
|
||
|
except SyntaxError as err:
|
||
|
# In case of an error, include line and offset.
|
||
|
raise SyntaxError("%s on line %s col %s" % (err.args[0], err.lineno, err.offset))
|
||
|
|
||
|
|
||
|
def parse_acl_formula_json(acl_formula):
|
||
|
"""
|
||
|
As parse_acl_formula(), but stringifies the result, and converts empty string to empty string.
|
||
|
"""
|
||
|
return json.dumps(parse_acl_formula(acl_formula)) if acl_formula else ""
|
||
|
|
||
|
|
||
|
named_constants = {
|
||
|
'True': True,
|
||
|
'False': False,
|
||
|
'None': None,
|
||
|
}
|
||
|
|
||
|
class _TreeConverter(ast.NodeVisitor):
|
||
|
# AST nodes are documented here: https://docs.python.org/2/library/ast.html#abstract-grammar
|
||
|
# pylint:disable=no-self-use
|
||
|
|
||
|
def visit_Expression(self, node):
|
||
|
return self.visit(node.body)
|
||
|
|
||
|
def visit_BoolOp(self, node):
|
||
|
return [node.op.__class__.__name__] + [self.visit(v) for v in node.values]
|
||
|
|
||
|
def visit_BinOp(self, node):
|
||
|
if not isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)):
|
||
|
return self.generic_visit(node)
|
||
|
return [node.op.__class__.__name__, self.visit(node.left), self.visit(node.right)]
|
||
|
|
||
|
def visit_UnaryOp(self, node):
|
||
|
if not isinstance(node.op, (ast.Not)):
|
||
|
return self.generic_visit(node)
|
||
|
return [node.op.__class__.__name__, self.visit(node.operand)]
|
||
|
|
||
|
def visit_Compare(self, node):
|
||
|
# We don't try to support chained comparisons like "1 < 2 < 3" (though it wouldn't be hard).
|
||
|
if len(node.ops) != 1 or len(node.comparators) != 1:
|
||
|
raise ValueError("Can't use chained comparisons")
|
||
|
return [node.ops[0].__class__.__name__, self.visit(node.left), self.visit(node.comparators[0])]
|
||
|
|
||
|
def visit_Name(self, node):
|
||
|
if node.id in named_constants:
|
||
|
return ["Const", named_constants[node.id]]
|
||
|
return ["Name", node.id]
|
||
|
|
||
|
def visit_Attribute(self, node):
|
||
|
return ["Attr", self.visit(node.value), node.attr]
|
||
|
|
||
|
def visit_Num(self, node):
|
||
|
return ["Const", node.n]
|
||
|
|
||
|
def visit_Str(self, node):
|
||
|
return ["Const", node.s]
|
||
|
|
||
|
def visit_List(self, node):
|
||
|
return ["List"] + [self.visit(e) for e in node.elts]
|
||
|
|
||
|
def visit_Tuple(self, node):
|
||
|
return self.visit_List(node) # We don't distinguish tuples and lists
|
||
|
|
||
|
def generic_visit(self, node):
|
||
|
raise ValueError("Unsupported syntax at %s:%s" % (node.lineno, node.col_offset + 1))
|