Completed
Branch v2 (f7f89f)
by Pieter
04:28
created

OpenApiSpecGenerator::convertAllToPathItem()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 73
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 49
c 0
b 0
f 0
nc 10
nop 2
dl 0
loc 73
rs 8.4905

How to fix   Long Method   

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 W2w\Lib\Apie\OpenApiSchema;
4
5
use W2w\Lib\Apie\ApiResourceMetadataFactory;
6
use W2w\Lib\Apie\Resources\ApiResourcesInterface;
7
use W2w\Lib\Apie\ClassResourceConverter;
8
use erasys\OpenApi\Spec\v3 as OASv3;
9
use W2w\Lib\Apie\Retrievers\SearchFilterProviderInterface;
10
use W2w\Lib\Apie\SearchFilters\SearchFilter;
11
use W2w\Lib\Apie\ValueObjects\PhpPrimitive;
12
13
/**
14
 * Class that generated an OpenAPI spec from a list of API resources.
15
 */
16
class OpenApiSpecGenerator
17
{
18
    private $apiResources;
19
20
    private $converter;
21
22
    private $info;
23
24
    private $schemaGenerator;
25
26
    private $apiResourceMetadataFactory;
27
28
    private $baseUrl;
29
30
    public function __construct(
31
        ApiResourcesInterface $apiResources,
32
        ClassResourceConverter $converter,
33
        OASv3\Info $info,
34
        SchemaGenerator $schemaGenerator,
35
        ApiResourceMetadataFactory $apiResourceMetadataFactory,
36
        string $baseUrl
37
    ) {
38
        $this->apiResources = $apiResources;
39
        $this->converter = $converter;
40
        $this->info = $info;
41
        $this->schemaGenerator = $schemaGenerator;
42
        $this->apiResourceMetadataFactory = $apiResourceMetadataFactory;
43
        $this->baseUrl = $baseUrl;
44
    }
45
46
    /**
47
     * Gets an OpenAPI spec document.
48
     *
49
     * @return OASv3\Document
50
     */
51
    public function getOpenApiSpec(): OASv3\Document
52
    {
53
        $paths = [];
54
        foreach ($this->apiResources->getApiResources() as $apiResourceClass) {
55
            $path = $this->converter->normalize($apiResourceClass);
56
            $paths['/' . $path] = $this->convertAllToPathItem($apiResourceClass, $path);
57
            $paths['/' . $path . '/{id}'] = $this->convertToPathItem($apiResourceClass, $path);
58
        }
59
60
        $stringSchema = new OASv3\Schema(['type' => 'string']);
61
        $stringOrIntSchema = new OASv3\Schema(['oneOf' => [$stringSchema, new OASv3\Schema(['type' => 'integer'])]]);
62
63
        $errorSchema = new OASv3\Reference('#/components/schemas/Error');
64
65
        $doc = new OASv3\Document(
66
            $this->info,
67
            $paths,
68
            '3.0.1',
69
            [
70
                'servers' => [
71
                    new OASv3\Server($this->baseUrl),
72
                ],
73
                'components' => new OASv3\Components([
74
                    'schemas' => [
75
                        'Error' => new OASv3\Schema([
76
                            'type'       => 'object',
77
                            'properties' => [
78
                                'type'    => $stringSchema,
79
                                'message' => $stringSchema,
80
                                'code'    => $stringOrIntSchema,
81
                                'trace'   => $stringSchema,
82
                            ],
83
                            'xml' => new OASv3\Xml(['name' => 'response']),
84
                        ]),
85
                    ],
86
                    'headers' => [
87
                        'x-ratelimit-limit' => new Oasv3\Header(
88
                            'Request limit per hour',
89
                            [
90
                                'example' => 100,
91
                                'schema'  => new OASv3\Schema([
92
                                    'type' => 'integer',
93
                                ]),
94
                            ]
95
                        ),
96
                        'x-ratelimit-remaining' => new Oasv3\Header(
97
                            'Request limit per hour',
98
                            [
99
                                'example' => 94,
100
                                'schema'  => new OASv3\Schema([
101
                                    'type' => 'integer',
102
                                ]),
103
                            ]
104
                        ),
105
                    ],
106
                    'responses' => [
107
                        'InvalidFormat' => new OASv3\Response(
108
                            'The body input could not be parsed',
109
                            [
110
                                'application/json' => new OASv3\MediaType(
111
                                    [
112
                                        'schema' => $errorSchema,
113
                                    ]
114
                                ),
115
                                'application/xml' => new OASv3\MediaType(
116
                                    [
117
                                        'schema' => $errorSchema,
118
                                    ]
119
                                ),
120
                            ]
121
                        ),
122
                        'ValidationError' => new OASv3\Response(
123
                            'The body input was in a proper format, but the input values were not valid',
124
                            [
125
                                'application/json' => new OASv3\MediaType(
126
                                    [
127
                                        'schema' => $errorSchema,
128
                                    ]
129
                                ),
130
                                'application/xml' => new OASv3\MediaType(
131
                                    [
132
                                        'schema' => $errorSchema,
133
                                    ]
134
                                ),
135
                            ]
136
                        ),
137
                        'TooManyRequests' => new OASv3\Response(
138
                            'Too many requests per seconds were sent',
139
                            [
140
                                'application/json' => new OASv3\MediaType(
141
                                    [
142
                                        'schema' => $errorSchema,
143
                                    ]
144
                                ),
145
                                'application/xml' => new OASv3\MediaType(
146
                                    [
147
                                        'schema' => $errorSchema,
148
                                    ]
149
                                ),
150
                            ]
151
                        ),
152
                        'MaintenanceMode' => new OASv3\Response(
153
                            'App is in maintenance mode',
154
                            [
155
                                'application/json' => new OASv3\MediaType(
156
                                    [
157
                                        'schema' => $errorSchema,
158
                                    ]
159
                                ),
160
                                'application/xml' => new OASv3\MediaType(
161
                                    [
162
                                        'schema' => $errorSchema,
163
                                    ]
164
                                ),
165
                            ]
166
                        ),
167
                        'NotFound' => new OASv3\Response(
168
                            'Response when resource could not be found',
169
                            [
170
                                'application/json' => new OASv3\MediaType(
171
                                    [
172
                                       'schema' => $errorSchema,
173
                                    ]
174
                                ),
175
                                'application/xml' => new OASv3\MediaType(
176
                                    [
177
                                       'schema' => $errorSchema,
178
                                    ]
179
                                ),
180
                            ]
181
                        ),
182
                        'NotAuthorized' => new OASv3\Response(
183
                            'You have no permission to do this call',
184
                            [
185
                                'application/json' => new OASv3\MediaType(
186
                                    [
187
                                        'schema' => $errorSchema,
188
                                    ]
189
                                ),
190
                                'application/xml' => new OASv3\MediaType(
191
                                    [
192
                                        'schema' => $errorSchema,
193
                                    ]
194
                                ),
195
                            ]
196
                        ),
197
                        'InternalError' => new OASv3\Response(
198
                            'An internal error occured',
199
                            [
200
                                'application/json' => new OASv3\MediaType(
201
                                    [
202
                                        'schema' => $errorSchema,
203
                                    ]
204
                                ),
205
                                'application/xml' => new OASv3\MediaType(
206
                                    [
207
                                        'schema' => $errorSchema,
208
                                    ]
209
                                ),
210
                            ]
211
                        ),
212
                        'ServerDependencyError' => new OASv3\Response(
213
                            'The server required an external response which threw an error',
214
                            [
215
                                'application/json' => new OASv3\MediaType(
216
                                    [
217
                                        'schema' => $errorSchema,
218
                                    ]
219
                                ),
220
                                'application/xml' => new OASv3\MediaType(
221
                                    [
222
                                        'schema' => $errorSchema,
223
                                    ]
224
                                ),
225
                            ]
226
                        ),
227
                    ],
228
                ]),
229
            ]
230
        );
231
232
        return $doc;
233
    }
234
235
    /**
236
     * Returns the default HTTP headers we generated for every REST api call.
237
     *
238
     * @return array
239
     */
240
    private function getDefaultHeaders(): array
241
    {
242
        return [
243
            'x-ratelimit-limit'     => new OASv3\Reference('#/components/headers/x-ratelimit-limit'),
244
            'x-ratelimit-remaining' => new OASv3\Reference('#/components/headers/x-ratelimit-remaining'),
245
        ];
246
    }
247
248
    /**
249
     * Returns the content OpenAPI spec for a resource class and a certain operation.
250
     *
251
     * @param string $apiResourceClass
252
     * @param string $operation
253
     * @return OASv3\MediaType[]
254
     */
255
    private function convertToContent(string $apiResourceClass, string $operation): array
256
    {
257
        $readWrite = $this->determineReadWrite($operation);
258
        $jsonSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
259
        $xmlSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
260
        $xmlSchema->xml = new OASv3\Xml(['name' => 'item']);
261
262
        return [
263
            'application/json' => new OASv3\MediaType(
264
                [
265
                    'schema' => $jsonSchema,
266
                ]
267
            ),
268
            'application/xml' => new OASv3\MediaType(
269
                [
270
                    'schema' => $xmlSchema,
271
                ]
272
            ),
273
        ];
274
    }
275
276
    /**
277
     * Returns the content OpenAPI spec for a resource class when it returns an array of resources.
278
     *
279
     * @param string $apiResourceClass
280
     * @param string $operation
281
     * @return OASv3\MediaType[]
282
     */
283
    private function convertToContentArray(string $apiResourceClass, string $operation): array
284
    {
285
        $readWrite = $this->determineReadWrite($operation);
286
        $jsonSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
287
        $xmlSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
288
        $xmlSchema->xml = new OASv3\Xml(['name' => 'item']);
289
290
        return [
291
            'application/json' => new OASv3\MediaType(
292
                [
293
                    'schema' => new OASv3\Schema([
294
                        'type'  => 'array',
295
                        'items' => $jsonSchema,
296
                    ]),
297
                ]
298
            ),
299
            'application/xml' => new OASv3\MediaType(
300
                [
301
                    'schema' => new OASv3\Schema([
302
                        'type'  => 'array',
303
                        'items' => $xmlSchema,
304
                        'xml'   => new OASv3\Xml(['name' => 'response']),
305
                    ]),
306
                ]
307
            ),
308
        ];
309
    }
310
311
    /**
312
     * Determine if the operation is a read or a write.
313
     *
314
     * @param string $operation
315
     * @return string
316
     */
317
    private function determineReadWrite(string $operation): string
318
    {
319
        if ($operation === 'post' || $operation === 'put') {
320
            return 'write';
321
        }
322
323
        return 'read';
324
    }
325
326
    /**
327
     * Returns all paths of an api resource without an id in the url.
328
     *
329
     * @param string $apiResourceClass
330
     * @param string $resourceName
331
     * @return OASv3\PathItem
332
     */
333
    private function convertAllToPathItem(string $apiResourceClass, string $resourceName): OASv3\PathItem
334
    {
335
        $paths = [];
336
337
        if ($this->allowed($apiResourceClass, 'all')) {
338
            $paths['get'] = new OASv3\Operation(
339
                [
340
                    '200' => new OASv3\Response(
341
                        'Retrieves all instances of ' . $resourceName,
342
                        $this->convertToContentArray($apiResourceClass, 'get'),
343
                        $this->getDefaultHeaders()
344
                    ),
345
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
346
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
347
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
348
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
349
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
350
                ],
351
                null,
352
                null,
353
                [
354
                    'tags'       => [$resourceName],
355
                    'parameters' => [
356
                        new OASv3\Parameter('page', 'query', 'pagination index counting from 0', ['schema' => new OASv3\Schema(['type' => 'integer', 'minimum' => 0])]),
357
                        new OASv3\Parameter('limit', 'query', 'number of results', ['schema' => new OASv3\Schema(['type' => 'integer', 'minimum' => 1])]),
358
                    ],
359
                ]
360
            );
361
            $metadata = $this->apiResourceMetadataFactory->getMetadata($apiResourceClass);
362
            $retriever = $metadata->hasResourceRetriever() ? $metadata->getResourceRetriever() : null;
363
            if ($retriever instanceof SearchFilterProviderInterface) {
364
                foreach ($retriever->getSearchFilter($metadata)->getAllPrimitiveSearchFilter() as $name => $filter) {
365
                    $schema = $this->filter->getSchemaForFilter();
0 ignored issues
show
Bug Best Practice introduced by
The property filter does not exist on W2w\Lib\Apie\OpenApiSchema\OpenApiSpecGenerator. Did you maybe forget to declare it?
Loading history...
366
                    $paths['get']->parameters[] = new OASv3\Parameter(
367
                        $name,
368
                        'query',
369
                        'search filter ' . $name,
370
                        ['schema' => $schema]
371
                    );
372
                }
373
            }
374
        }
375
376
        if ($this->allowed($apiResourceClass, 'post')) {
377
            $paths['post'] = new OASv3\Operation(
378
                [
379
                    '200' => new OASv3\Response(
380
                        'Creates a new instance of ' . $resourceName,
381
                        $this->convertToContent($apiResourceClass, 'get'),
382
                        $this->getDefaultHeaders()
383
                    ),
384
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
385
                    '415' => new OASv3\Reference('#/components/responses/InvalidFormat'),
386
                    '424' => new OASv3\Reference('#/components/responses/ValidationError'),
387
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
388
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
389
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
390
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
391
                ],
392
                null,
393
                null,
394
                [
395
                    'tags'        => [$resourceName],
396
                    'requestBody' => new OASv3\RequestBody(
397
                        $this->convertToContent($apiResourceClass, 'post'),
398
                        'the resource as JSON to persist',
399
                        true
400
                    ),
401
                ]
402
            );
403
        }
404
405
        return new OASv3\PathItem($paths);
406
    }
407
408
    /**
409
     * Returns all paths of an api resource with an id in the url.
410
     * @param string $apiResourceClass
411
     * @param string $resourceName
412
     * @return OASv3\PathItem
413
     */
414
    private function convertToPathItem(string $apiResourceClass, string $resourceName): OASv3\PathItem
415
    {
416
        $paths = [
417
            'parameters' => [
418
                new OASv3\Parameter('id', 'path', 'the id of the resource', ['required' => true, 'schema' => new OASv3\Schema(['type' => 'string'])]),
419
            ],
420
        ];
421
        if ($this->allowed($apiResourceClass, 'get')) {
422
            $paths['get'] = new OASv3\Operation(
423
                [
424
                    '200' => new OASv3\Response(
425
                        'Retrieves a single instance of ' . $resourceName,
426
                        $this->convertToContent($apiResourceClass, 'get'),
427
                        $this->getDefaultHeaders()
428
                    ),
429
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
430
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
431
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
432
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
433
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
434
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
435
                ],
436
                null,
437
                null,
438
                [
439
                    'tags' => [$resourceName],
440
                ]
441
            );
442
        }
443
        if ($this->allowed($apiResourceClass, 'delete')) {
444
            $paths['delete'] = new OASv3\Operation(
445
                [
446
                    '204' => new OASv3\Response(
447
                        'Deletes a single instance of ' . $resourceName,
448
                        null,
449
                        $this->getDefaultHeaders()
450
                    ),
451
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
452
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
453
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
454
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
455
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
456
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
457
                ],
458
                null,
459
                null,
460
                [
461
                    'tags' => [$resourceName],
462
                ]
463
            );
464
        }
465
        if ($this->allowed($apiResourceClass, 'put')) {
466
            $paths['put'] = new OASv3\Operation(
467
                [
468
                    '200' => new OASv3\Response(
469
                        'Retrieves and update a single instance of ' . $resourceName,
470
                        $this->convertToContent($apiResourceClass, 'get'),
471
                        $this->getDefaultHeaders()
472
                    ),
473
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
474
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
475
                    '415' => new OASv3\Reference('#/components/responses/InvalidFormat'),
476
                    '424' => new OASv3\Reference('#/components/responses/ValidationError'),
477
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
478
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
479
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
480
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
481
                ],
482
                null,
483
                null,
484
                [
485
                    'tags'        => [$resourceName],
486
                    'requestBody' => new OASv3\RequestBody(
487
                        $this->convertToContent($apiResourceClass, 'put'),
488
                        'the resource as JSON to persist',
489
                        true
490
                    ),
491
                ]
492
            );
493
        }
494
495
        return new OASv3\PathItem($paths);
496
    }
497
498
    /**
499
     * Returns if a specific REST API call is an allowed method.
500
     *
501
     * @param string $apiResourceClass
502
     * @param string $operation
503
     * @return bool
504
     */
505
    private function allowed(string $apiResourceClass, string $operation): bool
506
    {
507
        $metadata = $this->apiResourceMetadataFactory->getMetadata($apiResourceClass);
508
        switch ($operation) {
509
            case 'all':
510
                return $metadata->allowGetAll();
511
            case 'get':
512
                return $metadata->allowGet();
513
            case 'post':
514
                return $metadata->allowPost();
515
            case 'put':
516
                return $metadata->allowPut();
517
            case 'delete':
518
                return $metadata->allowDelete();
519
        }
520
        // @codeCoverageIgnoreStart
521
        return false;
522
        // @codeCoverageIgnoreEnd
523
    }
524
}
525