Passed
Pull Request — master (#57)
by Viacheslav
11:07
created

TypeBuilder::trim()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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