Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional edit operator syntax (SYN-7486) #4046

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions changes/e1b7c7693e7454c32ca657a2bed734d5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
desc: Added syntax for conditional node property edit operators in Storm.
prs: []
type: feat
...
89 changes: 88 additions & 1 deletion synapse/lib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@

from synapse.lib.stormtypes import tobool, toint, toprim, tostr, tonumber, tocmprvalu, undef

SET_ALWAYS = 0
SET_UNSET = 1
SET_NEVER = 2

logger = logging.getLogger(__name__)

def parseNumber(x):
Expand Down Expand Up @@ -4148,6 +4152,89 @@
async for item in agen:
yield item

class CondSetOper(Oper):
def __init__(self, astinfo, kids, errok=False):
Value.__init__(self, astinfo, kids=kids)
self.errok = errok

def prepare(self):
self.isconst = False
if isinstance(self.kids[0], Const):
self.isconst = True
match self.kids[0].value():
case "unset":
self.valu = SET_UNSET

async def compute(self, runt, path):
if self.isconst:
return self.valu

valu = await self.kids[0].compute(runt, path)
match valu:
Cisphyx marked this conversation as resolved.
Show resolved Hide resolved
case "always":
return SET_ALWAYS
case "unset":
return SET_UNSET
case "never":
return SET_NEVER
case _:
mesg = f'Invalid conditional set operator ({valu}).'
exc = s_exc.StormRuntimeError(mesg=mesg)
raise self.addExcInfo(exc)

class EditCondPropSet(Edit):

async def run(self, runt, genr):

if runt.readonly:
mesg = 'Storm runtime is in readonly mode, cannot create or edit nodes and other graph data.'
Cisphyx marked this conversation as resolved.
Show resolved Hide resolved
raise self.addExcInfo(s_exc.IsReadOnly(mesg=mesg))

excignore = (s_exc.BadTypeValu,) if self.kids[1].errok else ()
rval = self.kids[2]

async for node, path in genr:

propname = await self.kids[0].compute(runt, path)
name = await tostr(propname)

prop = node.form.props.get(name)
Cisphyx marked this conversation as resolved.
Show resolved Hide resolved
if prop is None:
if (exc := await s_stormtypes.typeerr(propname, str)) is None:
mesg = f'No property named {name} on form {node.form.name}.'
exc = s_exc.NoSuchProp(mesg=mesg, name=name, form=node.form.name)

raise self.kids[0].addExcInfo(exc)

oper = await self.kids[1].compute(runt, path)
if oper == SET_NEVER or (oper == SET_UNSET and (oldv := node.get(name)) is not None):
yield node, path
await asyncio.sleep(0)
continue

if not node.form.isrunt:
# runt node property permissions are enforced by the callback
runt.confirmPropSet(prop)

isndef = isinstance(prop.type, s_types.Ndef)

try:
valu = await rval.compute(runt, path)
valu = await s_stormtypes.tostor(valu, isndef=isndef)

if isinstance(prop.type, s_types.Ival) and oldv is not None:
valu, _ = prop.type.norm(valu)
valu = prop.type.merge(oldv, valu)

Check warning on line 4227 in synapse/lib/ast.py

View check run for this annotation

Codecov / codecov/patch

synapse/lib/ast.py#L4226-L4227

Added lines #L4226 - L4227 were not covered by tests

await node.set(name, valu)

except excignore:
pass

yield node, path

await asyncio.sleep(0)

class EditPropSet(Edit):

async def run(self, runt, genr):
Expand Down Expand Up @@ -4199,7 +4286,7 @@

if not isarray:
mesg = f'Property set using ({oper}) is only valid on arrays.'
exc = s_exc.StormRuntimeError(mesg)
exc = s_exc.StormRuntimeError(mesg=mesg)
raise self.kids[0].addExcInfo(exc)

arry = node.get(name)
Expand Down
4 changes: 4 additions & 0 deletions synapse/lib/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
'TRYSETPLUS': '?+=',
'TRYSETMINUS': '?-=',
'UNIVNAME': 'universal property',
'UNSET': 'unset',
'EXPRUNIVNAME': 'universal property',
'VARTOKN': 'variable',
'EXPRVARTOKN': 'variable',
Expand Down Expand Up @@ -642,6 +643,8 @@ def massage_vartokn(astinfo, x):
'andexpr': s_ast.AndCond,
'baresubquery': s_ast.SubQuery,
'catchblock': s_ast.CatchBlock,
'condsetoper': s_ast.CondSetOper,
'condtrysetoper': lambda astinfo, kids: s_ast.CondSetOper(astinfo, kids, errok=True),
'condsubq': s_ast.SubqCond,
'dollarexpr': s_ast.DollarExpr,
'edgeaddn1': s_ast.EditEdgeAdd,
Expand All @@ -657,6 +660,7 @@ def massage_vartokn(astinfo, x):
'formname': s_ast.FormName,
'editpropdel': lambda astinfo, kids: s_ast.EditPropDel(astinfo, kids[1:]),
'editpropset': s_ast.EditPropSet,
'editcondpropset': s_ast.EditCondPropSet,
'edittagadd': s_ast.EditTagAdd,
'edittagdel': lambda astinfo, kids: s_ast.EditTagDel(astinfo, kids[1:]),
'edittagpropset': s_ast.EditTagPropSet,
Expand Down
17 changes: 16 additions & 1 deletion synapse/lib/storm.lark
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ _editblock: "[" _editoper* "]"

// A single edit operation
_editoper: editnodeadd
| editpropset | editunivset | edittagpropset | edittagadd
| editpropset | editunivset | edittagpropset | edittagadd | editcondpropset
| editpropdel | editunivdel | edittagpropdel | edittagdel
| editparens | edgeaddn1 | edgedeln1 | edgeaddn2 | edgedeln2

Expand All @@ -49,18 +49,33 @@ edittagadd: "+" [SETTAGOPER] tagname [(EQSPACE | EQNOSPACE) _valu]
editunivdel: EXPRMINUS univprop
edittagdel: EXPRMINUS tagname
editpropset: relprop (EQSPACE | EQNOSPACE | MODSET | TRYSET | TRYSETPLUS | TRYSETMINUS) _valu
editcondpropset: relprop condsetoper _valu
editpropdel: EXPRMINUS relprop
editunivset: univprop (EQSPACE | EQNOSPACE | MODSET | TRYSET | TRYSETPLUS | TRYSETMINUS) _valu
editnodeadd: formname (EQSPACE | EQNOSPACE | MODSET | TRYSET | TRYSETPLUS | TRYSETMINUS) _valu
edittagpropset: "+" tagprop (EQSPACE | EQNOSPACE | MODSET | TRYSET | TRYSETPLUS | TRYSETMINUS) _valu
edittagpropdel: EXPRMINUS tagprop

EQSPACE: /((?<=\s)=|=(?=\s))/
MODSET.4: "+=" | "-="
TRYSETPLUS.1: "?+="
TRYSETMINUS.1: "?-="
TRYSET.1: "?="
SETTAGOPER: "?"

condsetoper: ("*" UNSET | _DEREF "$" _condvarvaluatom) "="
| ("*" UNSET | _DEREF "$" _condvarvaluatom) "?=" -> condtrysetoper
UNSET: "unset"
invisig0th marked this conversation as resolved.
Show resolved Hide resolved
_condvarvaluatom: condvarvalue | condvarderef | condfunccall
condvarvalue: VARTOKN -> varvalue

!condvarderef: _condvarvaluatom "." (VARTOKN | "$" VARTOKN | _condderefexpr) -> varderef
_condderefexpr: "$"? conddollarexpr
conddollarexpr: "(" expror ")" -> dollarexpr

condfunccall: _condvarvaluatom _condcallargs -> funccall
_condcallargs: _LPARNOSPACE [(_valu | VARTOKN | (VARTOKN | NONQUOTEWORD) (EQSPACE | EQNOSPACE) _valu) ("," (_valu | VARTOKN | (VARTOKN | NONQUOTEWORD) (EQSPACE | EQNOSPACE) _valu))*] ","? ")"

// The set of non-edit non-commands in storm

_oper: stormfunc | initblock | emptyblock | finiblock | trycatch | subquery | _formpivot | formjoin
Expand Down
1 change: 1 addition & 0 deletions synapse/lib/storm_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
'TRYSETMINUS': p_t.Operator,
'TRYSETPLUS': p_t.Operator,
'UNIVNAME': p_t.Name,
'UNSET': p_t.Operator,
'EXPRUNIVNAME': p_t.Name,
'VARTOKN': p_t.Name.Variable,
'EXPRVARTOKN': p_t.Name.Variable,
Expand Down
87 changes: 87 additions & 0 deletions synapse/tests/test_lib_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,93 @@ async def test_ast_variable_props(self):
q = 'test:str=foo $newp=($node.repr(), bar) [*$newp=foo]'
await self.asyncraises(s_exc.StormRuntimeError, core.nodes(q))

async def test_ast_condsetoper(self):
async with self.getTestCore() as core:

q = '$var=hehe $foo=unset [test:str=foo :$var*unset=heval]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('heval', nodes[0].get('hehe'))

q = '$var=hehe $foo=unset [test:str=foo :$var*unset=newp]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('heval', nodes[0].get('hehe'))

q = '$var=hehe $foo=unset [test:str=foo :$var*$foo=newp]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('heval', nodes[0].get('hehe'))

q = '$var=hehe $foo=always [test:str=foo :$var*$foo=yep]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('yep', nodes[0].get('hehe'))

q = '[test:str=foo -:hehe]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.none(nodes[0].get('hehe'))

q = '$var=hehe $foo=never [test:str=foo :$var*$foo=yep]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.none(nodes[0].get('hehe'))

q = '$var=hehe $foo=unset [test:str=foo :$var*$foo=heval]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('heval', nodes[0].get('hehe'))

with self.raises(s_exc.BadTypeValu):
q = '$var=tick $foo=always [test:str=foo :$var*$foo=heval]'
nodes = await core.nodes(q)

q = '$var=tick $foo=always [test:str=foo :$var*$foo?=heval]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.none(nodes[0].get('tick'))

q = '''
$opts=({"tick": "unset", "hehe": "always"})
[ test:str=foo
:hehe*$opts.hehe=newv
:tick*$opts.tick?=2020]
'''
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('newv', nodes[0].get('hehe'))
tick = nodes[0].get('tick')
self.nn(tick)

q = '''
$opts=({"tick": "never", "hehe": "unset"})
[ test:str=foo
:hehe*$opts.hehe=newp
:tick*$opts.tick?=2020]
'''
nodes = await core.nodes(q)
self.len(1, nodes)
self.eq('newv', nodes[0].get('hehe'))
self.eq(tick, nodes[0].get('tick'))

q = '$foo=always [test:str=foo :tick*$foo?=2021]'
nodes = await core.nodes(q)
self.len(1, nodes)
self.ne(tick, nodes[0].get('tick'))

with self.raises(s_exc.IsReadOnly):
q = '[test:str=foo :hehe*unset=heval]'
nodes = await core.nodes(q, opts={'readonly': True})

with self.raises(s_exc.NoSuchProp):
q = '[test:str=foo :newp*unset=heval]'
nodes = await core.nodes(q)

with self.raises(s_exc.StormRuntimeError):
q = '$foo=newp [test:str=foo :hehe*$foo=heval]'
nodes = await core.nodes(q)

async def test_ast_editparens(self):

async with self.getTestCore() as core:
Expand Down
14 changes: 14 additions & 0 deletions synapse/tests/test_lib_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,13 @@
'$pvar=stuff test:arrayprop +:$pvar*[=neato]',
'$pvar=ints test:arrayprop +:$pvar*[=$othervar]',
'$foo = ({"foo": ${ inet:fqdn }})',
'[test:str=foo :hehe*unset=heval]',
'[test:str=foo :hehe*$foo=heval]',
'[test:str=foo :$foo*unset=heval]',
'[test:str=foo :$foo*$bar=heval]',
'[test:str=foo :$foo*$bar.baz=heval]',
'[test:str=foo :$foo*$bar.("baz")=heval]',
'[test:str=foo :$foo*$bar.baz()=heval]',
]

# Generated with print_parse_list below
Expand Down Expand Up @@ -1358,6 +1365,13 @@
'Query: [SetVarOper: [Const: pvar, Const: stuff], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, Const: neato]]]',
'Query: [SetVarOper: [Const: pvar, Const: ints], LiftProp: [Const: test:arrayprop], FiltOper: [Const: +, ArrayCond: [RelProp: [VarValue: [Const: pvar]], Const: =, VarValue: [Const: othervar]]]]',
'Query: [SetVarOper: [Const: foo, DollarExpr: [ExprDict: [Const: foo, EmbedQuery: inet:fqdn]]]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [Const: hehe], CondSetOper: [Const: unset], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [Const: hehe], CondSetOper: [VarValue: [Const: foo]], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [VarValue: [Const: foo]], CondSetOper: [Const: unset], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [VarValue: [Const: foo]], CondSetOper: [VarValue: [Const: bar]], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [VarValue: [Const: foo]], CondSetOper: [VarDeref: [VarValue: [Const: bar], Const: baz]], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [VarValue: [Const: foo]], CondSetOper: [VarDeref: [VarValue: [Const: bar], DollarExpr: [Const: baz]]], Const: heval]]',
'Query: [EditNodeAdd: [FormName: [Const: test:str], Const: =, Const: foo], EditCondPropSet: [RelProp: [VarValue: [Const: foo]], CondSetOper: [FuncCall: [VarDeref: [VarValue: [Const: bar], Const: baz], CallArgs: [], CallKwargs: []]], Const: heval]]',
]

class GrammarTest(s_t_utils.SynTest):
Expand Down
Loading