Skip to content

Commit 9ec23d8

Browse files
authored
Merge pull request #7 from santalvarez/feature/not
Version 0.5.0
2 parents 611d099 + bc61a43 commit 9ec23d8

File tree

5 files changed

+130
-97
lines changed

5 files changed

+130
-97
lines changed

docs/operators.md

+12-68
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,18 @@
22

33
## Default Operators
44

5-
### Equal
6-
7-
- **id**: "equal"
8-
9-
- **description**: Compares if the value of the object is equal to the value of the condition.
10-
11-
12-
### Not Equal
13-
14-
- **id**: "not_equal"
15-
- **description**: Compares if the value of the object is not equal to the value of the condition.
16-
17-
18-
### Greater Than
19-
20-
- **id**: "greater_than"
21-
22-
- **description**: Compares if the value of the object is greater than the value of the condition.
23-
24-
25-
### Greater Than Inclusive
26-
27-
- **id**: "greater_than_inclusive"
28-
29-
- **description**: Compares if the value of the object is greater than or equal to the value of the condition.
30-
31-
32-
### Less Than
33-
34-
- **id**: "less_than"
35-
36-
- **description**: Compares if the value of the object is less than the value of the condition.
37-
38-
39-
### Less Than Inclusive
40-
41-
- **id**: "less_than_inclusive"
42-
43-
- **description**: Compares if the value of the object is less than or equal to the value of the condition.
44-
45-
46-
### In
47-
48-
- **id**: "in"
49-
50-
- **description**: Compares if the value of the object is in the value of the condition.
51-
52-
53-
### Not In
54-
55-
- **id**: "not_in"
56-
57-
- **description**: Compares if the value of the object is not in the value of the condition.
58-
59-
60-
### Contains
61-
62-
- **id**: "contains"
63-
64-
- **description**: Compares if the value of the object contains the value of the condition.
65-
66-
67-
### Not Contains
68-
69-
- **id**: "not_contains"
70-
71-
- **description**: Compares if the value of the object does not contain the value of the condition.
72-
5+
| Operator | ID | Description |
6+
|----------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------|
7+
| Equal | "equal" | Compares if the value of the object is equal to the value of the condition. |
8+
| Not Equal | "not_equal" | Compares if the value of the object is not equal to the value of the condition. |
9+
| Greater Than | "greater_than" | Compares if the value of the object is greater than the value of the condition. |
10+
| Greater Than Inclusive| "greater_than_inclusive" | Compares if the value of the object is greater than or equal to the value of the condition. |
11+
| Less Than | "less_than" | Compares if the value of the object is less than the value of the condition. |
12+
| Less Than Inclusive | "less_than_inclusive" | Compares if the value of the object is less than or equal to the value of the condition. |
13+
| In | "in" | Compares if the value of the object is in the value of the condition. |
14+
| Not In | "not_in" | Compares if the value of the object is not in the value of the condition. |
15+
| Contains | "contains" | Compares if the value of the object contains the value of the condition. |
16+
| Not Contains | "not_contains" | Compares if the value of the object does not contain the value of the condition. |
7317

7418
## Custom Operator
7519

docs/rules.md

+20-14
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ A basic rule consists of a name and a multi condition
1313
}
1414
```
1515

16-
**name(str, req):** The name of the rule
17-
18-
**conditions(dict, req):** A [multi condition](#multi-condition). Defining a simple condition in here is not allowed, it needs to be inside a multi condition.
16+
| key | description | type | required |
17+
| --- | --- | --- | --- |
18+
| name | The name of the rule | str | yes |
19+
| description | A description of the rule. | str | no |
20+
| conditions | A [multi condition](#multi-condition). All rules start with a multi condition. | dict | yes |
21+
| extra | A dict that can be used to store extra information about the rule. | dict | no |
1922

2023
## Condition Types
2124

@@ -30,19 +33,19 @@ A simple condition consists of an operator and a value.
3033
"value": "John"
3134
}
3235
```
36+
Table describing the keys:
3337

34-
**operator(str, req):** The operator to use to compare the object with the defined value. Find info on built-in operators and how to define your own [here](operators.md).
35-
36-
**path(str):** A [JSONPath](https://goessner.net/articles/JsonPath/) expression indicating what attribute of the object to evaluate.
37-
38-
**value(any, req):** The value that will be used to compare with the object.
39-
40-
**params(dict):** A dict that can provide the operator more information about how to process the object.
38+
| key | description | type | required |
39+
| --- | --- | --- | --- |
40+
| operator | The operator to use to compare the object with the defined value. Find info on built-in operators and how to define your own [here](operators.md). | str | yes |
41+
| path | A [JSONPath](https://goessner.net/articles/JsonPath/) expression indicating what attribute of the object to evaluate. Appart from accessing attributes it also supports accessing array elements by [index]. | str | no |
42+
| value | The value that will be used to compare with the object. | any | yes |
43+
| params | A dict that can provide the operator more information about how to process the object. | dict | no |
4144

4245

4346
### Multi Condition
4447

45-
Contains either the **any** or **all** fields. These fields contain a list of conditions that can be simple, multi or a mix of both.
48+
Contains either the **any**, **all** or **not** fields. These fields contain a list of conditions that can be simple, multi or a mix of both.
4649

4750
```json
4851
{
@@ -61,9 +64,12 @@ Contains either the **any** or **all** fields. These fields contain a list of co
6164
}
6265
```
6366

64-
**all(list):** All conditions inside have to match.
65-
66-
**any(list):** One of the conditions inside have to match.
67+
Only one of these fields can be present in a multi condition.
68+
| key | description | type |
69+
| --- | --- | --- |
70+
| all | All conditions inside have to match. | list |
71+
| any | One of the conditions inside have to match. | list |
72+
| not | The result of the condition inside will be negated. | dict |
6773

6874

6975
## Results

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-rule-engine"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "A rule engine where rules are written in JSON format"
55
authors = ["Santiago Alvarez <santiago.salvarez@mercadolibre.com>"]
66
homepage = "https://github.com/santalvarez/python-rule-engine"

src/python_rule_engine/models/multi_condition.py

+31-13
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
class MultiCondition(Condition):
1010
def __init__(self, **data) -> None:
1111
super().__init__()
12-
if "all" in data and "any" in data:
13-
raise ValueError("Only one operator is supported")
14-
if "all" not in data and "any" not in data:
15-
raise ValueError("No valid operators found")
12+
13+
if sum([bool(data.get("any", [])), bool(data.get("all", [])), bool(data.get("not", {}))]) != 1:
14+
raise ValueError("Only one of any, all or not can be defined")
15+
1616
self.all = self.__validate_conditions(data.get("all", []), data["operators_dict"])
1717
self.any = self.__validate_conditions(data.get("any", []), data["operators_dict"])
18+
self.not_ = self.__validate_not_condition(data.get("not", {}), data["operators_dict"])
1819

1920
def __validate_conditions(self, data: List[dict], operators_dict) -> Optional[List[Condition]]:
2021
if not data:
@@ -28,26 +29,34 @@ def __validate_conditions(self, data: List[dict], operators_dict) -> Optional[Li
2829
cds.append(MultiCondition(**cd))
2930
return cds
3031

32+
def __validate_not_condition(self, data: dict, operators_dict) -> Optional[Condition]:
33+
if not data:
34+
return None
35+
data["operators_dict"] = operators_dict
36+
try:
37+
return SimpleCondition(**data)
38+
except ValueError:
39+
return MultiCondition(**data)
40+
3141
def evaluate(self, obj):
3242
""" Run a multi condition on an object or dict
3343
34-
:param MultiCondition rule: The rule
3544
:param Any obj: The object
36-
:return Rule: The original rule with aded result info
3745
"""
3846

3947
if self.any:
4048
return self.evaluate_any(obj)
4149

42-
return self.evaluate_all(obj)
50+
if self.all:
51+
return self.evaluate_all(obj)
52+
53+
return self.evaluate_not(obj)
4354

4455
def evaluate_any(self, obj):
4556
"""
46-
Run a multi condition on an object or dict with 'any' type.
57+
Run a multi condition on a dict with 'any' type.
4758
48-
:param multi_condition: The multi condition to be run.
4959
:param obj: The object to be tested.
50-
:return: The original multi condition with added result info.
5160
"""
5261

5362
for _, cond in enumerate(self.any):
@@ -59,11 +68,9 @@ def evaluate_any(self, obj):
5968

6069
def evaluate_all(self, obj):
6170
"""
62-
Run a multi condition on an object or dict with 'all' type.
71+
Run a multi condition on a dict with 'all' type.
6372
64-
:param multi_condition: The multi condition to be run.
6573
:param obj: The object to be tested.
66-
:return: The original multi condition with added result info.
6774
"""
6875

6976
for _, cond in enumerate(self.all):
@@ -74,3 +81,14 @@ def evaluate_all(self, obj):
7481
return
7582

7683
self.match = True
84+
85+
def evaluate_not(self, obj):
86+
"""
87+
Run a multi condition on a dict with 'not' type.
88+
89+
:param obj: The object to be tested.
90+
"""
91+
92+
self.not_.evaluate(obj)
93+
94+
self.match = not self.not_.match

tests/test_rule_engine.py

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from python_rule_engine import RuleEngine
22

33

4-
def test_basic_rule_on_dict():
4+
def test_rule_with_all_condition():
55
obj = {
66
"person": {
77
"name": "Santiago",
@@ -37,3 +37,68 @@ def test_basic_rule_on_dict():
3737

3838
assert results[0].conditions.match is True
3939

40+
def test_rule_with_any_condition():
41+
obj = {
42+
"person": {
43+
"name": "Santiago",
44+
"last_name": "Alvarez"
45+
}
46+
}
47+
48+
rule = {
49+
"name": "basic_rule",
50+
"conditions": {
51+
"any": [
52+
{
53+
"path": "$.person.name",
54+
"value": "Martin",
55+
"operator": "equal"
56+
},
57+
{
58+
"path": "$.person.last_name",
59+
"value": "Alvarez",
60+
"operator": "equal"
61+
}
62+
]
63+
}
64+
}
65+
66+
engine = RuleEngine([rule])
67+
68+
results = engine.evaluate(obj)
69+
70+
assert results[0].conditions.match is True
71+
72+
def test_rule_with_not_condition():
73+
obj = {
74+
"person": {
75+
"name": "Santiago",
76+
"last_name": "Alvarez"
77+
}
78+
}
79+
80+
rule = {
81+
"name": "basic_rule",
82+
"conditions": {
83+
"not": { # None of the conditions inside "all" should be true
84+
"all": [
85+
{
86+
"path": "$.person.name",
87+
"value": "Martin",
88+
"operator": "equal"
89+
},
90+
{
91+
"path": "$.person.last_name",
92+
"value": "Palermo",
93+
"operator": "equal"
94+
}
95+
]
96+
}
97+
}
98+
}
99+
100+
engine = RuleEngine([rule])
101+
102+
results = engine.evaluate(obj)
103+
104+
assert results[0].conditions.match is True

0 commit comments

Comments
 (0)