Failed Conditions
Pull Request — master (#328)
by Šimon
04:02
created

SchemaPrinter::printUnion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\Printer;
9
use GraphQL\Type\Definition\Directive;
10
use GraphQL\Type\Definition\EnumType;
11
use GraphQL\Type\Definition\InputObjectType;
12
use GraphQL\Type\Definition\InterfaceType;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\ScalarType;
15
use GraphQL\Type\Definition\Type;
16
use GraphQL\Type\Definition\UnionType;
17
use GraphQL\Type\Introspection;
18
use GraphQL\Type\Schema;
19
use function array_filter;
20
use function array_keys;
21
use function array_map;
22
use function array_merge;
23
use function array_values;
24
use function count;
25
use function explode;
26
use function implode;
27
use function ksort;
28
use function mb_strlen;
29
use function preg_match_all;
30
use function sprintf;
31
use function str_replace;
32
use function strlen;
33
use function substr;
34
35
/**
36
 * Given an instance of Schema, prints it in GraphQL type language.
37
 */
38
class SchemaPrinter
39
{
40
    /**
41
     * Accepts options as a second argument:
42
     *
43
     *    - commentDescriptions:
44
     *        Provide true to use preceding comments as the description.
45
     * @api
46
     * @param bool[] $options
47
     * @return string
48
     */
49 49
    public static function doPrint(Schema $schema, array $options = [])
50
    {
51 49
        return self::printFilteredSchema(
52 49
            $schema,
53
            function ($type) {
54 49
                return ! Directive::isSpecifiedDirective($type);
55 49
            },
56
            function ($type) {
57 49
                return ! Type::isBuiltInType($type);
58 49
            },
59 49
            $options
60
        );
61
    }
62
63
    /**
64
     * @param bool[] $options
65
     */
66 51
    private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options)
67
    {
68 51
        $directives = array_filter(
69 51
            $schema->getDirectives(),
70
            function ($directive) use ($directiveFilter) {
71 51
                return $directiveFilter($directive);
72 51
            }
73
        );
74
75 51
        $types = $schema->getTypeMap();
76 51
        ksort($types);
77 51
        $types = array_filter($types, $typeFilter);
78
79 51
        return sprintf(
80 51
            "%s\n",
81 51
            implode(
82 51
                "\n\n",
83 51
                array_filter(
84 51
                    array_merge(
85 51
                        [self::printSchemaDefinition($schema)],
86 51
                        array_map(
87
                            function ($directive) use ($options) {
88 7
                                return self::printDirective($directive, $options);
89 51
                            },
90 51
                            $directives
91
                        ),
92 51
                        array_map(
93
                            function ($type) use ($options) {
94 51
                                return self::printType($type, $options);
95 51
                            },
96 51
                            $types
97
                        )
98
                    )
99
                )
100
            )
101
        );
102
    }
103
104 51
    private static function printSchemaDefinition(Schema $schema)
105
    {
106 51
        if (self::isSchemaOfCommonNames($schema)) {
107 22
            return;
108
        }
109
110 29
        $operationTypes = [];
111
112 29
        $queryType = $schema->getQueryType();
113 29
        if ($queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
114 29
            $operationTypes[] = sprintf('  query: %s', $queryType->name);
115
        }
116
117 29
        $mutationType = $schema->getMutationType();
118 29
        if ($mutationType) {
0 ignored issues
show
introduced by
$mutationType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
119 1
            $operationTypes[] = sprintf('  mutation: %s', $mutationType->name);
120
        }
121
122 29
        $subscriptionType = $schema->getSubscriptionType();
123 29
        if ($subscriptionType) {
0 ignored issues
show
introduced by
$subscriptionType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
124 1
            $operationTypes[] = sprintf('  subscription: %s', $subscriptionType->name);
125
        }
126
127 29
        return sprintf("schema {\n%s\n}", implode("\n", $operationTypes));
128
    }
129
130
    /**
131
     * GraphQL schema define root types for each type of operation. These types are
132
     * the same as any other type and can be named in any manner, however there is
133
     * a common naming convention:
134
     *
135
     *   schema {
136
     *     query: Query
137
     *     mutation: Mutation
138
     *   }
139
     *
140
     * When using this naming convention, the schema description can be omitted.
141
     */
142 51
    private static function isSchemaOfCommonNames(Schema $schema)
143
    {
144 51
        $queryType = $schema->getQueryType();
145 51
        if ($queryType && $queryType->name !== 'Query') {
146 29
            return false;
147
        }
148
149 22
        $mutationType = $schema->getMutationType();
150 22
        if ($mutationType && $mutationType->name !== 'Mutation') {
151
            return false;
152
        }
153
154 22
        $subscriptionType = $schema->getSubscriptionType();
155 22
        if ($subscriptionType && $subscriptionType->name !== 'Subscription') {
156
            return false;
157
        }
158
159 22
        return true;
160
    }
161
162 7
    private static function printDirective($directive, $options)
163
    {
164 7
        return self::printDescription($options, $directive) .
165 7
            'directive @' . $directive->name . self::printArgs($options, $directive->args) .
166 7
            ' on ' . implode(' | ', $directive->locations);
167
    }
168
169 51
    private static function printDescription($options, $def, $indentation = '', $firstInBlock = true)
170
    {
171 51
        if (! $def->description) {
172 51
            return '';
173
        }
174 7
        $lines = self::descriptionLines($def->description, 120 - strlen($indentation));
175 7
        if (isset($options['commentDescriptions'])) {
176 2
            return self::printDescriptionWithComments($lines, $indentation, $firstInBlock);
177
        }
178
179 5
        $description = ($indentation && ! $firstInBlock)
180 2
            ? "\n" . $indentation . '"""'
181 5
            : $indentation . '"""';
182
183
        // In some circumstances, a single line can be used for the description.
184 5
        if (count($lines) === 1 &&
185 5
            mb_strlen($lines[0]) < 70 &&
186 5
            substr($lines[0], -1) !== '"'
187
        ) {
188 3
            return $description . self::escapeQuote($lines[0]) . "\"\"\"\n";
189
        }
190
191
        // Format a multi-line block quote to account for leading space.
192 3
        $hasLeadingSpace = isset($lines[0]) &&
193
            (
194 3
                substr($lines[0], 0, 1) === ' ' ||
195 3
                substr($lines[0], 0, 1) === '\t'
196
            );
197 3
        if (! $hasLeadingSpace) {
198 2
            $description .= "\n";
199
        }
200
201 3
        $lineLength = count($lines);
202 3
        for ($i = 0; $i < $lineLength; $i++) {
203 3
            if ($i !== 0 || ! $hasLeadingSpace) {
204 2
                $description .= $indentation;
205
            }
206 3
            $description .= self::escapeQuote($lines[$i]) . "\n";
207
        }
208 3
        $description .= $indentation . "\"\"\"\n";
209
210 3
        return $description;
211
    }
212
213 7
    private static function descriptionLines($description, $maxLen)
214
    {
215 7
        $lines    = [];
216 7
        $rawLines = explode("\n", $description);
217 7
        foreach ($rawLines as $line) {
218 7
            if ($line === '') {
219 2
                $lines[] = $line;
220
            } else {
221
                // For > 120 character long lines, cut at space boundaries into sublines
222
                // of ~80 chars.
223 7
                $sublines = self::breakLine($line, $maxLen);
224 7
                foreach ($sublines as $subline) {
225 7
                    $lines[] = $subline;
226
                }
227
            }
228
        }
229
230 7
        return $lines;
231
    }
232
233 7
    private static function breakLine($line, $maxLen)
234
    {
235 7
        if (strlen($line) < $maxLen + 5) {
236 7
            return [$line];
237
        }
238 2
        preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts);
239 2
        $parts = $parts[0];
240
241 2
        return array_map('trim', $parts);
242
    }
243
244 2
    private static function printDescriptionWithComments($lines, $indentation, $firstInBlock)
245
    {
246 2
        $description = $indentation && ! $firstInBlock ? "\n" : '';
247 2
        foreach ($lines as $line) {
248 2
            if ($line === '') {
249 1
                $description .= $indentation . "#\n";
250
            } else {
251 2
                $description .= $indentation . '# ' . $line . "\n";
252
            }
253
        }
254
255 2
        return $description;
256
    }
257
258 5
    private static function escapeQuote($line)
259
    {
260 5
        return str_replace('"""', '\\"""', $line);
261
    }
262
263 51
    private static function printArgs($options, $args, $indentation = '')
264
    {
265 51
        if (! $args) {
266 37
            return '';
267
        }
268
269
        // If every arg does not have a description, print them on one line.
270 22
        if (Utils::every(
271 22
            $args,
272
            function ($arg) {
273 22
                return empty($arg->description);
274 22
            }
275
        )) {
276 20
            return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
277
        }
278
279 4
        return sprintf(
280 4
            "(\n%s\n%s)",
281 4
            implode(
282 4
                "\n",
283 4
                array_map(
284
                    function ($arg, $i) use ($indentation, $options) {
285 4
                        return self::printDescription($options, $arg, '  ' . $indentation, ! $i) . '  ' . $indentation .
286 4
                            self::printInputValue($arg);
287 4
                    },
288 4
                    $args,
289 4
                    array_keys($args)
290
                )
291
            ),
292 4
            $indentation
293
        );
294
    }
295
296 22
    private static function printInputValue($arg)
297
    {
298 22
        $argDecl = $arg->name . ': ' . (string) $arg->getType();
299 22
        if ($arg->defaultValueExists()) {
300 9
            $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType()));
301
        }
302
303 22
        return $argDecl;
304
    }
305
306
    /**
307
     * @param bool[] $options
308
     */
309 51
    public static function printType(Type $type, array $options = [])
310
    {
311 51
        if ($type instanceof ScalarType) {
312 3
            return self::printScalar($type, $options);
313
        }
314
315 51
        if ($type instanceof ObjectType) {
316 51
            return self::printObject($type, $options);
317
        }
318
319 19
        if ($type instanceof InterfaceType) {
320 5
            return self::printInterface($type, $options);
321
        }
322
323 15
        if ($type instanceof UnionType) {
324 5
            return self::printUnion($type, $options);
325
        }
326
327 11
        if ($type instanceof EnumType) {
328 9
            return self::printEnum($type, $options);
329
        }
330
331 3
        if ($type instanceof InputObjectType) {
332 3
            return self::printInputObject($type, $options);
333
        }
334
335
        throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type)));
336
    }
337
338
    /**
339
     * @param bool[] $options
340
     */
341 3
    private static function printScalar(ScalarType $type, array $options)
342
    {
343 3
        return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name);
344
    }
345
346
    /**
347
     * @param bool[] $options
348
     */
349 51
    private static function printObject(ObjectType $type, array $options)
350
    {
351 51
        $interfaces            = $type->getInterfaces();
352 51
        $implementedInterfaces = ! empty($interfaces) ?
353 5
            ' implements ' . implode(
354 5
                ', ',
355 5
                array_map(
356
                    function ($i) {
357 5
                        return $i->name;
358 5
                    },
359 5
                    $interfaces
360
                )
361 51
            ) : '';
362
363 51
        return self::printDescription($options, $type) .
364 51
            sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type));
365
    }
366
367
    /**
368
     * @param bool[] $options
369
     */
370 51
    private static function printFields($options, $type)
371
    {
372 51
        $fields = array_values($type->getFields());
373
374 51
        return implode(
375 51
            "\n",
376 51
            array_map(
377
                function ($f, $i) use ($options) {
378 51
                    return self::printDescription($options, $f, '  ', ! $i) . '  ' .
379 51
                        $f->name . self::printArgs($options, $f->args, '  ') . ': ' .
380 51
                        (string) $f->getType() . self::printDeprecated($f);
381 51
                },
382 51
                $fields,
383 51
                array_keys($fields)
384
            )
385
        );
386
    }
387
388 51
    private static function printDeprecated($fieldOrEnumVal)
389
    {
390 51
        $reason = $fieldOrEnumVal->deprecationReason;
391 51
        if (empty($reason)) {
392 51
            return '';
393
        }
394 3
        if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
395 1
            return ' @deprecated';
396
        }
397
398
        return ' @deprecated(reason: ' .
399 3
            Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
400
    }
401
402
    /**
403
     * @param bool[] $options
404
     */
405 5
    private static function printInterface(InterfaceType $type, array $options)
406
    {
407 5
        return self::printDescription($options, $type) .
408 5
            sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type));
409
    }
410
411
    /**
412
     * @param bool[] $options
413
     */
414 5
    private static function printUnion(UnionType $type, array $options)
415
    {
416 5
        return self::printDescription($options, $type) .
417 5
            sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes()));
418
    }
419
420
    /**
421
     * @param bool[] $options
422
     */
423 9
    private static function printEnum(EnumType $type, array $options)
424
    {
425 9
        return self::printDescription($options, $type) .
426 9
            sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options));
427
    }
428
429
    /**
430
     * @param bool[] $options
431
     */
432 9
    private static function printEnumValues($values, $options)
433
    {
434 9
        return implode(
435 9
            "\n",
436 9
            array_map(
437
                function ($value, $i) use ($options) {
438 9
                    return self::printDescription($options, $value, '  ', ! $i) . '  ' .
439 9
                        $value->name . self::printDeprecated($value);
440 9
                },
441 9
                $values,
442 9
                array_keys($values)
443
            )
444
        );
445
    }
446
447
    /**
448
     * @param bool[] $options
449
     */
450 3
    private static function printInputObject(InputObjectType $type, array $options)
451
    {
452 3
        $fields = array_values($type->getFields());
453
454 3
        return self::printDescription($options, $type) .
455 3
            sprintf(
456 3
                "input %s {\n%s\n}",
457 3
                $type->name,
458 3
                implode(
459 3
                    "\n",
460 3
                    array_map(
461
                        function ($f, $i) use ($options) {
462 3
                            return self::printDescription($options, $f, '  ', ! $i) . '  ' . self::printInputValue($f);
463 3
                        },
464 3
                        $fields,
465 3
                        array_keys($fields)
466
                    )
467
                )
468
            );
469
    }
470
471
    /**
472
     * @api
473
     * @param bool[] $options
474
     * @return string
475
     */
476 2
    public static function printIntrosepctionSchema(Schema $schema, array $options = [])
477
    {
478 2
        return self::printFilteredSchema(
479 2
            $schema,
480 2
            [Directive::class, 'isSpecifiedDirective'],
481 2
            [Introspection::class, 'isIntrospectionType'],
482 2
            $options
483
        );
484
    }
485
}
486