Completed
Push — master ( 974258...005b1a )
by Vladimir
19:57 queued 16:19
created

ASTDefinitionBuilder::makeEnumDef()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
dl 0
loc 21
ccs 0
cts 14
cp 0
rs 9.7998
c 1
b 0
f 0
cc 2
nc 1
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Executor\Values;
9
use GraphQL\Language\AST\DirectiveDefinitionNode;
10
use GraphQL\Language\AST\EnumTypeDefinitionNode;
11
use GraphQL\Language\AST\EnumTypeExtensionNode;
12
use GraphQL\Language\AST\EnumValueDefinitionNode;
13
use GraphQL\Language\AST\FieldDefinitionNode;
14
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
15
use GraphQL\Language\AST\InputValueDefinitionNode;
16
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
17
use GraphQL\Language\AST\ListTypeNode;
18
use GraphQL\Language\AST\NamedTypeNode;
19
use GraphQL\Language\AST\Node;
20
use GraphQL\Language\AST\NonNullTypeNode;
21
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
22
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
23
use GraphQL\Language\AST\TypeNode;
24
use GraphQL\Language\AST\UnionTypeDefinitionNode;
25
use GraphQL\Language\Token;
26
use GraphQL\Type\Definition\CustomScalarType;
27
use GraphQL\Type\Definition\Directive;
28
use GraphQL\Type\Definition\EnumType;
29
use GraphQL\Type\Definition\FieldArgument;
30
use GraphQL\Type\Definition\InputObjectType;
31
use GraphQL\Type\Definition\InputType;
32
use GraphQL\Type\Definition\InterfaceType;
33
use GraphQL\Type\Definition\NonNull;
34
use GraphQL\Type\Definition\ObjectType;
35
use GraphQL\Type\Definition\Type;
36
use GraphQL\Type\Definition\UnionType;
37
use Throwable;
38
use function array_reverse;
39
use function implode;
40
use function is_array;
41
use function is_string;
42
use function sprintf;
43
44
class ASTDefinitionBuilder
45
{
46
    /** @var Node[] */
47
    private $typeDefinitionsMap;
48
49
    /** @var callable */
50
    private $typeConfigDecorator;
51
52
    /** @var bool[] */
53
    private $options;
54
55
    /** @var callable */
56
    private $resolveType;
57
58
    /** @var Type[] */
59
    private $cache;
60
61
    /**
62
     * @param Node[] $typeDefinitionsMap
63
     * @param bool[] $options
64
     */
65 20
    public function __construct(
66
        array $typeDefinitionsMap,
67
        $options,
68
        callable $resolveType,
69
        ?callable $typeConfigDecorator = null
70
    ) {
71 20
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
72 20
        $this->typeConfigDecorator = $typeConfigDecorator;
73 20
        $this->options             = $options;
74 20
        $this->resolveType         = $resolveType;
75
76 20
        $this->cache = Type::getAllBuiltInTypes();
77 20
    }
78
79 7
    public function buildDirective(DirectiveDefinitionNode $directiveNode)
80
    {
81 7
        return new Directive([
82 7
            'name'        => $directiveNode->name->value,
83 7
            'description' => $this->getDescription($directiveNode),
84 7
            'locations'   => Utils::map(
85 7
                $directiveNode->locations,
86
                static function ($node) {
87 7
                    return $node->value;
88 7
                }
89
            ),
90 7
            'args'        => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
91 7
            'astNode'     => $directiveNode,
92
        ]);
93
    }
94
95
    /**
96
     * Given an ast node, returns its string description.
97
     */
98 11
    private function getDescription($node)
99
    {
100 11
        if ($node->description) {
101 1
            return $node->description->value;
102
        }
103 10
        if (isset($this->options['commentDescriptions'])) {
104 1
            $rawValue = $this->getLeadingCommentBlock($node);
105 1
            if ($rawValue !== null) {
106 1
                return BlockString::value("\n" . $rawValue);
107
            }
108
        }
109
110 9
        return null;
111
    }
112
113 1
    private function getLeadingCommentBlock($node)
114
    {
115 1
        $loc = $node->loc;
116 1
        if (! $loc || ! $loc->startToken) {
117
            return null;
118
        }
119 1
        $comments = [];
120 1
        $token    = $loc->startToken->prev;
121 1
        while ($token &&
122 1
            $token->kind === Token::COMMENT &&
123 1
            $token->next && $token->prev &&
124 1
            $token->line + 1 === $token->next->line &&
125 1
            $token->line !== $token->prev->line
126
        ) {
127 1
            $value      = $token->value;
128 1
            $comments[] = $value;
129 1
            $token      = $token->prev;
130
        }
131
132 1
        return implode("\n", array_reverse($comments));
133
    }
134
135 8
    private function makeInputValues($values)
136
    {
137 8
        return Utils::keyValMap(
138 8
            $values,
139
            static function ($value) {
140 4
                return $value->name->value;
141 8
            },
142
            function ($value) {
143
                // Note: While this could make assertions to get the correctly typed
144
                // value, that would throw immediately while type system validation
145
                // with validateSchema() will produce more actionable results.
146 4
                $type = $this->internalBuildWrappedType($value->type);
147
148
                $config = [
149 4
                    'name'        => $value->name->value,
150 4
                    'type'        => $type,
151 4
                    'description' => $this->getDescription($value),
152 4
                    'astNode'     => $value,
153
                ];
154 4
                if (isset($value->defaultValue)) {
155
                    $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
156
                }
157
158 4
                return $config;
159 8
            }
160
        );
161
    }
162
163
    /**
164
     * @return Type|InputType
165
     *
166
     * @throws Error
167
     */
168 6
    private function internalBuildWrappedType(TypeNode $typeNode)
169
    {
170 6
        $typeDef = $this->buildType($this->getNamedTypeNode($typeNode));
171
172 5
        return $this->buildWrappedType($typeDef, $typeNode);
173
    }
174
175
    /**
176
     * @param string|NamedTypeNode $ref
177
     *
178
     * @return Type
179
     *
180
     * @throws Error
181
     */
182 12
    public function buildType($ref)
183
    {
184 12
        if (is_string($ref)) {
185 1
            return $this->internalBuildType($ref);
186
        }
187
188 11
        return $this->internalBuildType($ref->name->value, $ref);
189
    }
190
191
    /**
192
     * @param string             $typeName
193
     * @param NamedTypeNode|null $typeNode
194
     *
195
     * @return Type
196
     *
197
     * @throws Error
198
     */
199 12
    private function internalBuildType($typeName, $typeNode = null)
200
    {
201 12
        if (! isset($this->cache[$typeName])) {
202 8
            if (isset($this->typeDefinitionsMap[$typeName])) {
203 4
                $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
204 4
                if ($this->typeConfigDecorator) {
205
                    $fn = $this->typeConfigDecorator;
206
                    try {
207
                        $config = $fn($type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap);
208
                    } catch (Throwable $e) {
209
                        throw new Error(
210
                            sprintf('Type config decorator passed to %s threw an error ', static::class) .
211
                            sprintf('when building %s type: %s', $typeName, $e->getMessage()),
212
                            null,
213
                            null,
214
                            null,
215
                            null,
216
                            $e
217
                        );
218
                    }
219
                    if (! is_array($config) || isset($config[0])) {
220
                        throw new Error(
221
                            sprintf(
222
                                'Type config decorator passed to %s is expected to return an array, but got %s',
223
                                static::class,
224
                                Utils::getVariableType($config)
225
                            )
226
                        );
227
                    }
228
                    $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
229
                }
230 4
                $this->cache[$typeName] = $type;
231
            } else {
232 4
                $fn                     = $this->resolveType;
233 4
                $this->cache[$typeName] = $fn($typeName, $typeNode);
234
            }
235
        }
236
237 11
        return $this->cache[$typeName];
238
    }
239
240
    /**
241
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def
242
     *
243
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
244
     *
245
     * @throws Error
246
     */
247 4
    private function makeSchemaDef(Node $def)
248
    {
249
        switch (true) {
250 4
            case $def instanceof ObjectTypeDefinitionNode:
251 3
                return $this->makeTypeDef($def);
252 1
            case $def instanceof InterfaceTypeDefinitionNode:
253
                return $this->makeInterfaceDef($def);
254 1
            case $def instanceof EnumTypeDefinitionNode:
255
                return $this->makeEnumDef($def);
256 1
            case $def instanceof UnionTypeDefinitionNode:
257
                return $this->makeUnionDef($def);
258 1
            case $def instanceof ScalarTypeDefinitionNode:
259
                return $this->makeScalarDef($def);
260 1
            case $def instanceof InputObjectTypeDefinitionNode:
261 1
                return $this->makeInputObjectDef($def);
262
            default:
263
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
264
        }
265
    }
266
267 3
    private function makeTypeDef(ObjectTypeDefinitionNode $def)
268
    {
269 3
        $typeName = $def->name->value;
270
271 3
        return new ObjectType([
272 3
            'name'        => $typeName,
273 3
            'description' => $this->getDescription($def),
274
            'fields'      => function () use ($def) {
275
                return $this->makeFieldDefMap($def);
276 3
            },
277
            'interfaces'  => function () use ($def) {
278
                return $this->makeImplementedInterfaces($def);
279 3
            },
280 3
            'astNode'     => $def,
281
        ]);
282
    }
283
284
    private function makeFieldDefMap($def)
285
    {
286
        return $def->fields
287
            ? Utils::keyValMap(
288
                $def->fields,
289
                static function ($field) {
290
                    return $field->name->value;
291
                },
292
                function ($field) {
293
                    return $this->buildField($field);
294
                }
295
            )
296
            : [];
297
    }
298
299 1
    public function buildField(FieldDefinitionNode $field)
300
    {
301
        return [
302
            // Note: While this could make assertions to get the correctly typed
303
            // value, that would throw immediately while type system validation
304
            // with validateSchema() will produce more actionable results.
305 1
            'type'              => $this->internalBuildWrappedType($field->type),
306
            'description'       => $this->getDescription($field),
307
            'args'              => $field->arguments ? $this->makeInputValues($field->arguments) : null,
308
            'deprecationReason' => $this->getDeprecationReason($field),
309
            'astNode'           => $field,
310
        ];
311
    }
312
313
    /**
314
     * Given a collection of directives, returns the string value for the
315
     * deprecation reason.
316
     *
317
     * @param EnumValueDefinitionNode | FieldDefinitionNode $node
318
     *
319
     * @return string
320
     */
321
    private function getDeprecationReason($node)
322
    {
323
        $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
324
325
        return $deprecated['reason'] ?? null;
326
    }
327
328
    private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
329
    {
330
        if ($def->interfaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $def->interfaces of type GraphQL\Language\AST\NamedTypeNode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
331
            // Note: While this could make early assertions to get the correctly
332
            // typed values, that would throw immediately while type system
333
            // validation with validateSchema() will produce more actionable results.
334
            return Utils::map(
335
                $def->interfaces,
336
                function ($iface) {
337
                    return $this->buildType($iface);
338
                }
339
            );
340
        }
341
342
        return null;
343
    }
344
345
    private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
346
    {
347
        $typeName = $def->name->value;
348
349
        return new InterfaceType([
350
            'name'        => $typeName,
351
            'description' => $this->getDescription($def),
352
            'fields'      => function () use ($def) {
353
                return $this->makeFieldDefMap($def);
354
            },
355
            'astNode'     => $def,
356
        ]);
357
    }
358
359
    private function makeEnumDef(EnumTypeDefinitionNode $def)
360
    {
361
        return new EnumType([
362
            'name'        => $def->name->value,
363
            'description' => $this->getDescription($def),
364
            'values'      => $def->values
365
                ? Utils::keyValMap(
366
                    $def->values,
367
                    static function ($enumValue) {
368
                        return $enumValue->name->value;
369
                    },
370
                    function ($enumValue) {
371
                        return [
372
                            'description'       => $this->getDescription($enumValue),
373
                            'deprecationReason' => $this->getDeprecationReason($enumValue),
374
                            'astNode'           => $enumValue,
375
                        ];
376
                    }
377
                )
378
                : [],
379
            'astNode'     => $def,
380
        ]);
381
    }
382
383
    private function makeUnionDef(UnionTypeDefinitionNode $def)
384
    {
385
        return new UnionType([
386
            'name'        => $def->name->value,
387
            'description' => $this->getDescription($def),
388
            // Note: While this could make assertions to get the correctly typed
389
            // values below, that would throw immediately while type system
390
            // validation with validateSchema() will produce more actionable results.
391
            'types'       => $def->types
392
                ? Utils::map(
393
                    $def->types,
394
                    function ($typeNode) {
395
                        return $this->buildType($typeNode);
396
                    }
397
                )
398
                : [],
399
            'astNode'     => $def,
400
        ]);
401
    }
402
403
    private function makeScalarDef(ScalarTypeDefinitionNode $def)
404
    {
405
        return new CustomScalarType([
406
            'name'        => $def->name->value,
407
            'description' => $this->getDescription($def),
408
            'astNode'     => $def,
409
            'serialize'   => static function ($value) {
410
                return $value;
411
            },
412
        ]);
413
    }
414
415 1
    private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
416
    {
417 1
        return new InputObjectType([
418 1
            'name'        => $def->name->value,
419 1
            'description' => $this->getDescription($def),
420
            'fields'      => function () use ($def) {
421 1
                return $def->fields
422 1
                    ? $this->makeInputValues($def->fields)
423 1
                    : [];
424 1
            },
425 1
            'astNode'     => $def,
426
        ]);
427
    }
428
429
    /**
430
     * @param mixed[] $config
431
     *
432
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
433
     *
434
     * @throws Error
435
     */
436
    private function makeSchemaDefFromConfig(Node $def, array $config)
437
    {
438
        switch (true) {
439
            case $def instanceof ObjectTypeDefinitionNode:
440
                return new ObjectType($config);
441
            case $def instanceof InterfaceTypeDefinitionNode:
442
                return new InterfaceType($config);
443
            case $def instanceof EnumTypeDefinitionNode:
444
                return new EnumType($config);
445
            case $def instanceof UnionTypeDefinitionNode:
446
                return new UnionType($config);
447
            case $def instanceof ScalarTypeDefinitionNode:
448
                return new CustomScalarType($config);
449
            case $def instanceof InputObjectTypeDefinitionNode:
450
                return new InputObjectType($config);
451
            default:
452
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
453
        }
454
    }
455
456 6
    private function getNamedTypeNode(TypeNode $typeNode) : TypeNode
457
    {
458 6
        $namedType = $typeNode;
459 6
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
460 3
            $namedType = $namedType->type;
461
        }
462
463 6
        return $namedType;
464
    }
465
466 5
    private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) : Type
467
    {
468 5
        if ($inputTypeNode instanceof ListTypeNode) {
469
            return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
470
        }
471 5
        if ($inputTypeNode instanceof NonNullTypeNode) {
472 3
            $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
473
474 3
            return Type::nonNull(NonNull::assertNullableType($wrappedType));
475
        }
476
477 5
        return $innerType;
478
    }
479
480
    /**
481
     * @return mixed[]
482
     */
483 2
    public function buildInputField(InputValueDefinitionNode $value) : array
484
    {
485 2
        $type = $this->internalBuildWrappedType($value->type);
486
487
        $config = [
488 1
            'name' => $value->name->value,
489 1
            'type' => $type,
490 1
            'description' => $this->getDescription($value),
491 1
            'astNode' => $value,
492
        ];
493
494 1
        if ($value->defaultValue) {
495
            $config['defaultValue'] = $value->defaultValue;
496
        }
497
498 1
        return $config;
499
    }
500
501
    /**
502
     * @return mixed[]
503
     */
504
    public function buildEnumValue(EnumValueDefinitionNode $value) : array
505
    {
506
        return [
507
            'description' => $this->getDescription($value),
508
            'deprecationReason' => $this->getDeprecationReason($value),
509
            'astNode' => $value,
510
        ];
511
    }
512
}
513