Completed
Push — master ( e6b663...fd87f3 )
by Edward
06:50
created

CallbackBuilder::onFinish()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 26
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 35
ccs 23
cts 23
cp 1
crap 2
rs 9.504
1
<?php
2
declare(strict_types=1);
3
4
namespace Remorhaz\JSON\Path\Query;
5
6
use function array_map;
7
use function array_reverse;
8
use PhpParser\BuilderFactory;
9
use PhpParser\Node as PhpAstNode;
10
use PhpParser\Node\Arg;
11
use PhpParser\Node\Expr;
12
use PhpParser\Node\Expr\Assign;
13
use PhpParser\Node\Stmt\Expression;
14
use PhpParser\Node\Stmt\Return_;
15
use PhpParser\PrettyPrinter\Standard;
16
use Remorhaz\JSON\Data\Value\NodeValueInterface;
17
use Remorhaz\JSON\Path\Runtime\EvaluatorInterface;
18
use Remorhaz\JSON\Path\Value\ValueListInterface;
19
use Remorhaz\JSON\Path\Runtime\RuntimeInterface;
20
use Remorhaz\UniLex\AST\AbstractTranslatorListener;
21
use Remorhaz\UniLex\AST\Node as QueryAstNode;
22
use Remorhaz\UniLex\Exception as UniLexException;
23
use Remorhaz\UniLex\Stack\PushInterface;
24
25
final class CallbackBuilder extends AbstractTranslatorListener implements CallbackBuilderInterface
26
{
27
28
    private const ARG_RUNTIME = 'runtime';
29
30
    private const ARG_EVALUATOR = 'evaluator';
31
32
    private const ARG_INPUT = 'input';
33
34
    private $php;
35
36
    private $references = [];
37
38
    private $runtime;
39
40
    private $evaluator;
41
42
    private $input;
43
44
    private $stmts = [];
45
46
    private $queryCallback;
47
48
    private $capabilities;
49
50 9
    public function __construct()
51
    {
52 9
        $this->php = new BuilderFactory;
53 9
    }
54
55 4
    public function getCallback(): callable
56
    {
57 4
        if (isset($this->queryCallback)) {
58 3
            return $this->queryCallback;
59
        }
60
61 1
        throw new Exception\QueryCallbackNotFoundException;
62
    }
63
64 3
    public function getCapabilities(): CapabilitiesInterface
65
    {
66 3
        if (isset($this->capabilities)) {
67 2
            return $this->capabilities;
68
        }
69
70 1
        throw new Exception\CapabilitiesNotFoundException;
71
    }
72
73 5
    public function onStart(QueryAstNode $node): void
74
    {
75 5
        $this->runtime = $this->php->var(self::ARG_RUNTIME);
76 5
        $this->evaluator = $this->php->var(self::ARG_EVALUATOR);
77 5
        $this->input = $this->php->var(self::ARG_INPUT);
78 5
    }
79
80 3
    public function onFinish(): void
81
    {
82
        $inputParam = $this
83 3
            ->php
84 3
            ->param(self::ARG_INPUT)
85 3
            ->setType(NodeValueInterface::class)
86 3
            ->getNode();
87
        $runtimeParam = $this
88 3
            ->php
89 3
            ->param(self::ARG_RUNTIME)
90 3
            ->setType(RuntimeInterface::class)
91 3
            ->getNode();
92
        $evaluatorParam = $this
93 3
            ->php
94 3
            ->param(self::ARG_EVALUATOR)
95 3
            ->setType(EvaluatorInterface::class)
96 3
            ->getNode();
97 3
        $stmts = array_map(
98
            function (PhpAstNode $stmt): PhpAstNode {
99 2
                return $stmt instanceof Expr ? new Expression($stmt): $stmt;
100 3
            },
101 3
            $this->stmts
102
        );
103
104 3
        $closure = new Expr\Closure(
105
            [
106 3
                'stmts' => $stmts,
107
                'returnType' => ValueListInterface::class,
108 3
                'params' => [$inputParam, $runtimeParam, $evaluatorParam],
109
            ]
110
        );
111 3
        $return = new Return_($closure);
112
113 3
        $callbackCode = (new Standard)->prettyPrint([$return]);
114 3
        $this->queryCallback = eval($callbackCode);
115 3
    }
116
117 1
    public function onBeginProduction(QueryAstNode $node, PushInterface $stack): void
118
    {
119 1
        $stack->push(...array_reverse($node->getChildList()));
120 1
    }
121
122
    /**
123
     * @param QueryAstNode $node
124
     * @throws UniLexException
125
     */
126 5
    public function onFinishProduction(QueryAstNode $node): void
127
    {
128 5
        if ($this->hasReference($node)) {
129
            return;
130
        }
131 5
        switch ($node->getName()) {
132
            case AstNodeType::GET_INPUT:
133
                /** @see RuntimeInterface::getInput() */
134 4
                $this->addRuntimeMethodCall(
135 4
                    $node,
136 4
                    'getInput',
137 4
                    $this->input,
138
                );
139 4
                break;
140
141
            case AstNodeType::SET_OUTPUT:
142 5
                $this->capabilities = new Capabilities(
143 5
                    $node->getAttribute('is_definite'),
144 5
                    $node->getAttribute('is_path'),
145
                );
146 5
                $this->stmts[] = new Return_($this->getReference($node->getChild(0)));
147 4
                break;
148
149
            case AstNodeType::FETCH_FILTER_CONTEXT:
150
                /** @see RuntimeInterface::fetchFilterContext() */
151
                $this->addRuntimeMethodCall(
152
                    $node,
153
                    'fetchFilterContext',
154
                    $this->getReference($node->getChild(0)),
155
                );
156
                break;
157
158
            case AstNodeType::SPLIT_FILTER_CONTEXT:
159
                /** @see RuntimeInterface::splitFilterContext() */
160
                $this->addRuntimeMethodCall(
161
                    $node,
162
                    'splitFilterContext',
163
                    $this->getReference($node->getChild(0)),
164
                );
165
                break;
166
167
            case AstNodeType::JOIN_FILTER_RESULTS:
168
                /** @see RuntimeInterface::joinFilterResults() */
169
                $this->addRuntimeMethodCall(
170
                    $node,
171
                    'joinFilterResults',
172
                    $this->getReference($node->getChild(0)),
173
                    $this->getReference($node->getChild(1)),
174
                );
175
                break;
176
177
            case AstNodeType::EVALUATE:
178
                /** @see EvaluatorInterface::evaluate() */
179
                $this->addEvaluatorMethodCall(
180
                    $node,
181
                    'evaluate',
182
                    $this->getReference($node->getChild(0)),
183
                    $this->getReference($node->getChild(1)),
184
                );
185
                break;
186
187
            case AstNodeType::FILTER:
188
                /** @see RuntimeInterface::fetchFilteredValues() */
189
                $this->addRuntimeMethodCall(
190
                    $node,
191
                    'fetchFilteredValues',
192
                    $this->getReference($node->getChild(0)),
193
                    $this->getReference($node->getChild(1)),
194
                );
195
                break;
196
197
            case AstNodeType::EVALUATE_LOGICAL_OR:
198
                /** @see EvaluatorInterface::logicalOr() */
199
                $this->addEvaluatorMethodCall(
200
                    $node,
201
                    'logicalOr',
202
                    $this->getReference($node->getChild(0)),
203
                    $this->getReference($node->getChild(1)),
204
                );
205
                break;
206
207
            case AstNodeType::EVALUATE_LOGICAL_AND:
208
                /** @see EvaluatorInterface::logicalAnd() */
209
                $this->addEvaluatorMethodCall(
210
                    $node,
211
                    'logicalAnd',
212
                    $this->getReference($node->getChild(0)),
213
                    $this->getReference($node->getChild(1)),
214
                );
215
                break;
216
217
            case AstNodeType::EVALUATE_LOGICAL_NOT:
218
                /** @see EvaluatorInterface::logicalNot() */
219
                $this->addEvaluatorMethodCall(
220
                    $node,
221
                    'logicalNot',
222
                    $this->getReference($node->getChild(0)),
223
                );
224
                break;
225
226
            case AstNodeType::CALCULATE_IS_EQUAL:
227
                /** @see EvaluatorInterface::isEqual() */
228
                $this->addEvaluatorMethodCall(
229
                    $node,
230
                    'isEqual',
231
                    $this->getReference($node->getChild(0)),
232
                    $this->getReference($node->getChild(1)),
233
                );
234
                break;
235
236
            case AstNodeType::CALCULATE_IS_GREATER:
237
                /** @see EvaluatorInterface::isGreater() */
238
                $this->addEvaluatorMethodCall(
239
                    $node,
240
                    'isGreater',
241
                    $this->getReference($node->getChild(0)),
242
                    $this->getReference($node->getChild(1)),
243
                );
244
                break;
245
246
            case AstNodeType::CALCULATE_IS_REGEXP:
247
                /** @see EvaluatorInterface::isRegExp() */
248
                $this->addEvaluatorMethodCall(
249
                    $node,
250
                    'isRegExp',
251
                    $this->php->val($node->getAttribute('pattern')),
252
                    $this->getReference($node->getChild(0)),
253
                );
254
                break;
255
256
            case AstNodeType::FETCH_CHILDREN:
257
                /** @see RuntimeInterface::fetchChildren() */
258
                $this->addRuntimeMethodCall(
259
                    $node,
260
                    'fetchChildren',
261
                    $this->getReference($node->getChild(0)),
262
                    $this->getReference($node->getChild(1)),
263
                );
264
                break;
265
266
            case AstNodeType::FETCH_CHILDREN_DEEP:
267
                /** @see RuntimeInterface::fetchChildrenDeep() */
268
                $this->addRuntimeMethodCall(
269
                    $node,
270
                    'fetchChildrenDeep',
271
                    $this->getReference($node->getChild(0)),
272
                    $this->getReference($node->getChild(1)),
273
                );
274
                break;
275
276
            case AstNodeType::MATCH_ANY_CHILD:
277
                /** @see RuntimeInterface::matchAnyChild() */
278
                $this->addRuntimeMethodCall(
279
                    $node,
280
                    'matchAnyChild',
281
                );
282
                break;
283
284
            case AstNodeType::MATCH_PROPERTY_STRICTLY:
285
                /** @see RuntimeInterface::matchPropertyStrictly() */
286
                $this->addRuntimeMethodCall(
287
                    $node,
288
                    'matchPropertyStrictly',
289
                    ...array_map(
290
                        [$this->php, 'val'],
291
                        $node->getAttribute('names')
292
                    ),
293
                );
294
                break;
295
296
            case AstNodeType::MATCH_ELEMENT_STRICTLY:
297
                /** @see RuntimeInterface::matchElementStrictly() */
298
                $this->addRuntimeMethodCall(
299
                    $node,
300
                    'matchElementStrictly',
301
                    ...array_map(
302
                        [$this->php, 'val'],
303
                        $node->getAttribute('indexes')
304
                    ),
305
                );
306
                break;
307
308
            case AstNodeType::MATCH_ELEMENT_SLICE:
309
                /** @see RuntimeInterface::matchElementSlice() */
310
                $this->addRuntimeMethodCall(
311
                    $node,
312
                    'matchElementSlice',
313
                    $this
314
                        ->php
315
                        ->val($node->getAttribute('hasStart') ? $node->getAttribute('start') : null),
316
                    $this
317
                        ->php
318
                        ->val($node->getAttribute('hasEnd') ? $node->getAttribute('end') : null),
319
                    $this
320
                        ->php
321
                        ->val($node->getAttribute('step')),
322
                );
323
                break;
324
325
            case AstNodeType::AGGREGATE:
326
                /** @see EvaluatorInterface::aggregate() */
327
                $this->addEvaluatorMethodCall(
328
                    $node,
329
                    'aggregate',
330
                    $this->php->val($node->getAttribute('name')),
331
                    $this->getReference($node->getChild(0)),
332
                );
333
                break;
334
335
            case AstNodeType::POPULATE_LITERAL:
336
                /** @see RuntimeInterface::populateLiteral() */
337
                $this->addRuntimeMethodCall(
338
                    $node,
339
                    'populateLiteral',
340
                    $this->getReference($node->getChild(0)),
341
                    $this->getReference($node->getChild(1)),
342
                );
343
                break;
344
345
            case AstNodeType::POPULATE_ARRAY_ELEMENTS:
346
                /** @see RuntimeInterface::populateArrayElements() */
347
                $this->addRuntimeMethodCall(
348
                    $node,
349
                    'populateArrayElements',
350
                    $this->getReference($node->getChild(0)),
351
                    new Arg(
352
                        $this->getReference($node->getChild(1)),
353
                        false,
354
                        true,
355
                    ),
356
                );
357
                break;
358
359
            case AstNodeType::CREATE_SCALAR:
360
                $attributes = $node->getAttributeList();
361
                // TODO: allow accessing null attributes
362
                $value = $this->php->val($attributes['value'] ?? null);
363
                /** @see RuntimeInterface::createScalar() */
364
                $this->addRuntimeMethodCall(
365
                    $node,
366
                    'createScalar',
367
                    $value,
368
                );
369
                break;
370
371
            case AstNodeType::CREATE_ARRAY:
372
                // [ X:APPEND_TO_ARRAY ]
373
                $items = [];
374
                foreach ($node->getChildList() as $child) {
375
                    $items[] = $this->getReference($child);
376
                }
377
                $this->stmts[] = new Assign(
378
                    $this->createReference($node),
379
                    $this->php->val($items),
380
                );
381
                break;
382
383
            case AstNodeType::APPEND_TO_ARRAY:
384
                // [ 0:<value> ]
385
                $this->setReference(
386
                    $node,
387
                    $this->getReference($node->getChild(0)),
388
                );
389
                break;
390
391
            case AstNodeType::CREATE_LITERAL_ARRAY:
392
                /** @see RuntimeInterface::createArray() */
393
                $this->addRuntimeMethodCall(
394
                    $node,
395
                    'createArray',
396
                    $this->getReference($node->getChild(0)),
397
                    new Arg(
398
                        $this->getReference($node->getChild(1)),
399
                        false,
400
                        true,
401
                    ),
402
                );
403
                break;
404
        }
405 4
    }
406
407 4
    private function getVarName(QueryAstNode $node): string
408
    {
409 4
        return "var{$node->getId()}";
410
    }
411
412 4
    private function createReference(QueryAstNode $node): Expr
413
    {
414 4
        $reference = $this->php->var($this->getVarName($node));
415 4
        $this->setReference($node, $reference);
416
417 4
        return $reference;
418
    }
419
420 4
    private function setReference(QueryAstNode $node, Expr $expr): void
421
    {
422 4
        if (isset($this->references[$node->getId()])) {
423
            throw new Exception\ReferenceAlreadyExistsException($node->getId());
424
        }
425
426 4
        $this->references[$node->getId()] = $expr;
427 4
    }
428
429 5
    private function hasReference(QueryAstNode $node): bool
430
    {
431 5
        return isset($this->references[$node->getId()]);
432
    }
433
434 5
    private function getReference(QueryAstNode $node): Expr
435
    {
436 5
        if (!isset($this->references[$node->getId()])) {
437 1
            throw new Exception\ReferenceNotFoundException($node->getId());
438
        }
439
440 4
        return $this->references[$node->getId()];
441
    }
442
443 4
    private function addRuntimeMethodCall(QueryAstNode $node, string $method, PhpAstNode ...$args): void
444
    {
445
        $methodCall = $this
446 4
            ->php
447 4
            ->methodCall($this->runtime, $method, $args);
448 4
        $this->stmts[] = new Assign($this->createReference($node), $methodCall);
449 4
    }
450
451
    private function addEvaluatorMethodCall(QueryAstNode $node, string $method, PhpAstNode ...$args): void
452
    {
453
        $methodCall = $this
454
            ->php
455
            ->methodCall($this->evaluator, $method, $args);
456
        $this->stmts[] = new Assign($this->createReference($node), $methodCall);
457
    }
458
}
459