Passed
Push — master ( 8435c3...5821ca )
by Vladimir
10:05 queued 11s
created

SchemaPrinter::isSchemaOfCommonNames()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.0493

Importance

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