Skip to content

Commit 7472484

Browse files
committed
Add directive to list classes by their parent
1 parent b695395 commit 7472484

6 files changed

Lines changed: 166 additions & 21 deletions

File tree

src/AST/FieldAccess.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
class FieldAccess implements PathNode
1919
{
20-
public function __construct(private readonly FieldName $fieldName)
20+
public function __construct(private readonly FieldName|Expression $fieldName)
2121
{
2222
}
2323

src/AST/Wildcard.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ public function visit(Executor $param, $currentObject, $root): bool
1313
{
1414
return true;
1515
}
16+
17+
public function getName(): string
18+
{
19+
return '*';
20+
}
1621
}

src/Executor.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
use ArrayAccess;
1717
use Generator;
18+
use phpDocumentor\JsonPath\AST\Expression;
1819
use phpDocumentor\JsonPath\AST\FieldName;
1920
use phpDocumentor\JsonPath\AST\PathNode;
2021
use phpDocumentor\JsonPath\AST\QueryNode;
22+
use phpDocumentor\JsonPath\AST\Wildcard;
2123
use Symfony\Component\PropertyAccess\PropertyAccess;
2224
use Symfony\Component\PropertyAccess\PropertyAccessor;
2325
use Symfony\Component\PropertyAccess\PropertyPath;
@@ -26,6 +28,7 @@
2628
use function current;
2729
use function is_array;
2830
use function is_iterable;
31+
use function is_object;
2932
use function iterator_to_array;
3033
use function strrpos;
3134
use function substr;
@@ -100,8 +103,28 @@ public function evaluateFunctionCall(
100103
}
101104

102105
/** @return Generator<mixed> */
103-
public function evaluateFieldAccess(mixed $currentElement, FieldName $fieldName): Generator
106+
public function evaluateFieldAccess(mixed $currentElement, FieldName|Expression $fieldName): Generator
104107
{
108+
if ($fieldName instanceof Wildcard && is_iterable($currentElement)) {
109+
foreach ($currentElement as $element) {
110+
foreach ($element as $value) {
111+
yield $value;
112+
}
113+
}
114+
115+
return;
116+
}
117+
118+
if ($currentElement instanceof Generator) {
119+
foreach ($currentElement as $element) {
120+
foreach ($this->evaluateFieldAccess($element, $fieldName) as $result) {
121+
yield $result;
122+
}
123+
}
124+
125+
return;
126+
}
127+
105128
if (
106129
(is_array($currentElement) || $currentElement instanceof ArrayAccess) &&
107130
isset($currentElement[$fieldName->getName()])
@@ -125,6 +148,19 @@ public function evaluateFieldAccess(mixed $currentElement, FieldName $fieldName)
125148

126149
yield from $result;
127150
} else {
151+
if (is_object($currentElement) === false) {
152+
return;
153+
}
154+
155+
if (
156+
$this->propertyAccessor->isReadable(
157+
$currentElement,
158+
new PropertyPath($fieldName->getName()),
159+
) === false
160+
) {
161+
return;
162+
}
163+
128164
yield $this->propertyAccessor->getValue($currentElement, new PropertyPath($fieldName->getName()));
129165
}
130166
}

src/Parser/ParserBuilder.php

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@
2626
use phpDocumentor\JsonPath\AST\Wildcard;
2727

2828
use function is_array;
29-
use function Parsica\Parsica\alphaChar;
3029
use function Parsica\Parsica\alphaNumChar;
30+
use function Parsica\Parsica\any;
3131
use function Parsica\Parsica\atLeastOne;
3232
use function Parsica\Parsica\between;
3333
use function Parsica\Parsica\char;
3434
use function Parsica\Parsica\choice;
3535
use function Parsica\Parsica\collect;
3636
use function Parsica\Parsica\keepSecond;
37+
use function Parsica\Parsica\noneOfS;
3738
use function Parsica\Parsica\optional;
3839
use function Parsica\Parsica\recursive;
3940
use function Parsica\Parsica\sepBy;
@@ -47,7 +48,7 @@ final class ParserBuilder
4748
/** @return Parser<RootNode> */
4849
private static function rootNode(): Parser
4950
{
50-
return char('$')->map(static fn () => new RootNode());
51+
return char('$')->map(static fn () => new RootNode())->label('$');
5152
}
5253

5354
/** @return Parser<CurrentNode> */
@@ -62,16 +63,26 @@ private static function fieldAccess(): Parser
6263
$fieldName = self::fieldName();
6364

6465
return choice(
65-
keepSecond(char('.'), $fieldName),
66+
keepSecond(char('.'), any($fieldName, self::wildcard())),
6667
between(string("['"), string("']"), $fieldName),
6768
)->map(static fn ($args) => new FieldAccess($args));
6869
}
6970

71+
/** @return Parser<Wildcard> */
72+
private static function wildcard(): Parser
73+
{
74+
return string('*')->label('Wildcard')->map(static fn () => new Wildcard());
75+
}
76+
7077
/** @return Parser<FilterNode> */
7178
private static function filter(): Parser
7279
{
7380
return choice(
74-
string('[*]')->map(static fn () => new FilterNode(new Wildcard())),
81+
between(
82+
string('['),
83+
string(']'),
84+
self::wildcard(),
85+
)->map(static fn ($wildcard) => new FilterNode($wildcard)),
7586
between(
7687
string('[?('),
7788
string(')]'),
@@ -88,9 +99,11 @@ private static function expression(): Parser
8899
);
89100

90101
$value = choice(
91-
between(char('"'), char('"'), atLeastOne(alphaChar()))->map(static fn ($value) => new Value($value)),
92-
between(char("'"), char("'"), atLeastOne(alphaChar()))->map(static fn ($value) => new Value($value)),
93-
);
102+
between(char('"'), char('"'), atLeastOne(noneOfS('"')))
103+
->map(static fn ($value) => new Value($value)),
104+
between(char("'"), char("'"), atLeastOne(noneOfS("'")))
105+
->map(static fn ($value) => new Value($value)),
106+
)->label('VALUE');
94107

95108
return collect(
96109
choice(
@@ -109,15 +122,9 @@ private static function currentNodeFollowUp(): Parser
109122
self::fieldAccess(),
110123
);
111124

112-
$path = recursive();
113-
$path->recurse(collect($inner, $path));
114-
115-
return collect(
116-
self::currentNode(),
117-
some($inner)->optional()->map(static fn ($args) => is_array($args) ? $args : []),
118-
)->map(
119-
static fn ($args) => new Path([$args[0], ...$args[1]])
120-
);
125+
return self::currentNode()->followedBy(
126+
some($inner)->map(static fn ($args) => is_array($args) ? $args : []),
127+
)->map(static fn ($args) => new Path([new CurrentNode(), ...$args]));
121128
}
122129

123130
/** @return Parser<FunctionCall> */
@@ -135,10 +142,13 @@ private static function functionCall(): Parser
135142
)->map(static fn ($a) => new FunctionCall($a[0], ...$a[1]));
136143
}
137144

138-
/** @return Parser<list<Path>> */
145+
/** @return Parser<list<mixed>> */
139146
private static function arguments(): Parser
140147
{
141-
return sepBy(char(','), self::currentNodeFollowUp());
148+
return sepBy(
149+
char(','),
150+
choice(self::currentNodeFollowUp(), self::currentNode()),
151+
);
142152
}
143153

144154
/** @return Parser<FieldName> */
@@ -173,8 +183,9 @@ public function build(): Parser
173183
{
174184
return choice(
175185
self::rootFollowUp(),
186+
self::currentNodeFollowUp(),
176187
self::rootNode(),
177188
self::currentNode(),
178-
)->thenEof();
189+
)->thenEof()->label('End of Query');
179190
}
180191
}

tests/unit/ExecutorTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public function testQuerySubProperty(): void
8383
new RootNode(),
8484
new FieldAccess(new FieldName('store')),
8585
new FieldAccess(new FieldName('books')),
86+
new FieldAccess(new Wildcard()),
8687
],
8788
),
8889
$root,
@@ -108,6 +109,7 @@ public function testQuerySubPropertyByFilter(): void
108109
new RootNode(),
109110
new FieldAccess(new FieldName('store')),
110111
new FieldAccess(new FieldName('books')),
112+
new FieldAccess(new Wildcard()),
111113
new FilterNode(
112114
new Comparison(
113115
new Path([
@@ -149,6 +151,9 @@ public function testQuerySubPropertyByFilterFunctionCall(): void
149151
new FieldAccess(
150152
new FieldName('books'),
151153
),
154+
new FieldAccess(
155+
new Wildcard(),
156+
),
152157
new FilterNode(
153158
new Comparison(
154159
new FunctionCall(

tests/unit/Parser/ParserBuilderTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public function testCurrentNodeIsParsed(): void
4747
self::assertEquals(new CurrentNode(), $result->output());
4848
}
4949

50+
public function testCurrentNodeChildren(): void
51+
{
52+
$result = $this->parser->tryString('@.*');
53+
self::assertEquals(new Path([new CurrentNode(), new FieldAccess(new Wildcard())]), $result->output());
54+
}
55+
5056
public function testRootFieldAccess(): void
5157
{
5258
$result = $this->parser->tryString('$.store');
@@ -124,9 +130,67 @@ public function testFilterExpressionCurrentObjectPropertyEquals(): void
124130
);
125131
}
126132

133+
public function testFilterExpressionCurrentObjectProperyWildCard(): void
134+
{
135+
$result = $this->parser->tryString('$.store.books[?(@.* == "phpDoc")]');
136+
self::assertEquals(
137+
new Path([
138+
new RootNode(),
139+
new FieldAccess(
140+
new FieldName('store'),
141+
),
142+
new FieldAccess(
143+
new FieldName('books'),
144+
),
145+
new FilterNode(
146+
new Comparison(
147+
new Path([
148+
new CurrentNode(),
149+
new FieldAccess(new Wildcard()),
150+
]),
151+
'==',
152+
new Value(
153+
'phpDoc',
154+
),
155+
),
156+
),
157+
]),
158+
$result->output(),
159+
);
160+
}
161+
127162
public function testFilterExpressionCurrentObjectTypeEquals(): void
128163
{
129164
$result = $this->parser->tryString('$.store.books[?(type(@) == "api")]');
165+
self::assertEquals(
166+
new Path([
167+
new RootNode(),
168+
new FieldAccess(
169+
new FieldName('store'),
170+
),
171+
new FieldAccess(
172+
new FieldName('books'),
173+
),
174+
new FilterNode(
175+
new Comparison(
176+
new FunctionCall(
177+
'type',
178+
new CurrentNode(),
179+
),
180+
'==',
181+
new Value(
182+
'api',
183+
),
184+
),
185+
),
186+
]),
187+
$result->output(),
188+
);
189+
}
190+
191+
public function testFilterExpressionCurrentObjectChildrenTypeEquals(): void
192+
{
193+
$result = $this->parser->tryString('$.store.books[?(type(@.*) == "api")]');
130194
self::assertEquals(
131195
new Path([
132196
new RootNode(),
@@ -142,6 +206,7 @@ public function testFilterExpressionCurrentObjectTypeEquals(): void
142206
'type',
143207
new Path([
144208
new CurrentNode(),
209+
new FieldAccess(new Wildcard()),
145210
]),
146211
),
147212
'==',
@@ -174,4 +239,27 @@ public function testFilterExpressionWildcard(): void
174239
$result->output(),
175240
);
176241
}
242+
243+
public function testFilterExpressionWildcardNested(): void
244+
{
245+
$result = $this->parser->tryString('$.store.books_title.*[*]');
246+
self::assertEquals(
247+
new Path([
248+
new RootNode(),
249+
new FieldAccess(
250+
new FieldName('store'),
251+
),
252+
new FieldAccess(
253+
new FieldName('books_title'),
254+
),
255+
new FieldAccess(
256+
new Wildcard(),
257+
),
258+
new FilterNode(
259+
new Wildcard(),
260+
),
261+
]),
262+
$result->output(),
263+
);
264+
}
177265
}

0 commit comments

Comments
 (0)