Test Setup Failed
Push — master ( 1595cb...1df194 )
by Kirill
02:11
created

LinkerVisitor   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 124
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 30
dl 0
loc 124
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Railt package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Railt\SDL\Backend\Linker;
13
14
use GraphQL\Contracts\TypeSystem\DirectiveInterface;
15
use GraphQL\Contracts\TypeSystem\Type\NamedTypeInterface;
16
use Phplrt\Contracts\Ast\NodeInterface;
17
use Phplrt\Source\Exception\NotAccessibleException;
18
use Phplrt\Visitor\Visitor;
19
use Railt\SDL\Backend\Context\Context;
20
use Railt\SDL\Backend\ContextInterface;
21
use Railt\SDL\Exception\TypeNotFoundException;
22
use Railt\SDL\Frontend\Ast\Definition\Type\TypeDefinitionNode;
23
use Railt\SDL\Frontend\Ast\Executable\DirectiveNode;
24
use Railt\SDL\Frontend\Ast\Extension\Type\TypeExtensionNode;
25
use Railt\SDL\Frontend\Ast\Identifier;
26
use Railt\SDL\Frontend\Ast\Type\NamedTypeNode;
27
use Railt\SDL\Frontend\Ast\TypeName;
28
use Railt\TypeSystem\Schema;
29
30
/**
31
 * Class LinkerVisitor
32
 */
33
class LinkerVisitor extends Visitor
34
{
35
    /**
36
     * @var string
37
     */
38
    private const ERROR_TYPE_NOT_FOUND = 'Type "%s" not found or could not be loaded';
39
40
    /**
41
     * @var string
42
     */
43
    private const ERROR_DIRECTIVE_NOT_FOUND = 'Directive "@%s" not found or could not be loaded';
44
45
    /**
46
     * @var ContextInterface
47
     */
48
    private ContextInterface $context;
49
50
    /**
51
     * @var LinkerInterface
52
     */
53
    private LinkerInterface $linker;
54
55
    /**
56
     * @var array|string[]
57
     */
58
    private array $genericArguments = [];
59
60
    /**
61
     * LinkerVisitor constructor.
62
     *
63
     * @param ContextInterface $context
64
     * @param LinkerInterface $linker
65
     */
66
    public function __construct(ContextInterface $context, LinkerInterface $linker)
67
    {
68
        $this->context = $context;
69
        $this->linker = $linker;
70
    }
71
72
    /**
73
     * @param NodeInterface $node
74
     * @return void
75
     */
76
    public function enter(NodeInterface $node): void
77
    {
78
        if ($node instanceof TypeDefinitionNode && $node->name instanceof TypeName) {
79
            $map = fn(Identifier $name): string => $name->value;
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected ':', expecting T_DOUBLE_ARROW on line 79 at column 39
Loading history...
80
81
            $this->genericArguments = \array_map($map, $node->name->arguments);
82
        }
83
    }
84
85
    /**
86
     * @param NodeInterface $node
87
     * @return mixed|void|null
88
     */
89
    public function leave(NodeInterface $node)
90
    {
91
        switch (true) {
92
            //
93
            // If current context was closed then we should remove
94
            // current state of generic arguments.
95
            //
96
            case $node instanceof TypeDefinitionNode:
97
                $this->genericArguments = [];
98
99
                break;
100
101
            //
102
            // Load all type dependencies.
103
            //
104
            // Note: Skip type loading if type is part of type template parameter.
105
            //
106
            case $node instanceof NamedTypeNode:
107
            case $node instanceof TypeExtensionNode:
108
                if (! \in_array($node->name->value, $this->genericArguments, true)) {
109
                    $this->assertTypeExists($node->name);
110
                }
111
112
                break;
113
114
            //
115
            // Load all directive dependencies
116
            //
117
            case $node instanceof DirectiveNode:
118
                $this->assertDirectiveExists($node);
119
                break;
120
        }
121
    }
122
123
    /**
124
     * @param Identifier $name
125
     * @return void
126
     */
127
    private function assertTypeExists(Identifier $name): void
128
    {
129
        if (! $this->context->hasType($name->value)) {
130
            $this->loadOr($name->value, function () use ($name): void {
131
                throw $this->typeNotFound($name);
132
            });
133
        }
134
    }
135
136
    /**
137
     * @param string $type
138
     * @param \Closure $otherwise
139
     * @return void
140
     */
141
    private function loadOr(string $type, \Closure $otherwise): void
142
    {
143
        /** @var Schema $schema */
144
        $schema = $this->context->getSchema();
145
146
        foreach ($this->linker->getAutoloaders() as $autoloader) {
147
            $result = $autoloader($type);
148
149
            switch (true) {
150
                // When autoload returns NamedTypeInterface
151
                case $result instanceof NamedTypeInterface:
152
                    $schema->addType($result);
153
154
                    return;
155
156
                // When autoload returns DirectiveInterface
157
                case $result instanceof DirectiveInterface:
158
                    $schema->addDirective($result);
159
160
                    break;
161
            }
162
        }
163
164
        $otherwise();
165
    }
166
167
    /**
168
     * @param Identifier $name
169
     * @return TypeNotFoundException
170
     * @throws NotAccessibleException
171
     * @throws \RuntimeException
172
     */
173
    private function typeNotFound(Identifier $name): TypeNotFoundException
174
    {
175
        $error = \sprintf(self::ERROR_TYPE_NOT_FOUND, $name->value);
176
177
        return TypeNotFoundException::fromAst($error, $name);
178
    }
179
180
    /**
181
     * @param DirectiveNode $directive
182
     * @return void
183
     */
184
    private function assertDirectiveExists(DirectiveNode $directive): void
185
    {
186
        $name = $directive->name->name->value;
187
188
        if (! $this->context->hasDirective($name)) {
189
            $this->loadOr('@' . $name, function () use ($directive): void {
190
                throw $this->directiveNotFound($directive);
191
            });
192
        }
193
    }
194
195
    /**
196
     * @param DirectiveNode $directive
197
     * @return TypeNotFoundException
198
     * @throws NotAccessibleException
199
     * @throws \RuntimeException
200
     */
201
    private function directiveNotFound(DirectiveNode $directive): TypeNotFoundException
202
    {
203
        $error = \sprintf(self::ERROR_DIRECTIVE_NOT_FOUND, $directive->name->name->value);
204
205
        return TypeNotFoundException::fromAst($error, $directive);
206
    }
207
}
208