Skip to content

Commit

Permalink
Merge pull request #63 from jg-rp/membership
Browse files Browse the repository at this point in the history
Fix membership operators and filter expression literals
  • Loading branch information
jg-rp authored Jul 6, 2024
2 parents c28196a + 8c9f0cf commit 8cada95
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

**Fixes**

- Fixed handling of JSONPath literals in filter expressions. We now raise a `JSONPathSyntaxError` if a filter expression literal is not part of a comparison or function expression. See [jsonpath-compliance-test-suite#81](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/81).
- Fixed handling of JSONPath literals in filter expressions. We now raise a `JSONPathSyntaxError` if a filter expression literal is not part of a comparison, membership or function expression. See [jsonpath-compliance-test-suite#81](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite/pull/81).

**Features**

- Allow JSONPath filter expression membership operators (`contains` and `in`) to operate on object/mapping data as well as arrays/sequences. See [#55](https://github.com/jg-rp/python-jsonpath/issues/55).
- Added a `select` method to the JSONPath [query iterator interface](https://jg-rp.github.io/python-jsonpath/query/), generating a projection of each JSONPath match by selecting a subset of its values.
- Added the `addne` and `addap` operations to [JSONPatch](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch). `addne` (add if not exists) is like the standard `add` operation, but only adds object keys/values if the key does not exist. `addap` (add or append) is like the standard `add` operation, but assumes an index of `-` if the target index can not be resolved.

Expand Down
5 changes: 3 additions & 2 deletions jsonpath/env.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Core JSONPath configuration object."""

from __future__ import annotations

import re
Expand Down Expand Up @@ -548,9 +549,9 @@ def compare( # noqa: PLR0911
return self._lt(right, left) or self._eq(left, right)
if operator == "<=":
return self._lt(left, right) or self._eq(left, right)
if operator == "in" and isinstance(right, Sequence):
if operator == "in" and isinstance(right, (Mapping, Sequence)):
return left in right
if operator == "contains" and isinstance(left, Sequence):
if operator == "contains" and isinstance(left, (Mapping, Sequence)):
return right in left
if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str):
return bool(right.fullmatch(left))
Expand Down
22 changes: 19 additions & 3 deletions jsonpath/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ class Parser:
]
)

# Infix operators that accept filter expression literals.
INFIX_LITERAL_OPERATORS = frozenset(
[
"==",
">=",
">",
"<=",
"<",
"!=",
"<>",
"=~",
"in",
"contains",
]
)

PREFIX_OPERATORS = frozenset(
[
TOKEN_NOT,
Expand Down Expand Up @@ -530,14 +546,14 @@ def parse_infix_expression(
self._raise_for_non_comparable_function(left, tok)
self._raise_for_non_comparable_function(right, tok)

if operator not in self.COMPARISON_OPERATORS:
if isinstance(left, Literal):
if operator not in self.INFIX_LITERAL_OPERATORS:
if isinstance(left, (Literal, Nil)):
raise JSONPathSyntaxError(
"filter expression literals outside of "
"function expressions must be compared",
token=tok,
)
if isinstance(right, Literal):
if isinstance(right, (Literal, Nil)):
raise JSONPathSyntaxError(
"filter expression literals outside of "
"function expressions must be compared",
Expand Down
40 changes: 40 additions & 0 deletions tests/test_find.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,46 @@ class Case:
data=[{"a": True, "b": False}],
want=[{"a": True, "b": False}],
),
Case(
description="array contains literal",
path="$[[email protected] contains 'foo']",
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
want=[
{
"a": ["foo", "bar"],
}
],
),
Case(
description="object contains literal",
path="$[[email protected] contains 'foo']",
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
want=[
{
"a": {"foo": "bar"},
}
],
),
Case(
description="literal in array",
path="$[?'foo' in @.a]",
data=[{"a": ["foo", "bar"]}, {"a": ["bar"]}],
want=[
{
"a": ["foo", "bar"],
}
],
),
Case(
description="literal in object",
path="$[?'foo' in @.a]",
data=[{"a": {"foo": "bar"}}, {"a": {"bar": "baz"}}],
want=[
{
"a": {"foo": "bar"},
}
],
),
]


Expand Down

0 comments on commit 8cada95

Please sign in to comment.