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

AAP-10738 DROOLS-7475 Proposed feature for ['key'] accessor #539

Merged
merged 16 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
Alex-Izquierdo marked this conversation as resolved.
Show resolved Hide resolved

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)
Alex-Izquierdo marked this conversation as resolved.
Show resolved Hide resolved
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