From bc4f8b8062effbbd0caa41cc046fbda18ee30804 Mon Sep 17 00:00:00 2001 From: Matteo Mortari Date: Thu, 3 Aug 2023 22:10:39 +0200 Subject: [PATCH] AAP-10738 DROOLS-7475 Proposed feature for ['key'] accessor (#539) 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**: * https://github.com/ansible/ansible-rulebook/pull/539 * https://github.com/ansible/drools_jpy/pull/50 * https://github.com/kiegroup/drools-ansible-rulebook-integration/pull/56 * https://github.com/kiegroup/drools/pull/5333 * https://issues.redhat.com/browse/AAP-10738 * https://github.com/kiegroup/drools-ansible-rulebook-integration/pull/65 * https://github.com/ansible/drools_jpy/pull/51 --------- Co-authored-by: Madhu Kanoor Co-authored-by: Alex --- CHANGELOG.md | 1 + ansible_rulebook/condition_parser.py | 17 +++++- docs/conditions.rst | 54 +++++++++++++++++++ setup.cfg | 2 +- .../operators/test_logical_operators.yml | 15 ++++++ .../operators/test_selectattr_operator.yml | 15 ++++++ tests/e2e/test_operators.py | 15 ++++++ tests/test_ast.py | 53 ++++++++++++++++++ 8 files changed, 170 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004496e6..b7584b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## [Unreleased] ### Added +- rulebook and Drools bracket notation syntax ### Fixed diff --git a/ansible_rulebook/condition_parser.py b/ansible_rulebook/condition_parser.py index 87c637c5..b2f25902 100644 --- a/ansible_rulebook/condition_parser.py +++ b/ansible_rulebook/condition_parser.py @@ -31,6 +31,7 @@ delimitedList, infix_notation, one_of, + originalTextFor, pyparsing_common, ) @@ -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])) ) diff --git a/docs/conditions.rst b/docs/conditions.rst index ba16c838..1f128dda 100644 --- a/docs/conditions.rst +++ b/docs/conditions.rst @@ -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 `_. + +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 `_. + Supported Operators ******************* @@ -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 `_. + Checking if an object exists in a list based on a test ------------------------------------------------------ @@ -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 `_. + FAQ *** diff --git a/setup.cfg b/setup.cfg index 0b64178e..f2fbfda5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = janus ansible-runner websockets - drools_jpy == 0.3.4 + drools_jpy == 0.3.6 [options.packages.find] include = diff --git a/tests/e2e/files/rulebooks/operators/test_logical_operators.yml b/tests/e2e/files/rulebooks/operators/test_logical_operators.yml index d076f7be..7507e7b8 100644 --- a/tests/e2e/files/rulebooks/operators/test_logical_operators.yml +++ b/tests/e2e/files/rulebooks/operators/test_logical_operators.yml @@ -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 @@ -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" + diff --git a/tests/e2e/files/rulebooks/operators/test_selectattr_operator.yml b/tests/e2e/files/rulebooks/operators/test_selectattr_operator.yml index 2178595b..11f2ed7f 100644 --- a/tests/e2e/files/rulebooks/operators/test_selectattr_operator.yml +++ b/tests/e2e/files/rulebooks/operators/test_selectattr_operator.yml @@ -112,6 +112,15 @@ - 15 - 8121.99 - 1111 + - id: "Testcase #12" + asd: + x: + - 0 + - + - 0 + - 0 + - a: + b: 3.1415 rules: @@ -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" diff --git a/tests/e2e/test_operators.py b/tests/e2e/test_operators.py index c26d4a39..8d0bcb15 100644 --- a/tests/e2e/test_operators.py +++ b/tests/e2e/test_operators.py @@ -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(): @@ -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" diff --git a/tests/test_ast.py b/tests/test_ast.py index d6d910a6..3a549f15 100644 --- a/tests/test_ast.py +++ b/tests/test_ast.py @@ -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",