Passed
Push — master ( 6239d4...3c4b08 )
by Viacheslav
15:37 queued 05:40
created

TypeBuilder::tableOfContents()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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