Completed
Pull Request — master (#98)
by Michal
01:35
created

OpenApiHandler::getPaths()   F

Complexity

Conditions 14
Paths 481

Size

Total Lines 129

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 0
Metric Value
dl 0
loc 129
c 0
b 0
f 0
ccs 0
cts 107
cp 0
rs 2.2566
cc 14
nc 481
nop 3
crap 210

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
            foreach ($handler->additionalData() as $additionalDataKey => $additionalDataValue) {
314
                $settings['x-' . $additionalDataKey] = $additionalDataValue;
315
            }
316
317
            if ($handler->deprecated()) {
318
                $settings['deprecated'] = true;
319
            }
320
321
            $parameters = $this->createParamsList($handler);
322
            if (!empty($parameters)) {
323
                $settings['parameters'] = $parameters;
324
            }
325
326
            $requestBody = $this->createRequestBody($handler);
327
            if (!empty($requestBody)) {
328
                $settings['requestBody'] = $requestBody;
329
            }
330
331
            $authorization = $api->getAuthorization();
332
            if ($authorization instanceof BearerTokenAuthorization) {
333
                $settings['security'] = [
334
                    [
335
                        'Bearer' => [],
336
                    ],
337
                ];
338
            } elseif ($authorization instanceof BasicAuthentication) {
339
                $settings['security'] = [
340
                    [
341
                        'Basic' => [],
342
                    ],
343
                ];
344
            } elseif ($authorization instanceof QueryApiKeyAuthentication) {
345
                $settings['security'] = [
346
                    [
347
                        $this->normalizeSecuritySchemeName('query', $authorization->getQueryParamName()) => [],
348
                    ],
349
                ];
350
            } elseif ($authorization instanceof HeaderApiKeyAuthentication) {
351
                $settings['security'] = [
352
                    [
353
                        $this->normalizeSecuritySchemeName('header', $authorization->getHeaderName()) => [],
354
                    ],
355
                ];
356
            } elseif ($authorization instanceof CookieApiKeyAuthentication) {
357
                $settings['security'] = [
358
                    [
359
                        $this->normalizeSecuritySchemeName('cookie', $authorization->getCookieName()) => [],
360
                    ],
361
                ];
362
            }
363
            $settings['responses'] = $responses;
364
            $list[$path][strtolower($api->getEndpoint()->getMethod())] = $settings;
365
        }
366
        return $list;
367
    }
368
369
    private function getBasePath(array $apis, string $baseUrl): string
370
    {
371
        $basePath = '';
372
        foreach ($apis as $handler) {
373
            $basePath = $this->getLongestCommonSubstring($basePath, $this->apiLink->link($handler->getEndpoint()));
374
        }
375
        return rtrim(str_replace($baseUrl, '', $basePath), '/');
376
    }
377
378
    private function getLongestCommonSubstring($path1, $path2)
379
    {
380
        if ($path1 === null) {
381
            return $path2;
382
        }
383
        $commonSubstring = '';
384
        $shortest = min(strlen($path1), strlen($path2));
385
        for ($i = 0; $i <= $shortest; ++$i) {
386
            if (substr($path1, 0, $i) !== substr($path2, 0, $i)) {
387
                break;
388
            }
389
            $commonSubstring = substr($path1, 0, $i);
390
        }
391
        return $commonSubstring;
392
    }
393
394
    /**
395
     * Create array with params for specified handler
396
     *
397
     * @param ApiHandlerInterface $handler
398
     *
399
     * @return array
400
     */
401
    private function createParamsList(ApiHandlerInterface $handler)
402
    {
403
        $parameters = [];
404
        foreach ($handler->params() as $param) {
405
            if ($param->getType() !== InputParam::TYPE_GET) {
406
                continue;
407
            }
408
409
            $schema = [
410
                'type' => $param->isMulti() ? 'array' : 'string',
411
            ];
412
413
            $parameter = [
414
                'name' => $param->getKey() . ($param->isMulti() ? '[]' : ''),
415
                'in' => $this->createIn($param->getType()),
416
                'required' => $param->isRequired(),
417
                'description' => $param->getDescription(),
418
            ];
419
420
            if ($param->isMulti()) {
421
                $schema['items'] = ['type' => 'string'];
422
            }
423
            if ($param->getAvailableValues()) {
424
                $schema['enum'] = $param->getAvailableValues();
425
            }
426
            if ($param->getExample() || $param->getDefault()) {
427
                $schema['example'] = $param->getExample() ?: $param->getDefault();
428
            }
429
430
            $parameter['schema'] = $schema;
431
432
            $parameters[] = $parameter;
433
        }
434
        return $parameters;
435
    }
436
437
    private function createRequestBody(ApiHandlerInterface $handler)
438
    {
439
        $postParams = [
440
            'properties' => [],
441
            'required' => [],
442
        ];
443
        $postParamsExample = [];
444
        foreach ($handler->params() as $param) {
445
            if ($param instanceof JsonInputParam) {
446
                $schema = json_decode($param->getSchema(), true);
447
                if ($param->getExample()) {
448
                    $schema['example'] = $param->getExample();
449
                }
450
                return [
451
                    'description' => $param->getDescription(),
452
                    'required' => $param->isRequired(),
453
                    'content' => [
454
                        'application/json' => [
455
                            'schema' => $this->transformSchema($schema),
456
                        ],
457
                    ],
458
                ];
459
            }
460
            if ($param instanceof RawInputParam) {
461
                return [
462
                    'description' => $param->getDescription(),
463
                    'required' => $param->isRequired(),
464
                    'content' => [
465
                        'text/plain' => [
466
                            'schema' => [
467
                                'type' => 'string',
468
                            ],
469
                        ],
470
                    ],
471
                ];
472
            }
473
            if ($param->getType() === InputParam::TYPE_POST) {
474
                $property = [
475
                    'type' => $param->isMulti() ? 'array' : 'string',
476
                    'description' => $param->getDescription(),
477
                ];
478
                if ($param->isMulti()) {
479
                    $property['items'] = ['type' => 'string'];
480
                }
481
                if ($param->getAvailableValues()) {
482
                    $property['enum'] = $param->getAvailableValues();
483
                }
484
485
                $postParams['properties'][$param->getKey() . ($param->isMulti() ? '[]' : '')] = $property;
486
                if ($param->isRequired()) {
487
                    $postParams['required'][] = $param->getKey() . ($param->isMulti() ? '[]' : '');
488
                }
489
490
                if ($param->getExample() || $param->getDefault()) {
491
                    $postParamsExample[$param->getKey()] = $param->getExample() ?: $param->getDefault();
492
                }
493
            }
494
        }
495
496
        if (!empty($postParams['properties'])) {
497
            $postParamsSchema = [
498
                'type' => 'object',
499
                'properties' => $postParams['properties'],
500
                'required' => $postParams['required'],
501
            ];
502
503
            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...
504
                $postParamsSchema['example'] = $postParamsExample;
505
            }
506
507
            return [
508
                'required' => true,
509
                'content' => [
510
                    'application/x-www-form-urlencoded' => [
511
                        'schema' => $postParamsSchema,
512
                    ],
513
                ],
514
            ];
515
        }
516
517
        return null;
518
    }
519
520
    private function createIn($type)
521
    {
522
        if ($type == InputParam::TYPE_GET) {
523
            return 'query';
524
        }
525
        if ($type == InputParam::TYPE_COOKIE) {
526
            return 'cookie';
527
        }
528
        return 'body';
529
    }
530
531
    private function transformSchema(array $schema)
532
    {
533
        $this->transformTypes($schema);
534
535
        if (isset($schema['definitions'])) {
536
            foreach ($schema['definitions'] as $name => $definition) {
537
                $this->addDefinition($name, $this->transformSchema($definition));
538
            }
539
            unset($schema['definitions']);
540
        }
541
        return json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES)), true);
542
    }
543
544
    private function transformTypes(array &$schema)
545
    {
546
        foreach ($schema as $key => &$value) {
547
            if ($key === 'type' && is_array($value)) {
548
                if (count($value) === 2 && in_array('null', $value)) {
549
                    unset($value[array_search('null', $value)]);
550
                    $value = implode(',', $value);
551
                    $schema['nullable'] = true;
552
                } else {
553
                    throw new InvalidArgumentException('Type cannot be array and if so, one element have to be "null"');
554
                }
555
            } elseif (is_array($value)) {
556
                $this->transformTypes($value);
557
            }
558
        }
559
    }
560
561
    private function addDefinition($name, $definition)
562
    {
563
        if (isset($this->definitions[$name])) {
564
            throw new InvalidArgumentException('Definition with name ' . $name . ' already exists. Rename it or use existing one.');
565
        }
566
        $this->definitions[$name] = $definition;
567
    }
568
569
    private function normalizeSecuritySchemeName(string $type, string $name): string
570
    {
571
        return 'api_key__' . $type . '__' . strtolower(str_replace('-', '_', $name));
572
    }
573
}
574