Skip to content

Commit

Permalink
AAP-10738 DROOLS-7475 Proposed feature for ['key'] accessor (#539)
Browse files Browse the repository at this point in the history
work-in-progress as i'm volunteering to work on this e2e, so this is a
quite-complete draft for the ansible-rulebook side; i'll keep posted as
i will progress on drools-ansible-rulebook-integration side.

EDIT: demonstrate working locally and added e2e test as requested

**JIRA**: https://issues.redhat.com/browse/DROOLS-7475

**referenced Pull Requests**: 

* #539
* ansible/drools_jpy#50
*
kiegroup/drools-ansible-rulebook-integration#56
* https://github.com/kiegroup/drools/pull/5333
* https://issues.redhat.com/browse/AAP-10738
*
kiegroup/drools-ansible-rulebook-integration#65
* ansible/drools_jpy#51

---------

Co-authored-by: Madhu Kanoor <[email protected]>
Co-authored-by: Alex <[email protected]>
  • Loading branch information
3 people authored Aug 3, 2023
1 parent ecb20c2 commit bc4f8b8
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
## [Unreleased]

### Added
- rulebook and Drools bracket notation syntax

### Fixed

Expand Down
17 changes: 16 additions & 1 deletion ansible_rulebook/condition_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
delimitedList,
infix_notation,
one_of,
originalTextFor,
pyparsing_common,
)

Expand Down Expand Up @@ -101,7 +102,21 @@
| Keyword("fact")
)
varname = (
Combine(valid_prefix + ZeroOrMore("." + ident))
Combine(
valid_prefix
+ ZeroOrMore(
("." + ident)
| (
("[")
+ (
originalTextFor(QuotedString('"'))
| originalTextFor(QuotedString("'"))
| pyparsing_common.signed_integer
)
+ ("]")
)
)
)
.copy()
.add_parse_action(lambda toks: Identifier(toks[0]))
)
Expand Down
54 changes: 54 additions & 0 deletions docs/conditions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,56 @@ The data type is of great importance for the rules engine. The following types a
* floats (dot notation and scientific notation)
* null

Navigate structured data
************************

You can navigate strutured event, fact, var data objects using either dot notation or bracket notation:

.. code-block:: yaml
rules:
- name: Using dot notation
condition: event.something.nested == true
action:
debug:
- name: Analogous, but using bracket notation
condition: event.something["nested"] == true
action:
debug:
Both of the above examples checks for the same value (attribute "nested" inside of "something") to be equal to `true`.

Bracket notation might be preferable to dot notation when the structured data contains a key using symbols
or other special characters:

.. code-block:: yaml
name: Looking for specific metadata
condition: event.resource.metadata.labels["app.kubernetes.io/name"] == "hello-pvdf"
action:
debug:
You can find more information about dot notation and bracket notation also in the Ansible playbook `manual <https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#referencing-key-value-dictionary-variables>`_.

You can access list in strutured event, fact, var data objects using bracket notation too.
The first item in a list is item 0, the second item is item 1.
Like Python, you can access the `n`-to-last item in the list by supplying a negative index.
For example:

.. code-block:: yaml
rules:
- name: Looking for the first item in the list
condition: event.letters[0] == "a"
action:
debug:
- name: Looking for the last item in the list
condition: event.letters[-1] == "z"
action:
debug:
You can find more information the bracket notation for list also in the Ansible playbook `manual <https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#referencing-list-variables>`_.

Supported Operators
*******************

Expand Down Expand Up @@ -796,6 +846,8 @@ Check if an item does not exist in a list based on a test
| The value is based on the operator used, if the operator is regex then the value is a pattern.
| If the operator is one of >,>=,<,<= then the value is either an integer or a float
You can find more information for the *select* condition also in the Ansible playbook `manual <https://docs.ansible.com/ansible/latest/playbook_guide/complex_data_manipulation.html#loops-and-list-comprehensions>`_.

Checking if an object exists in a list based on a test
------------------------------------------------------

Expand Down Expand Up @@ -834,6 +886,8 @@ Checking if an object does not exist in a list based on a test
| If the operator is one of >, >=, <, <= then the value is either an integer or a float.
| If the operator is in or not in then the value is list of integer, float or string.
You can find more information for the *selectattr* condition also in the Ansible playbook `manual <https://docs.ansible.com/ansible/latest/playbook_guide/complex_data_manipulation.html#loops-and-list-comprehensions>`_.


FAQ
***
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ install_requires =
janus
ansible-runner
websockets
drools_jpy == 0.3.4
drools_jpy == 0.3.6

[options.packages.find]
include =
Expand Down
15 changes: 15 additions & 0 deletions tests/e2e/files/rulebooks/operators/test_logical_operators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@
- id: "Testcase #11"
left_hand: 5
right_hand: 5
- id: "Testcase #12"
asd:
x:
- 0
-
- 0
- 0
- a:
b: 3.1415



Expand Down Expand Up @@ -223,3 +232,9 @@
debug:
msg: "Testcase #11 passes"

- name: "Testcase #12"
condition: event.asd["x"][1][2].a["b"] == 3.1415
action:
debug:
msg: "Testcase #12 passes"

15 changes: 15 additions & 0 deletions tests/e2e/files/rulebooks/operators/test_selectattr_operator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@
- 15
- 8121.99
- 1111
- id: "Testcase #12"
asd:
x:
- 0
-
- 0
- 0
- a:
b: 3.1415


rules:
Expand Down Expand Up @@ -213,3 +222,9 @@
action:
debug:
msg: "Output for testcase #11"

- name: selectattr and squared accessor interaction
condition: event.asd["x"][1][2] is selectattr("a.b", "==", 3.1415)
action:
debug:
msg: "Output for testcase #12"
15 changes: 15 additions & 0 deletions tests/e2e/test_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ def test_logical_operators(update_environment):
with check:
assert "Testcase #11 passes" in result.stdout, "Testcase #11 failed"

with check:
assert "Testcase #12 passes" in result.stdout, "Testcase #12 failed"


@pytest.mark.e2e
def test_string_match():
Expand Down Expand Up @@ -785,3 +788,15 @@ def test_selectattr_operator():
)
== 1
), "testcase #11 failed"

with check:
assert (
len(
[
line
for line in result.stdout.splitlines()
if "Output for testcase #12" in line
]
)
== 1
), "testcase #12 failed"
53 changes: 53 additions & 0 deletions tests/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,59 @@ def test_parse_condition():
}
} == visit_condition(parse_condition("fact.range.i > 1"), {})

assert {
"EqualsExpression": {
"lhs": {"Fact": "range['pi']"},
"rhs": {"Float": 3.1415},
}
} == visit_condition(parse_condition("fact.range['pi'] == 3.1415"), {})
assert {
"EqualsExpression": {
"lhs": {"Fact": 'range["pi"]'},
"rhs": {"Float": 3.1415},
}
} == visit_condition(parse_condition('fact.range["pi"] == 3.1415'), {})
# `Should start with event., events.,fact., facts. or vars.` semantic check
with pytest.raises(InvalidIdentifierException):
visit_condition(
parse_condition('fact["range"].pi == 3.1415'),
{},
)
assert {
"EqualsExpression": {
"lhs": {"Fact": 'range["pi"].value'},
"rhs": {"Float": 3.1415},
}
} == visit_condition(
parse_condition('fact.range["pi"].value == 3.1415'), {}
)
assert {
"EqualsExpression": {
"lhs": {"Fact": "range[0]"},
"rhs": {"Float": 3.1415},
}
} == visit_condition(parse_condition("fact.range[0] == 3.1415"), {})
assert {
"EqualsExpression": {
"lhs": {"Fact": "range[-1]"},
"rhs": {"Float": 3.1415},
}
} == visit_condition(parse_condition("fact.range[-1] == 3.1415"), {})
# invalid index must be signed int, not a floating point
with pytest.raises(ConditionParsingException):
visit_condition(
parse_condition("fact.range[-1.23] == 3.1415"),
{},
)
assert {
"EqualsExpression": {
"lhs": {"Fact": 'range["x"][1][2].a["b"]'},
"rhs": {"Float": 3.1415},
}
} == visit_condition(
parse_condition('fact.range["x"][1][2].a["b"] == 3.1415'), {}
)

assert {
"NegateExpression": {
"Event": "enabled",
Expand Down

0 comments on commit bc4f8b8

Please sign in to comment.