Completed
Pull Request — master (#100)
by Michal
01:37
created

OpenApiHandler::getPaths()   F

Complexity

Conditions 15
Paths 481

Size

Total Lines 131

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
dl 0
loc 131
c 0
b 0
f 0
ccs 0
cts 109
cp 0
rs 1.9766
cc 15
nc 481
nop 3
crap 240

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
declare(strict_types=1);
4
5
namespace Tomaj\NetteApi\Handlers;
6
7
use InvalidArgumentException;
8
use Nette\Application\UI\InvalidLinkException;
9
use Nette\Http\IResponse;
10
use Nette\Http\Request;
11
use Symfony\Component\Yaml\Yaml;
12
use Tomaj\NetteApi\Api;
13
use Tomaj\NetteApi\ApiDecider;
14
use Tomaj\NetteApi\Authorization\BasicAuthentication;
15
use Tomaj\NetteApi\Authorization\BearerTokenAuthorization;
16
use Tomaj\NetteApi\Authorization\CookieApiKeyAuthentication;
17
use Tomaj\NetteApi\Authorization\HeaderApiKeyAuthentication;
18
use Tomaj\NetteApi\Authorization\QueryApiKeyAuthentication;
19
use Tomaj\NetteApi\Link\ApiLink;
20
use Tomaj\NetteApi\Output\JsonOutput;
21
use Tomaj\NetteApi\Output\RedirectOutput;
22
use Tomaj\NetteApi\Params\GetInputParam;
23
use Tomaj\NetteApi\Params\InputParam;
24
use Tomaj\NetteApi\Params\JsonInputParam;
25
use Tomaj\NetteApi\Params\RawInputParam;
26
use Tomaj\NetteApi\Response\JsonApiResponse;
27
use Tomaj\NetteApi\Response\ResponseInterface;
28
use Tomaj\NetteApi\Response\TextApiResponse;
29
30
class OpenApiHandler extends BaseHandler
31
{
32
    /** @var ApiDecider */
33
    private $apiDecider;
34
35
    /** @var ApiLink */
36
    private $apiLink;
37
38
    /** @var Request */
39
    private $request;
40
41
    private $initData = [];
42
43
    private $definitions = [];
44
45
    /**
46
     * OpenApiHandler constructor.
47
     * @param ApiDecider $apiDecider
48
     * @param ApiLink $apiLink
49
     * @param Request $request
50
     * @param array $initData - structured data for initialization response
51
     */
52
    public function __construct(
53
        ApiDecider $apiDecider,
54
        ApiLink $apiLink,
55
        Request $request,
56
        array $initData = []
57
    ) {
58
        parent::__construct();
59
        $this->apiDecider = $apiDecider;
60
        $this->apiLink = $apiLink;
61
        $this->request = $request;
62
        $this->initData = $initData;
63
    }
64
65
    public function params(): array
66
    {
67
        return [
68
            (new GetInputParam('format'))->setAvailableValues(['json', 'yaml'])->setDescription('Response format'),
69
        ];
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function description(): string
76
    {
77
        return 'Open API';
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function tags(): array
84
    {
85
        return ['openapi'];
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function handle(array $params): ResponseInterface
92
    {
93
        $version = $this->getEndpoint()->getVersion();
94
        $apis = $this->getApis($version);
95
        $scheme = $this->request->getUrl()->getScheme();
96
        $host = $this->request->getUrl()->getHost();
97
        $baseUrl = $scheme . '://' . $host;
98
        $basePath = $this->getBasePath($apis, $baseUrl);
99
100
        $securitySchemes = [];
101
102
        foreach ($apis as $api) {
103
            $authorization = $api->getAuthorization();
104
            if ($authorization instanceof BasicAuthentication) {
105
                $securitySchemes['Basic'] = [
106
                    'type' => 'http',
107
                    'scheme' => 'basic',
108
                ];
109
                continue;
110
            }
111
            if ($authorization instanceof BearerTokenAuthorization) {
112
                $securitySchemes['Bearer'] = [
113
                    'type' => 'http',
114
                    'scheme' => 'bearer',
115
                ];
116
                continue;
117
            }
118
            if ($authorization instanceof QueryApiKeyAuthentication) {
119
                $queryParamName = $authorization->getQueryParamName();
120
                $securitySchemes[$this->normalizeSecuritySchemeName('query', $queryParamName)] = [
121
                    'type' => 'apiKey',
122
                    'in' => 'query',
123
                    'name' => $queryParamName,
124
                ];
125
                continue;
126
            }
127
            if ($authorization instanceof HeaderApiKeyAuthentication) {
128
                $headerName = $authorization->getHeaderName();
129
                $securitySchemes[$this->normalizeSecuritySchemeName('header', $headerName)] = [
130
                    'type' => 'apiKey',
131
                    'in' => 'header',
132
                    'name' => $headerName,
133
                ];
134
                continue;
135
            }
136
            if ($authorization instanceof CookieApiKeyAuthentication) {
137
                $cookieName = $authorization->getCookieName();
138
                $securitySchemes[$this->normalizeSecuritySchemeName('cookie', $cookieName)] = [
139
                    'type' => 'apiKey',
140
                    'in' => 'cookie',
141
                    'name' => $cookieName,
142
                ];
143
                continue;
144
            }
145
        }
146
147
        $data = [
148
            'openapi' => '3.0.0',
149
            'info' => [
150
                'version' => (string)$version,
151
                'title' => 'Nette API',
152
            ],
153
            'servers' => [
154
                [
155
                    'url' => $scheme . '://' . $host . $basePath,
156
                ],
157
            ],
158
            'components' => [
159
                'securitySchemes' => $securitySchemes,
160
                'schemas' => [
161
                    'ErrorWrongInput' => [
162
                        'type' => 'object',
163
                        'properties' => [
164
                            'status' => [
165
                                'type' => 'string',
166
                                'enum' => ['error'],
167
                            ],
168
                            'message' => [
169
                                'type' => 'string',
170
                                'enum' => ['Wrong input'],
171
                            ],
172
                        ],
173
                        'required' => ['status', 'message'],
174
                    ],
175
                    'ErrorForbidden' => [
176
                        'type' => 'object',
177
                        'properties' => [
178
                            'status' => [
179
                                'type' => 'string',
180
                                'enum' => ['error'],
181
                            ],
182
                            'message' => [
183
                                'type' => 'string',
184
                                'enum' => ['Authorization header HTTP_Authorization is not set', 'Authorization header contains invalid structure', 'Authorization header doesn\'t contain bearer token', 'API key is not set'],
185
                            ],
186
                        ],
187
                        'required' => ['status', 'message'],
188
                    ],
189
                    'InternalServerError' => [
190
                        'type' => 'object',
191
                        'properties' => [
192
                            'status' => [
193
                                'type' => 'string',
194
                                'enum' => ['error'],
195
                            ],
196
                            'message' => [
197
                                'type' => 'string',
198
                                'enum' => ['Internal server error'],
199
                            ],
200
                        ],
201
                        'required' => ['status', 'message'],
202
                    ],
203
                ],
204
            ],
205
206
            'paths' => $this->getPaths($apis, $baseUrl, $basePath),
207
        ];
208
209
        if (!$securitySchemes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $securitySchemes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
210
            unset($data['components']['securitySchemes']);
211
        }
212
213
        if (!empty($this->definitions)) {
214
            $data['components']['schemas'] = array_merge($this->definitions, $data['components']['schemas']);
215
        }
216
217
        $data = array_replace_recursive($data, $this->initData);
218
219
        if ($params['format'] === 'yaml') {
220
            return new TextApiResponse(IResponse::S200_OK, Yaml::dump($data, PHP_INT_MAX, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE));
221
        }
222
        return new JsonApiResponse(IResponse::S200_OK, $data);
223
    }
224
225
    private function getApis(int $version): array
226
    {
227
        return array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) {
228
            return $version === $api->getEndpoint()->getVersion();
229
        });
230
    }
231
232
    /**
233
     * @param Api[] $versionApis
234
     * @param string $baseUrl
235
     * @param string $basePath
236
     * @return array
237
     * @throws InvalidLinkException
238
     */
239
    private function getPaths(array $versionApis, string $baseUrl, string $basePath): array
240
    {
241
        $list = [];
242
        foreach ($versionApis as $api) {
243
            $handler = $api->getHandler();
244
            $path = str_replace([$baseUrl, $basePath], '', $this->apiLink->link($api->getEndpoint()));
245
            $responses = [];
246
            foreach ($handler->outputs() as $output) {
247
                if ($output instanceof JsonOutput) {
248
                    $schema = $this->transformSchema(json_decode($output->getSchema(), true));
249
                    $responses[$output->getCode()] = [
250
                        'description' => $output->getDescription(),
251
                        'content' => [
252
                            'application/json' => [
253
                                'schema' => $schema,
254
                            ],
255
                        ]
256
                    ];
257
                }
258
259
                if ($output instanceof RedirectOutput) {
260
                    $responses[$output->getCode()] = [
261
                        'description' => 'Redirect',
262
                        'headers' => [
263
                            'Location' => [
264
                                'description' => $output->getDescription(),
265
                                'schema' => [
266
                                    'type' => 'string',
267
                                ]
268
                            ],
269
                        ]
270
                    ];
271
                }
272
            }
273
274
            $responses[IResponse::S400_BAD_REQUEST] = [
275
                'description' => 'Bad request',
276
                'content' => [
277
                    'application/json' => [
278
                        'schema' => [
279
                            '$ref' => '#/components/schemas/ErrorWrongInput',
280
                        ],
281
                    ]
282
                ],
283
            ];
284
285
            $responses[IResponse::S403_FORBIDDEN] = [
286
                'description' => 'Operation forbidden',
287
                'content' => [
288
                    'application/json' => [
289
                        'schema' => [
290
                            '$ref' => '#/components/schemas/ErrorForbidden',
291
                        ],
292
                    ],
293
                ],
294
            ];
295
296
            $responses[IResponse::S500_INTERNAL_SERVER_ERROR] = [
297
                'description' => 'Internal server error',
298
                'content' => [
299
                    'application/json' => [
300
                        'schema' => [
301
                            '$ref' => '#/components/schemas/InternalServerError',
302
                        ],
303
                    ],
304
                ],
305
            ];
306
307
            $settings = [
308
                'summary' => $handler->summary(),
309
                'description' => $handler->description(),
310
                'tags' => $handler->tags(),
311
            ];
312
313
            if ($handler instanceof AdditionalDataHandlerInterface) {
314
                foreach ($handler->additionalData() as $additionalDataKey => $additionalDataValue) {
315
                    $settings['x-' . $additionalDataKey] = $additionalDataValue;
316
                }
317
            }
318
319
            if ($handler->deprecated()) {
320
                $settings['deprecated'] = true;
321
            }
322
323
            $parameters = $this->createParamsList($handler);
324
            if (!empty($parameters)) {
325
                $settings['parameters'] = $parameters;
326
            }
327
328
            $requestBody = $this->createRequestBody($handler);
329
            if (!empty($requestBody)) {
330
                $settings['requestBody'] = $requestBody;
331
            }
332
333
            $authorization = $api->getAuthorization();
334
            if ($authorization instanceof BearerTokenAuthorization) {
335
                $settings['security'] = [
336
                    [
337
                        'Bearer' => [],
338
                    ],
339
                ];
340
            } elseif ($authorization instanceof BasicAuthentication) {
341
                $settings['security'] = [
342
                    [
343
                        'Basic' => [],
344
                    ],
345
                ];
346
            } elseif ($authorization instanceof QueryApiKeyAuthentication) {
347
                $settings['security'] = [
348
                    [
349
                        $this->normalizeSecuritySchemeName('query', $authorization->getQueryParamName()) => [],
350
                    ],
351
                ];
352
            } elseif ($authorization instanceof HeaderApiKeyAuthentication) {
353
                $settings['security'] = [
354
                    [
355
                        $this->normalizeSecuritySchemeName('header', $authorization->getHeaderName()) => [],
356
                    ],
357
                ];
358
            } elseif ($authorization instanceof CookieApiKeyAuthentication) {
359
                $settings['security'] = [
360
                    [
361
                        $this->normalizeSecuritySchemeName('cookie', $authorization->getCookieName()) => [],
362
                    ],
363
                ];
364
            }
365
            $settings['responses'] = $responses;
366
            $list[$path][strtolower($api->getEndpoint()->getMethod())] = $settings;
367
        }
368
        return $list;
369
    }
370
371
    private function getBasePath(array $apis, string $baseUrl): string
372
    {
373
        $basePath = '';
374
        foreach ($apis as $handler) {
375
            $basePath = $this->getLongestCommonSubstring($basePath, $this->apiLink->link($handler->getEndpoint()));
376
        }
377
        return rtrim(str_replace($baseUrl, '', $basePath), '/');
378
    }
379
380
    private function getLongestCommonSubstring($path1, $path2)
381
    {
382
        if ($path1 === null) {
383
            return $path2;
384
        }
385
        $commonSubstring = '';
386
        $shortest = min(strlen($path1), strlen($path2));
387
        for ($i = 0; $i <= $shortest; ++$i) {
388
            if (substr($path1, 0, $i) !== substr($path2, 0, $i)) {
389
                break;
390
            }
391
            $commonSubstring = substr($path1, 0, $i);
392
        }
393
        return $commonSubstring;
394
    }
395
396
    /**
397
     * Create array with params for specified handler
398
     *
399
     * @param ApiHandlerInterface $handler
400
     *
401
     * @return array
402
     */
403
    private function createParamsList(ApiHandlerInterface $handler)
404
    {
405
        $parameters = [];
406
        foreach ($handler->params() as $param) {
407
            if ($param->getType() !== InputParam::TYPE_GET) {
408
                continue;
409
            }
410
411
            $schema = [
412
                'type' => $param->isMulti() ? 'array' : 'string',
413
            ];
414
415
            $parameter = [
416
                'name' => $param->getKey() . ($param->isMulti() ? '[]' : ''),
417
                'in' => $this->createIn($param->getType()),
418
                'required' => $param->isRequired(),
419
                'description' => $param->getDescription(),
420
            ];
421
422
            if ($param->isMulti()) {
423
                $schema['items'] = ['type' => 'string'];
424
            }
425
            if ($param->getAvailableValues()) {
426
                $schema['enum'] = $param->getAvailableValues();
427
            }
428
            if ($param->getExample() || $param->getDefault()) {
429
                $schema['example'] = $param->getExample() ?: $param->getDefault();
430
            }
431
432
            $parameter['schema'] = $schema;
433
434
            $parameters[] = $parameter;
435
        }
436
        return $parameters;
437
    }
438
439
    private function createRequestBody(ApiHandlerInterface $handler)
440
    {
441
        $postParams = [
442
            'properties' => [],
443
            'required' => [],
444
        ];
445
        $postParamsExample = [];
446
        foreach ($handler->params() as $param) {
447
            if ($param instanceof JsonInputParam) {
448
                $schema = json_decode($param->getSchema(), true);
449
                if ($param->getExample()) {
450
                    $schema['example'] = $param->getExample();
451
                }
452
                return [
453
                    'description' => $param->getDescription(),
454
                    'required' => $param->isRequired(),
455
                    'content' => [
456
                        'application/json' => [
457
                            'schema' => $this->transformSchema($schema),
458
                        ],
459
                    ],
460
                ];
461
            }
462
            if ($param instanceof RawInputParam) {
463
                return [
464
                    'description' => $param->getDescription(),
465
                    'required' => $param->isRequired(),
466
                    'content' => [
467
                        'text/plain' => [
468
                            'schema' => [
469
                                'type' => 'string',
470
                            ],
471
                        ],
472
                    ],
473
                ];
474
            }
475
            if ($param->getType() === InputParam::TYPE_POST) {
476
                $property = [
477
                    'type' => $param->isMulti() ? 'array' : 'string',
478
                    'description' => $param->getDescription(),
479
                ];
480
                if ($param->isMulti()) {
481
                    $property['items'] = ['type' => 'string'];
482
                }
483
                if ($param->getAvailableValues()) {
484
                    $property['enum'] = $param->getAvailableValues();
485
                }
486
487
                $postParams['properties'][$param->getKey() . ($param->isMulti() ? '[]' : '')] = $property;
488
                if ($param->isRequired()) {
489
                    $postParams['required'][] = $param->getKey() . ($param->isMulti() ? '[]' : '');
490
                }
491
492
                if ($param->getExample() || $param->getDefault()) {
493
                    $postParamsExample[$param->getKey()] = $param->getExample() ?: $param->getDefault();
494
                }
495
            }
496
        }
497
498
        if (!empty($postParams['properties'])) {
499
            $postParamsSchema = [
500
                'type' => 'object',
501
                'properties' => $postParams['properties'],
502
                'required' => $postParams['required'],
503
            ];
504
505
            if ($postParamsExample) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postParamsExample of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
506
                $postParamsSchema['example'] = $postParamsExample;
507
            }
508
509
            return [
510
                'required' => true,
511
                'content' => [
512
                    'application/x-www-form-urlencoded' => [
513
                        'schema' => $postParamsSchema,
514
                    ],
515
                ],
516
            ];
517
        }
518
519
        return null;
520
    }
521
522
    private function createIn($type)
523
    {
524
        if ($type == InputParam::TYPE_GET) {
525
            return 'query';
526
        }
527
        if ($type == InputParam::TYPE_COOKIE) {
528
            return 'cookie';
529
        }
530
        return 'body';
531
    }
532
533
    private function transformSchema(array $schema)
534
    {
535
        $this->transformTypes($schema);
536
537
        if (isset($schema['definitions'])) {
538
            foreach ($schema['definitions'] as $name => $definition) {
539
                $this->addDefinition($name, $this->transformSchema($definition));
540
            }
541
            unset($schema['definitions']);
542
        }
543
        return json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES)), true);
544
    }
545
546
    private function transformTypes(array &$schema)
547
    {
548
        foreach ($schema as $key => &$value) {
549
            if ($key === 'type' && is_array($value)) {
550
                if (count($value) === 2 && in_array('null', $value)) {
551
                    unset($value[array_search('null', $value)]);
552
                    $value = implode(',', $value);
553
                    $schema['nullable'] = true;
554
                } else {
555
                    throw new InvalidArgumentException('Type cannot be array and if so, one element have to be "null"');
556
                }
557
            } elseif (is_array($value)) {
558
                $this->transformTypes($value);
559
            }
560
        }
561
    }
562
563
    private function addDefinition($name, $definition)
564
    {
565
        if (isset($this->definitions[$name])) {
566
            throw new InvalidArgumentException('Definition with name ' . $name . ' already exists. Rename it or use existing one.');
567
        }
568
        $this->definitions[$name] = $definition;
569
    }
570
571
    private function normalizeSecuritySchemeName(string $type, string $name): string
572
    {
573
        return 'api_key__' . $type . '__' . strtolower(str_replace('-', '_', $name));
574
    }
575
}
576