Passed
Pull Request — master (#52)
by Viacheslav
10:42
created

TypeBuilder::renderTypeDef()   F

Complexity

Conditions 18
Paths 384

Size

Total Lines 111
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 69
c 1
b 0
f 0
nc 384
nop 3
dl 0
loc 111
rs 1.7333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Swaggest\PhpCodeBuilder\Markdown;
4
5
use Swaggest\CodeBuilder\TableRenderer;
6
use Swaggest\JsonSchema\Schema;
7
use Swaggest\PhpCodeBuilder\PhpCode;
8
9
class TypeBuilder
10
{
11
    const EXAMPLES = 'examples';
12
    const EXAMPLE = 'example';
13
14
    /** @var \SplObjectStorage */
15
    private $processed;
16
17
    public $trimNamePrefix = [
18
        '#/definitions'
19
    ];
20
21
    public $addNamePrefix = '';
22
23
    /**
24
     * Map of type name to type doc.
25
     * @var array<string,string>
26
     */
27
    public $types = [];
28
29
    public $uniqueTypeSchemas = [];
30
31
    public $file = '';
32
33
    public function __construct()
34
    {
35
        $this->processed = new \SplObjectStorage();
36
    }
37
38
39
    /**
40
     * @param Schema|boolean|null $schema
41
     * @param string $path
42
     * @return string
43
     */
44
    public function getTypeString($schema, $path = '')
45
    {
46
        if ($schema === null) {
47
            return '';
48
        }
49
50
        $schema = Schema::unboolSchema($schema);
51
52
        $isOptional = false;
53
        $isObject = false;
54
        $isArray = false;
55
        $isBoolean = false;
56
        $isString = false;
57
        $isNumber = false;
58
59
        if ($schema->const !== null) {
60
            return '`' . var_export($schema->const, true) . '`';
61
        }
62
63
        if (!empty($schema->enum)) {
64
            $res = '';
65
            foreach ($schema->enum as $value) {
66
                $res .= '<br>`' . var_export($value, true) . '`, ';
67
            }
68
            return substr($res, 4, -2);
69
        }
70
71
        if (!empty($schema->getFromRefs())) {
72
            $refs = $schema->getFromRefs();
73
            $path = $refs[0];
74
        }
75
76
        $type = $schema->type;
77
        if ($type === null) {
0 ignored issues
show
introduced by
The condition $type === null is always false.
Loading history...
78
            $type = [];
79
80
            if (!empty($schema->properties) || !empty($schema->additionalProperties) || !empty($schema->patternProperties)) {
81
                $type[] = Schema::OBJECT;
82
            }
83
84
            if (!empty($schema->items) || !empty($schema->additionalItems)) {
85
                $type[] = Schema::_ARRAY;
86
            }
87
        }
88
89
        if (!is_array($type)) {
90
            $type = [$type];
91
        }
92
93
        $or = [];
94
95
        if ($schema->oneOf !== null) {
96
            foreach ($schema->oneOf as $i => $item) {
97
                $or[] = $this->getTypeString($item, $path . '/oneOf/' . $i);
98
            }
99
        }
100
101
        if ($schema->anyOf !== null) {
102
            foreach ($schema->anyOf as $i => $item) {
103
                $or[] = $this->getTypeString($item, $path . '/anyOf/' . $i);
104
            }
105
        }
106
107
        if ($schema->allOf !== null) {
108
            foreach ($schema->allOf as $i => $item) {
109
                $or[] = $this->getTypeString($item, $path . '/allOf/' . $i);
110
            }
111
        }
112
113
        if ($schema->then !== null) {
114
            $or[] = $this->getTypeString($schema->then, $path . '/then');
115
        }
116
117
        if ($schema->else !== null) {
118
            $or[] = $this->getTypeString($schema->else, $path . '/else');
119
        }
120
121
        foreach ($type as $i => $t) {
122
            switch ($t) {
123
                case Schema::NULL:
124
                    $isOptional = true;
125
                    break;
126
127
                case Schema::OBJECT:
128
                    $isObject = true;
129
                    break;
130
131
                case Schema::_ARRAY:
132
                    $isArray = true;
133
                    break;
134
135
                case Schema::NUMBER:
136
                case Schema::INTEGER:
137
                    $isNumber = true;
138
                    break;
139
140
                case Schema::STRING:
141
                    $isString = true;
142
                    break;
143
144
                case Schema::BOOLEAN:
145
                    $isBoolean = true;
146
                    break;
147
148
            }
149
        }
150
151
152
        $namedTypeAdded = false;
153
        if (!empty($schema->properties) || $this->hasConstraints($schema)) {
154
            if ($this->processed->contains($schema)) {
155
                $or [] = $this->processed->offsetGet($schema);
156
                $namedTypeAdded = true;
157
            } else {
158
                if ($schema instanceof Schema) {
159
                    $typeName = $this->typeName($schema, $path);
160
                    $this->makeTypeDef($schema, $path);
161
162
                    $or [] = $typeName;
163
                    $namedTypeAdded = true;
164
                }
165
            }
166
        }
167
168
        if ($isObject) {
169
            $typeAdded = false;
170
171
            if ($namedTypeAdded) {
172
                $typeAdded = true;
173
            }
174
175
            if ($schema->additionalProperties instanceof Schema) {
176
                $typeName = $this->getTypeString($schema->additionalProperties, $path . '/additionalProperties');
177
                $or [] = "`Map<String,`$typeName`>`";
178
                $typeAdded = true;
179
            }
180
181
            if (!empty($schema->patternProperties)) {
182
                foreach ($schema->patternProperties as $pattern => $propertySchema) {
183
                    if ($propertySchema instanceof Schema) {
184
                        $typeName = $this->getTypeString($propertySchema, $path . '/patternProperties/' . $pattern);
185
                        $or [] = $typeName;
186
                        $typeAdded = true;
187
                    }
188
                }
189
            }
190
191
            if (!$typeAdded) {
192
                $or [] = '`Object`';
193
            }
194
        }
195
196
        if ($isArray) {
197
            $typeAdded = false;
198
199
            if ($schema->items instanceof Schema) {
200
                $typeName = $this->getTypeString($schema->items, $path . '/items');
201
                $or [] = "`Array<`$typeName`>`";
202
                $typeAdded = true;
203
            }
204
205
            if ($schema->additionalItems instanceof Schema) {
206
                $typeName = $this->getTypeString($schema->additionalItems, $path . '/additionalItems');
207
                $or [] = "`Array<`$typeName`>`";
208
                $typeAdded = true;
209
            }
210
211
            if (!$typeAdded) {
212
                $or [] = '`Array`';
213
            }
214
        }
215
216
        if ($isOptional) {
217
            $or [] = '`null`';
218
        }
219
220
        if ($isString) {
221
            $or [] = '`String`';
222
        }
223
224
        if ($isNumber) {
225
            $or [] = '`Number`';
226
        }
227
228
        if ($isBoolean) {
229
            $or [] = '`Boolean`';
230
        }
231
232
        if ($schema->format !== null) {
233
            $or [] = 'Format: `' . $schema->format . '`';
234
        }
235
236
        $res = '';
237
        foreach ($or as $item) {
238
            if (!empty($item) && $item !== '*') {
239
                $res .= ', ' . $item;
240
            }
241
        }
242
243
        if ($res !== '') {
244
            $res = substr($res, 2);
245
        } else {
246
            $res = '`*`';
247
        }
248
249
        $res = str_replace('``', '', $res);
250
251
        return $res;
252
    }
253
254
    private function typeName(Schema $schema, $path, $raw = false)
255
    {
256
        if ($fromRefs = $schema->getFromRefs()) {
257
            $path = $fromRefs[count($fromRefs) - 1];
258
        }
259
260
        foreach ($this->trimNamePrefix as $prefix) {
261
            if ($prefix === substr($path, 0, strlen($prefix))) {
262
                $path = substr($path, strlen($prefix));
263
            }
264
        }
265
266
        if (($path === '#' || empty($path)) && !empty($schema->title)) {
267
            $path = $schema->title;
268
        }
269
270
        $name = PhpCode::makePhpName($this->addNamePrefix . '_' . $path, false);
271
272
        if ($raw) {
273
            return $name;
274
        }
275
276
        return '[`' . $name . '`](#' . strtolower($name) . ')';
277
    }
278
279
    private static function constraints()
280
    {
281
        static $constraints;
282
283
        if ($constraints === null) {
284
            $names = Schema::names();
285
            $constraints = [
286
                $names->multipleOf,
287
                $names->maximum,
288
                $names->exclusiveMaximum,
289
                $names->minimum,
290
                $names->exclusiveMinimum,
291
                $names->maxLength,
292
                $names->minLength,
293
                $names->pattern,
294
                $names->maxItems,
295
                $names->minItems,
296
                $names->uniqueItems,
297
                $names->maxProperties,
298
                $names->minProperties,
299
            ];
300
        }
301
302
        return $constraints;
303
    }
304
305
    /**
306
     * @param Schema $schema
307
     */
308
    private function hasConstraints($schema)
309
    {
310
        foreach (self::constraints() as $name) {
311
            if ($schema->$name !== null) {
312
                return true;
313
            }
314
        }
315
316
        return false;
317
    }
318
319
    public function renderTypeDef(Schema $schema, $typeName, $path)
320
    {
321
        $head = '';
322
        if (!empty($schema->title) && $schema->title != $typeName) {
323
            $head .= $schema->title . "\n";
324
        }
325
326
        if (!empty($schema->description)) {
327
            $head .= $schema->description . "\n";
328
        }
329
330
        $examples = [];
331
        if (!empty($schema->{self::EXAMPLES})) {
332
            $examples = $schema->{self::EXAMPLES};
333
        }
334
335
        if (!empty($schema->{self::EXAMPLE})) {
336
            $examples[] = $schema->{self::EXAMPLE};
337
        }
338
339
        if (!empty($examples)) {
340
            $head .= "Example:\n\n";
341
            foreach ($examples as $example) {
342
                $head .= <<<MD
343
```json
344
$example
345
```
346
347
348
MD;
349
350
            }
351
        }
352
353
        $tnl = strtolower($typeName);
354
355
        $res = <<<MD
356
357
358
### <a id="$tnl"></a>$typeName
359
$head
360
361
MD;
362
363
364
        $rows = [];
365
        foreach (self::constraints() as $name) {
366
            if ($schema->$name !== null) {
367
                $value = $schema->$name;
368
369
                if ($value instanceof Schema) {
370
                    $value = $this->typeName($value, $path . '/' . $name);
371
                }
372
373
                $rows [] = [
374
                    'Constraint' => $name,
375
                    'Value' => $value,
376
                ];
377
            }
378
        }
379
        $res .= TableRenderer::create(new \ArrayIterator($rows))
380
            ->stripEmptyColumns()
381
            ->setColDelimiter('|')
382
            ->setHeadRowDelimiter('-')
383
            ->setOutlineVertical(true)
384
            ->multilineCellDelimiter('<br>')
385
            ->setShowHeader();
386
387
        $res .= "\n\n";
388
389
        $rows = [];
390
        $hasDescription = false;
391
        if (!empty($schema->properties)) {
392
            foreach ($schema->properties as $propertyName => $propertySchema) {
393
                $typeString = $this->getTypeString($propertySchema, $path . '/' . $propertyName);
394
                $desc = $this->description($propertySchema);
395
                if (!empty($desc)) {
396
                    $hasDescription = true;
397
                }
398
                $isRequired = false;
399
                if (!empty($schema->required)) {
400
                    $isRequired = in_array($propertyName, $schema->required);
401
                }
402
                $rows [] = array(
403
                    'Property' => '`' . $propertyName . '`' . ($isRequired ? ' (required)' : ''),
404
                    'Type' => $typeString,
405
                    'Description' => $desc,
406
                );
407
            }
408
409
            if (!$hasDescription) {
410
                foreach ($rows as &$row) {
411
                    unset($row['Description']);
412
                }
413
            }
414
415
            $res .= TableRenderer::create(new \ArrayIterator($rows))
416
                ->stripEmptyColumns()
417
                ->setColDelimiter('|')
418
                ->setHeadRowDelimiter('-')
419
                ->setOutlineVertical(true)
420
                ->multilineCellDelimiter('<br>')
421
                ->setShowHeader();
422
423
        }
424
425
        $res .= <<<MD
426
427
MD;
428
429
        return $res;
430
    }
431
432
    private function makeTypeDef(Schema $schema, $path)
433
    {
434
        $tn = $this->typeName($schema, $path, true);
435
        $typeName = $this->typeName($schema, $path);
436
        $this->processed->attach($schema, $typeName);
437
438
        $res = $this->renderTypeDef($schema, $tn, $path);
439
440
        if (isset($this->uniqueTypeSchemas[$res])) {
441
            return $this->uniqueTypeSchemas[$res];
442
        }
443
444
        $this->types[$typeName] = $res;
445
        $this->uniqueTypeSchemas[$res] = $typeName;
446
        $this->file .= $res;
447
448
        return $typeName;
449
    }
450
451
    public function sortTypes()
452
    {
453
        ksort($this->types);
454
    }
455
456
    public function tableOfContents()
457
    {
458
        if (count($this->types) === 0) {
459
            return '';
460
        }
461
462
        $res = '# Types' . "\n\n";
463
464
        foreach ($this->types as $name => $doc) {
465
            $res .= '  * ' . $name . "\n";
466
        }
467
468
        $res .= "\n\n";
469
470
        return $res;
471
    }
472
473
    private function description(Schema $schema)
474
    {
475
        $res = str_replace("\n", " ", $schema->title . $schema->description);
476
        if ($res) {
477
            return rtrim($res, '.') . '.';
478
        }
479
480
        return '';
481
    }
482
}