Completed
Pull Request — master (#56)
by Michal
12:52
created

OpenApiHandler::getLongestCommonSubstring()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15

Duplication

Lines 15
Ratio 100 %

Importance

Changes 0
Metric Value
dl 15
loc 15
rs 9.7666
c 0
b 0
f 0
cc 4
nc 4
nop 2
1
<?php
2
3
namespace Tomaj\NetteApi\Handlers;
4
5
use InvalidArgumentException;
6
use JSONSchemaFaker\Faker;
7
use Nette\Http\Request;
8
use Symfony\Component\Yaml\Yaml;
9
use Tomaj\NetteApi\ApiDecider;
10
use Tomaj\NetteApi\Authorization\BearerTokenAuthorization;
11
use Tomaj\NetteApi\Link\ApiLink;
12
use Tomaj\NetteApi\Output\JsonOutput;
13
use Tomaj\NetteApi\Params\InputParam;
14
use Tomaj\NetteApi\Params\JsonInputParam;
15
use Tomaj\NetteApi\Response\JsonApiResponse;
16
use Tomaj\NetteApi\Response\TextApiResponse;
17
use Tracy\Debugger;
18
19
class OpenApiHandler extends BaseHandler
20
{
21
    /** @var ApiDecider */
22
    private $apiDecider;
23
24
    /** @var ApiLink */
25
    private $apiLink;
26
27
    /** @var Request */
28
    private $request;
29
30
    private $initData = [];
31
32
    private $faker;
33
34
    private $definitions = [];
35
36
    /**
37
     * OpenApiHandler constructor.
38
     * @param ApiDecider $apiDecider
39
     * @param ApiLink $apiLink
40
     * @param Request $request
41
     * @param array $initData - structured data for initialization response
42
     */
43
    public function __construct(
44
        ApiDecider $apiDecider,
45
        ApiLink $apiLink,
46
        Request $request,
47
        array $initData = []
48
    ) {
49
        parent::__construct();
50
        $this->apiDecider = $apiDecider;
51
        $this->apiLink = $apiLink;
52
        $this->request = $request;
53
        $this->initData = $initData;
54
        $this->faker = new Faker();
55
    }
56
57
    public function params()
58
    {
59
        return [
60
            new InputParam(InputParam::TYPE_GET, 'format', InputParam::OPTIONAL, ['json', 'yaml'], false, 'Response format')
61
        ];
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function description()
68
    {
69
        return 'Open API';
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function tags()
76
    {
77
        return ['openapi'];
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function handle($params)
84
    {
85
        $version = $this->getEndpoint()->getVersion();
86
        $handlers = $this->getHandlers($version);
87
        $scheme = $this->request->getUrl()->getScheme();
88
        $host = $this->request->getUrl()->getHost();
89
        $baseUrl = $scheme . '://' . $host;
90
        $basePath = $this->getBasePath($handlers, $baseUrl);
91
92
        $responses = [
0 ignored issues
show
Unused Code introduced by
$responses is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
93
            404 => [
94
                'description' => 'Not found',
95
                'schema' => [
96
                    'type' => 'object',
97
                    'properties' => [
98
                        'status' => [
99
                            'type' => 'string',
100
                            'enum' => ['error'],
101
                        ],
102
                        'message' => [
103
                            'type' => 'string',
104
                            'enum' => ['Unknown api endpoint'],
105
                        ],
106
                    ],
107
                    'required' => ['status', 'message'],
108
                ],
109
            ],
110
            500 => [
111
                'description' => 'Internal server error',
112
                'schema' => [
113
                    'type' => 'object',
114
                    'properties' => [
115
                        'status' => [
116
                            'type' => 'string',
117
                            'enum' => ['error'],
118
                        ],
119
                        'message' => [
120
                            'type' => 'string',
121
                            'enum' => ['Internal server error'],
122
                        ],
123
                    ],
124
                    'required' => ['status', 'message'],
125
                ],
126
            ],
127
        ];
128
129
        $data = [
130
            'openapi' => '3.0.0',
131
            'info' => [
132
                'title' => $this->apiDecider->getTitle(),
133
                'description' => $this->apiDecider->getDescription(),
134
                'version' => $version,
135
            ],
136
            'servers' => [
137
                [
138
                    'url' => $scheme . '://' . $host . $basePath,
139
                ],
140
            ],
141
            'components' => [
142
                'securitySchemes' => [
143
                    'Bearer' => [
144
                        'type' => 'http',
145
                        'scheme' => 'bearer',
146
                    ],
147
                ],
148
                'schemas' => [
149
                    'ErrorWrongInput' => [
150
                        'type' => 'object',
151
                        'properties' => [
152
                            'status' => [
153
                                'type' => 'string',
154
                                'enum' => ['error'],
155
                            ],
156
                            'message' => [
157
                                'type' => 'string',
158
                                'enum' => ['Wrong input'],
159
                            ],
160
                        ],
161
                        'required' => ['status', 'message'],
162
                    ],
163
                    'ErrorForbidden' => [
164
                        'type' => 'object',
165
                        'properties' => [
166
                            'status' => [
167
                                'type' => 'string',
168
                                'enum' => ['error'],
169
                            ],
170
                            'message' => [
171
                                'type' => 'string',
172
                                'enum' => ['Authorization header HTTP_Authorization is not set', 'Authorization header contains invalid structure'],
173
                            ],
174
                        ],
175
                        'required' => ['status', 'message'],
176
                    ],
177
                    'InternalServerError' => [
178
                        'type' => 'object',
179
                        'properties' => [
180
                            'status' => [
181
                                'type' => 'string',
182
                                'enum' => ['error'],
183
                            ],
184
                            'message' => [
185
                                'type' => 'string',
186
                                'enum' => ['Internal server error'],
187
                            ],
188
                        ],
189
                        'required' => ['status', 'message'],
190
                    ],
191
                ],
192
            ],
193
194
            'paths' => $this->getHandlersList($handlers, $baseUrl, $basePath),
195
        ];
196
197
        if (!empty($this->definitions)) {
198
            $data['components']['schemas'] = array_merge($this->definitions, $data['components']['schemas']);
199
        }
200
201 View Code Duplication
        if ($params['format'] === 'yaml') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
            return new TextApiResponse(200, Yaml::dump($data, PHP_INT_MAX, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE), 'text/plain');
203
        }
204
205
        $data = array_merge_recursive($this->initData, $data);
206
        return new JsonApiResponse(200, $data);
207
    }
208
209
    /**
210
     * @param int $version
211
     * @return []
0 ignored issues
show
Documentation introduced by
The doc-type [] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
212
     */
213 View Code Duplication
    private function getHandlers($version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
    {
215
        $versionHandlers = array_filter($this->apiDecider->getHandlers(), function ($handler) use ($version) {
216
            return $version == $handler['endpoint']->getVersion();
217
        });
218
        return $versionHandlers;
219
    }
220
221
    /**
222
     * Create handler list for specified version
223
     *
224
     * @param array $versionHandlers
225
     * @param string $basePath
226
     *
227
     * @return array
228
     */
229
    private function getHandlersList($versionHandlers, $baseUrl, $basePath)
230
    {
231
        $list = [];
232
        foreach ($versionHandlers as $handler) {
233
            $path = str_replace([$baseUrl, $basePath], '', $this->apiLink->link($handler['endpoint']));
234
235
            $responses = [];
236
            foreach ($handler['handler']->outputs() as $output) {
237
                if ($output instanceof JsonOutput) {
238
                    $schema = $this->transformSchema(json_decode($output->getSchema(), true));
239
                    $responses[$output->getCode()] = [
240
                        'description' => $output->getDescription(),
241
                        'content' => [
242
                            'application/json' => [
243
                                'schema' => $schema,
244
                            ],
245
                        ]
246
                    ];
247
                }
248
            }
249
250
            $responses[400] = [
251
                'description' => 'Bad request',
252
                'content' => [
253
                    'application/json' => [
254
                        'schema' => [
255
                            '$ref' => '#/components/schemas/ErrorWrongInput',
256
                        ],
257
                    ]
258
                ],
259
            ];
260
261
            $responses[403] = [
262
                'description' => 'Operation forbidden',
263
                'content' => [
264
                    'application/json' => [
265
                        'schema' => [
266
                            '$ref' => '#/components/schemas/ErrorForbidden',
267
                        ],
268
                    ],
269
                ],
270
            ];
271
272
            $responses[500] = [
273
                'description' => 'Internal server error',
274
                'content' => [
275
                    'application/json' => [
276
                        'schema' => [
277
                            '$ref' => '#/components/schemas/InternalServerError',
278
                        ],
279
                    ],
280
                ],
281
            ];
282
283
            $settings = [
284
                'summary' => $handler['handler']->description(),
285
                'tags' => $handler['handler']->tags(),
286
            ];
287
288
            $parameters = $this->createParamsList($handler['handler']);
289
            if (!empty($parameters)) {
290
                $settings['parameters'] = $parameters;
291
            }
292
293
            $requestBody = $this->createRequestBody($handler['handler']);
294
            if (!empty($requestBody)) {
295
                $settings['requestBody'] = $requestBody;
296
            }
297
298 View Code Duplication
            if ($handler['authorization'] instanceof BearerTokenAuthorization) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299
                $settings['security'] = [
300
                    [
301
                        'Bearer' => [],
302
                    ],
303
                ];
304
            }
305
            $settings['responses'] = $responses;
306
            $list[$path][strtolower($handler['endpoint']->getMethod())] = $settings;
307
        }
308
        return $list;
309
    }
310
311 View Code Duplication
    private function getBasePath($handlers, $baseUrl)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
312
    {
313
        $basePath = null;
314
        foreach ($handlers as $handler) {
315
            $basePath = $this->getLongestCommonSubstring($basePath, $this->apiLink->link($handler['endpoint']));
316
        }
317
        return rtrim(str_replace($baseUrl, '', $basePath), '/');
318
    }
319
320 View Code Duplication
    private function getLongestCommonSubstring($path1, $path2)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
    {
322
        if ($path1 === null) {
323
            return $path2;
324
        }
325
        $commonSubstring = '';
326
        $shortest = min(strlen($path1), strlen($path2));
327
        for ($i = 0; $i <= $shortest; ++$i) {
328
            if (substr($path1, 0, $i) !== substr($path2, 0, $i)) {
329
                break;
330
            }
331
            $commonSubstring = substr($path1, 0, $i);
332
        }
333
        return $commonSubstring;
334
    }
335
336
    /**
337
     * Create array with params for specified handler
338
     *
339
     * @param ApiHandlerInterface $handler
340
     *
341
     * @return array
342
     */
343
    private function createParamsList(ApiHandlerInterface $handler)
344
    {
345
        $parameters = [];
346
        foreach ($handler->params() as $param) {
347
            if ($param instanceof JsonInputParam) {
348
                continue;
349
            }
350
351
            $parameter = [
352
                'name' => $param->getKey(),
353
                'in' => $this->createIn($param->getType()),
354
                'required' => $param->isRequired(),
355
                'description' => $param->getDescription(),
356
                'schema' => [
357
                    'type' => $param->isMulti() ? 'array' : 'string',
358
                ],
359
            ];
360
            if ($param->getAvailableValues()) {
361
                $parameter['schema']['enum'] = $param->getAvailableValues();
362
            }
363
        }
364
        return $parameters;
365
    }
366
367
    private function createRequestBody(ApiHandlerInterface $handler)
368
    {
369
        foreach ($handler->params() as $param) {
370
            if ($param instanceof JsonInputParam) {
371
                return [
372
                    'description' => $param->getDescription(),
373
                    'required' => $param->isRequired(),
374
                    'content' => [
375
                        'application/json' => [
376
                            'schema' => $this->transformSchema(json_decode($param->getSchema(), true)),
377
                        ],
378
                    ],
379
                ];
380
            }
381
        }
382
        return null;
383
    }
384
385 View Code Duplication
    private function createIn($type)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
386
    {
387
        if ($type == InputParam::TYPE_GET) {
388
            return 'query';
389
        }
390
        if ($type == InputParam::TYPE_COOKIE) {
391
            return 'cookie';
392
        }
393
        return 'body';
394
    }
395
396
    private function transformSchema(array $schema)
397
    {
398
        $originalSchema = json_decode(json_encode($schema));
399
        $this->transformTypes($schema);
400
401
        if (isset($schema['definitions'])) {
402
            foreach ($schema['definitions'] as $name => $definition) {
403
                $this->addDefinition($name, $definition);
404
            }
405
            unset($schema['definitions']);
406
            $schema = json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES)), true);
407
        }
408
        $schema['example'] = json_decode(json_encode($this->faker->generate($originalSchema)), true);
409
        return $schema;
410
    }
411
412
    private function transformTypes(array &$schema)
413
    {
414
        foreach ($schema as $key => &$value) {
415
            if ($key === 'type' && is_array($value)) {
416
                if (count($value) === 2 && in_array('null', $value)) {
417
                    unset($value[array_search('null', $value)]);
418
                    $value = implode(',', $value);
419
                    $schema['nullable'] = true;
420
                } else {
421
                    throw new InvalidArgumentException('Type cannot be array and if so, one element have to be "null"');
422
                }
423
            } elseif (is_array($value)) {
424
                $this->transformTypes($value);
425
            }
426
        }
427
    }
428
429
    private function addDefinition($name, $definition)
430
    {
431
        if (isset($this->definitions[$name])) {
432
            throw new InvalidArgumentException('Definition with name ' . $name . ' already exists. Rename it or use existing one.');
433
        }
434
        $this->definitions[$name] = $definition;
435
    }
436
}
437