Skip to content

Commit 78581c2

Browse files
committed
Add support for logical OR expressions, add support for Grouping
1 parent e78d936 commit 78581c2

File tree

2 files changed

+73
-36
lines changed

2 files changed

+73
-36
lines changed

src/Filters/QueryMatchFilter.php

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ class QueryMatchFilter extends AbstractFilter
2020
protected const MATCH_QUERY_NEGATION_WRAPPED = '^(?<negate>!)\((?<logicalexpr>.+)\)$';
2121
protected const MATCH_QUERY_NEGATION_UNWRAPPED = '^(?<negate>!)(?<logicalexpr>.+)$';
2222
protected const MATCH_QUERY_OPERATORS = '
23-
(@\.(?<key>[^\s<>!=]+)|@\[["\']?(?<keySquare>.*?)["\']?\]|(?<node>@))
24-
(\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?<comparisonValue>.+?(?=(&&|$))))?
25-
(\s*(?<logicaland>&&)\s*)?
23+
(@\.(?<key>[^\s<>!=]+)|@\[["\']?(?<keySquare>.*?)["\']?\]|(?<node>@)|(%group(?<group>\d+)%))
24+
(\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?<comparisonValue>.+?(?=(&&|$|\|\||%))))?
25+
(\s*(?<logicalandor>&&|\|\|)\s*)?
2626
';
2727

28+
protected const MATCH_GROUPED_EXPRESSION = '#\([^)(]*+(?:(?R)[^)(]*)*+\)#';
29+
2830
/**
2931
* @throws JSONPathException
3032
*/
@@ -40,6 +42,25 @@ public function filter($collection): array
4042
$filterExpression = $negationMatches['logicalexpr'];
4143
}
4244

45+
$filterGroups = [];
46+
if (
47+
\preg_match_all(
48+
static::MATCH_GROUPED_EXPRESSION,
49+
$filterExpression,
50+
$matches,
51+
PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL
52+
)
53+
) {
54+
foreach ($matches[0] as $i => $matchesGroup) {
55+
$test = \substr($matchesGroup[0], 1, -1);
56+
//sanity check that our group is a group and not something within a string or regular expression
57+
if (\preg_match('/' . static::MATCH_QUERY_OPERATORS . '/x', $test)) {
58+
$filterGroups[$i] = $test;
59+
$filterExpression = \str_replace($matchesGroup[0], "%group$i%", $filterExpression);
60+
}
61+
}
62+
}
63+
4364
$match = \preg_match_all(
4465
'/' . static::MATCH_QUERY_OPERATORS . '/x',
4566
$filterExpression,
@@ -50,18 +71,35 @@ public function filter($collection): array
5071
if (
5172
$match === false
5273
|| !isset($matches[1][0])
53-
|| isset($matches['logicaland'][array_key_last($matches['logicaland'])])
74+
|| isset($matches['logicalandor'][array_key_last($matches['logicalandor'])])
5475
) {
5576
throw new RuntimeException('Malformed filter query');
5677
}
5778

5879
$return = [];
5980

60-
for ($logicalAndNum = 0; $logicalAndNum < \count($matches[0]); $logicalAndNum++) {
61-
$key = $matches['key'][$logicalAndNum] ?: $matches['keySquare'][$logicalAndNum];
81+
for ($expressionPart = 0; $expressionPart < \count($matches[0]); $expressionPart++) {
82+
$filteredCollection = $collection;
83+
$logicalJoin = $expressionPart > 0 ? $matches['logicalandor'][$expressionPart - 1] : null;
84+
if ($logicalJoin === '&&') {
85+
//Restrict the nodes we need to look at to those already meeting criteria
86+
$filteredCollection = $return;
87+
$return = [];
88+
}
89+
90+
//Processing a group
91+
if ($matches['group'][$expressionPart] !== null) {
92+
$filter = '$[?(' . $filterGroups[$matches['group'][$expressionPart]] . ')]';
93+
$resolve = (new JSONPath($filteredCollection))->find($filter)->getData();
94+
$return = $resolve;
95+
continue;
96+
}
6297

63-
$operator = $matches['operator'][$logicalAndNum] ?? null;
64-
$comparisonValue = $matches['comparisonValue'][$logicalAndNum] ?? null;
98+
//Process a normal expression
99+
$key = $matches['key'][$expressionPart] ?: $matches['keySquare'][$expressionPart];
100+
101+
$operator = $matches['operator'][$expressionPart] ?? null;
102+
$comparisonValue = $matches['comparisonValue'][$expressionPart] ?? null;
65103

66104
if (\is_string($comparisonValue)) {
67105
$comparisonValue = \preg_replace('/^[\']/', '"', $comparisonValue);
@@ -73,46 +111,45 @@ public function filter($collection): array
73111
}
74112
}
75113

76-
$filteredCollection = $collection;
77-
if ($logicalAndNum > 0) {
78-
$filteredCollection = $return;
79-
$return = [];
80-
}
81-
82-
foreach ($filteredCollection as $value) {
83-
$value1 = null;
114+
foreach ($filteredCollection as $nodeIndex => $node) {
115+
if ($logicalJoin === '||' && \array_key_exists($nodeIndex, $return)) {
116+
//Short-circuit, node already exists in output due to previous test
117+
continue;
118+
}
119+
$selectedNode = null;
84120

85-
$notNothing = AccessHelper::keyExists($value, $key, $this->magicIsAllowed);
121+
$notNothing = AccessHelper::keyExists($node, $key, $this->magicIsAllowed);
86122
if ($key) {
87123
if ($notNothing) {
88-
$value1 = AccessHelper::getValue($value, $key, $this->magicIsAllowed);
124+
$selectedNode = AccessHelper::getValue($node, $key, $this->magicIsAllowed);
89125
} elseif (\str_contains($key, '.')) {
90-
$foundValue = (new JSONPath($value))->find($key)->getData();
126+
$foundValue = (new JSONPath($node))->find($key)->getData();
91127
if ($foundValue) {
92-
$value1 = $foundValue[0];
128+
$selectedNode = $foundValue[0];
93129
$notNothing = true;
94130
}
95131
}
96132
} else {
97-
$value1 = $value;
133+
//Node selection was plain @
134+
$selectedNode = $node;
98135
$notNothing = true;
99136
}
100137

101138
$comparisonResult = null;
102139
if ($notNothing) {
103140
$comparisonResult = match ($operator) {
104-
null => AccessHelper::keyExists($value, $key, $this->magicIsAllowed) || (!$key),
105-
"=", "==" => $this->compareEquals($value1, $comparisonValue),
106-
"!=", "!==", "<>" => !$this->compareEquals($value1, $comparisonValue),
107-
'=~' => @\preg_match($comparisonValue, $value1),
108-
'<' => $this->compareLessThan($value1, $comparisonValue),
109-
'<=' => $this->compareLessThan($value1, $comparisonValue)
110-
|| $this->compareEquals($value1, $comparisonValue),
111-
'>' => $this->compareLessThan($comparisonValue, $value1), //rfc semantics
112-
'>=' => $this->compareLessThan($comparisonValue, $value1) //rfc semantics
113-
|| $this->compareEquals($value1, $comparisonValue),
114-
"in" => \is_array($comparisonValue) && \in_array($value1, $comparisonValue, true),
115-
'nin', "!in" => \is_array($comparisonValue) && !\in_array($value1, $comparisonValue, true)
141+
null => AccessHelper::keyExists($node, $key, $this->magicIsAllowed) || (!$key),
142+
"=", "==" => $this->compareEquals($selectedNode, $comparisonValue),
143+
"!=", "!==", "<>" => !$this->compareEquals($selectedNode, $comparisonValue),
144+
'=~' => @\preg_match($comparisonValue, $selectedNode),
145+
'<' => $this->compareLessThan($selectedNode, $comparisonValue),
146+
'<=' => $this->compareLessThan($selectedNode, $comparisonValue)
147+
|| $this->compareEquals($selectedNode, $comparisonValue),
148+
'>' => $this->compareLessThan($comparisonValue, $selectedNode), //rfc semantics
149+
'>=' => $this->compareLessThan($comparisonValue, $selectedNode) //rfc semantics
150+
|| $this->compareEquals($selectedNode, $comparisonValue),
151+
"in" => \is_array($comparisonValue) && \in_array($selectedNode, $comparisonValue, true),
152+
'nin', "!in" => \is_array($comparisonValue) && !\in_array($selectedNode, $comparisonValue, true)
116153
};
117154
}
118155

@@ -121,11 +158,13 @@ public function filter($collection): array
121158
}
122159

123160
if ($comparisonResult) {
124-
$return[] = $value;
161+
$return[$nodeIndex] = $node;
125162
}
126163
}
127164
}
128165

166+
//Keep out returned nodes in the same order they were defined in the original collection
167+
\ksort($return);
129168
return $return;
130169
}
131170

tests/data/baselineFailedQueries.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
bracket_notation_with_quoted_closing_bracket_literal
22
dot_notation_with_key_root_literal
3-
filter_expression_with_different_grouped_operators
43
filter_expression_with_tautological_comparison
54
union_with_wildcard_and_number
65
array_slice_with_large_number_for_end_and_negative_step
@@ -17,7 +16,6 @@ bracket_notation_with_wildcard_after_recursive_descent
1716
dot_notation_with_number
1817
dot_notation_with_number_-1
1918
dot_notation_with_wildcard_after_recursive_descent
20-
filter_expression_with_boolean_or_operator
2119
filter_expression_with_bracket_notation_with_-1
2220
filter_expression_with_equals_with_root_reference
2321
# XFAIL

0 commit comments

Comments
 (0)