Completed
Push — master ( 8ccd33...ec0b9e )
by Viacheslav
12:44 queued 02:47
created

TypeBuilder::constraints()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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