Skip to content

Commit f60caf6

Browse files
authored
Add docs for variables resolving (#24)
1 parent 208f136 commit f60caf6

File tree

4 files changed

+135
-7
lines changed

4 files changed

+135
-7
lines changed

README.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ Usage
6262
# 2. Parse the JSON Logic expression:
6363
from jsonlogic import JSONLogicExpression
6464
65-
expr = JSONLogicExpression.from_json({">": [{"var": "my_int"}, 2]})
65+
expr = JSONLogicExpression.from_json({"map": [
66+
[1, 2],
67+
{"*": [{"var": ""}, {"var": "/my_int@1"}]},
68+
]})
6669
6770
# 3. Create an operator tree:
6871
root_op = expr.as_operator_tree(operator_registry)
@@ -80,14 +83,14 @@ Usage
8083
}
8184
)
8285
print(typ)
83-
#> BooleanType()
86+
#> ArrayType(IntegerType())
8487
8588
# 5. Evaluate with data:
8689
from jsonlogic.evaluation import evaluate
8790
value = evaluate(
8891
root_op,
89-
data={"my_int": 3},
92+
data={"my_int": 2},
9093
data_schema=None,
9194
)
9295
print(value)
93-
#> True
96+
#> [2, 4]

docs/source/usage/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The next sections will go over typechecking and evaluating the expression.
9191
.. toctree::
9292
:maxdepth: 2
9393

94+
resolving_variables
9495
typechecking
9596
evaluation
9697
creating_operators
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
Resolving variables
2+
===================
3+
4+
In the original `JsonLogic`_ format, operators `accessing data <https://jsonlogic.com/operations.html#accessing-data>`_
5+
use a dot-like notation. With the following expression:
6+
7+
.. code-block:: json
8+
9+
{"var": "some.var"}
10+
11+
and the following data:
12+
13+
.. code-block:: json
14+
15+
{
16+
"some": {
17+
"var": 1
18+
}
19+
}
20+
21+
the expression will evaluate to :json:`1`. However, this dot-like notation can be ambiguous:
22+
23+
- As the empty string :json:`""` is special cased to refer to the entire data object, it is impossible
24+
to refer to :json:`1` with the following data:
25+
26+
.. code-block:: json
27+
28+
{
29+
"": 1
30+
}
31+
32+
as the reference :json:`""` would evaluate to :json:`{"": 1}`, and :json:`"."` could be parsed as a reference to :json:`{"": {"": 1}}`.
33+
34+
- There is no way to escape dots if present in data keys. The reference :json:`"some.var"` could refer to both
35+
:json:`1` and :json:`2` with the following data:
36+
37+
.. code-block:: json
38+
39+
{
40+
"some": {
41+
"var": 1
42+
},
43+
"some.var": 2
44+
}
45+
46+
For this reason, an alternative format is proposed, based on the JSON Pointer standard (:rfc:`6901`). The following expressions:
47+
48+
with the following data:
49+
50+
.. code-block:: json
51+
52+
{
53+
"path": 1,
54+
"": 2,
55+
"": {"": 3},
56+
"path.to": 4,
57+
"path/": 5
58+
}
59+
60+
this is how the references will evaluate:
61+
62+
.. code-block:: bash
63+
64+
{"var": "/path"} -> 1
65+
{"var": ""} -> 2
66+
{"var": "/"} -> whole object
67+
{"var": "//"} -> 3
68+
{"var": "/path.to"} -> 4
69+
{"var": "/path~1"} -> 5
70+
71+
Variables scopes
72+
----------------
73+
74+
The original `JsonLogic`_ format implicitly uses the notion scope in the implementation
75+
of some operators such as `map <https://jsonlogic.com/operations.html#map-reduce-and-filter>`_:
76+
77+
.. code-block:: json
78+
79+
{
80+
"map": [
81+
[1, 2],
82+
{"*": [{"var": ""}, 2]}
83+
]
84+
}
85+
86+
In this case, the variable reference :json:`""` will refer to each element of the array :json:`[1, 2]`.
87+
This means that there is no way to access data from the top level object (say for example you wanted
88+
to multiply every element of the array with :json:`{"var": "some_const"}` instead of :json:`2`).
89+
90+
The notion of *scope* is thus introduced, so that it is still possible to access data from the parent scope.
91+
This scope can be specified by appending ``@n`` at the end of the variable reference. The current scope starts at
92+
0, so using :json:`"some_var@0"` is equivalent to :json:`"some_var"`.
93+
94+
Using our previous ``map`` example, with the following data:
95+
96+
.. code-block:: json
97+
98+
{
99+
"some_const": 2
100+
}
101+
102+
the operator can be written as:
103+
104+
.. code-block:: json
105+
106+
{
107+
"map": [
108+
[1, 2],
109+
{"*": [{"var": ""}, {"var": "some_const@1"}]}
110+
]
111+
}
112+
113+
and would evaluate to :json:`[2, 4]`.
114+
115+
For more details on resolving variables, you can refer to the API documentation: :doc:`../api/evaluation`.
116+
117+
.. _`JsonLogic`: https://jsonlogic.com/

tests/test_resolving.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,22 @@ def test_reference_parser(pointer_reference: str, dot_reference: str, expected:
5757
assert (parsed_ref.segments, scope) == expected
5858

5959

60-
def test_pointer_reference_empty_string() -> None:
60+
@pytest.mark.parametrize(
61+
["pointer_reference", "expected"],
62+
[
63+
("/", ([""], 0)),
64+
("//", (["", ""], 0)),
65+
],
66+
)
67+
def test_pointer_reference(pointer_reference: str, expected: tuple[list[str], int]) -> None:
6168
"""Pointer references are unambiguous, thus we can parse `"/"` as being `[""]`.
6269
6370
Note that this is not possible with dot references, so this is tested separately.
6471
(`""` is special cased to be the root document, and `"."` has to be parsed to `["", ""]`).
6572
"""
6673

67-
parsed_ref, scope = pointer_ref_parser("/")
68-
assert (parsed_ref.segments, scope) == ([""], 0)
74+
parsed_ref, scope = pointer_ref_parser(pointer_reference)
75+
assert (parsed_ref.segments, scope) == expected
6976

7077

7178
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)