Skip to content

Commit 3f85697

Browse files
authored
Merge pull request #134 from RonasIT/feat/update-openapi-version
[feat]: update openapi version
2 parents a5f9e4a + fb18533 commit 3f85697

File tree

83 files changed

+2674
-1774
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+2674
-1774
lines changed

src/Exceptions/SpecValidation/InvalidSwaggerVersionException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class InvalidSwaggerVersionException extends InvalidSwaggerSpecException
88
{
99
public function __construct(string $version)
1010
{
11-
$expectedVersion = SwaggerService::SWAGGER_VERSION;
11+
$expectedVersion = SwaggerService::OPEN_API_VERSION;
1212

1313
parent::__construct("Unrecognized Swagger version '{$version}'. Expected {$expectedVersion}.");
1414
}

src/Services/SwaggerService.php

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class SwaggerService
2929
{
3030
use GetDependenciesTrait;
3131

32-
public const SWAGGER_VERSION = '2.0';
32+
public const string OPEN_API_VERSION = '3.1.0';
3333

3434
protected $driver;
3535
protected $openAPIValidator;
@@ -46,19 +46,19 @@ class SwaggerService
4646
private $item;
4747
private $security;
4848

49-
protected $ruleToTypeMap = [
49+
protected array $ruleToTypeMap = [
5050
'array' => 'object',
5151
'boolean' => 'boolean',
5252
'date' => 'date',
5353
'digits' => 'integer',
5454
'integer' => 'integer',
5555
'numeric' => 'double',
5656
'string' => 'string',
57-
'int' => 'integer'
57+
'int' => 'integer',
5858
];
5959

6060
protected $booleanAnnotations = [
61-
'deprecated'
61+
'deprecated',
6262
];
6363

6464
public function __construct(Container $container)
@@ -138,12 +138,14 @@ protected function generateEmptyData(): array
138138
}
139139

140140
$data = [
141-
'swagger' => self::SWAGGER_VERSION,
142-
'host' => $this->getAppUrl(),
143-
'basePath' => $this->config['basePath'],
144-
'schemes' => $this->config['schemes'],
141+
'openapi' => self::OPEN_API_VERSION,
142+
'servers' => [
143+
['url' => $this->getAppUrl() . $this->config['basePath']],
144+
],
145145
'paths' => [],
146-
'definitions' => $this->config['definitions'],
146+
'components' => [
147+
'schemas' => $this->config['definitions'],
148+
],
147149
'info' => $this->prepareInfo($this->config['info'])
148150
];
149151

@@ -242,7 +244,9 @@ protected function getPathParams(): array
242244
'name' => $key,
243245
'description' => $this->generatePathDescription($key),
244246
'required' => true,
245-
'type' => 'string'
247+
'schema' => [
248+
'type' => 'string'
249+
]
246250
];
247251
}
248252

@@ -307,7 +311,7 @@ protected function saveResponseSchema(?array $content, string $definition): void
307311
$this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
308312
}
309313

310-
$this->data['definitions'][$definition] = [
314+
$this->data['components']['schemas'][$definition] = [
311315
'type' => $schemaType,
312316
'properties' => $schemaProperties
313317
];
@@ -329,7 +333,9 @@ protected function saveListResponseDefinitions(array $content, array &$schemaPro
329333

330334
protected function saveObjectResponseDefinitions(array $content, array &$schemaProperties, string $definition): void
331335
{
332-
$properties = Arr::get($this->data['definitions'], $definition, []);
336+
$definitions = (!empty($this->data['components']['schemas'])) ? $this->data['components']['schemas'] : [];
337+
338+
$properties = Arr::get($definitions, $definition, []);
333339

334340
foreach ($content as $name => $value) {
335341
$property = Arr::get($properties, "properties.{$name}", []);
@@ -395,7 +401,7 @@ protected function parseResponse($response)
395401
$this->saveResponseSchema($content, $definition);
396402

397403
if (is_array($this->item['responses'][$code])) {
398-
$this->item['responses'][$code]['schema']['$ref'] = "#/definitions/{$definition}";
404+
$this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
399405
}
400406
}
401407

@@ -418,17 +424,23 @@ protected function saveExample($code, $content, $produce)
418424

419425
protected function makeResponseExample($content, $mimeType, $description = ''): array
420426
{
421-
$responseExample = ['description' => $description];
427+
$example = match ($mimeType) {
428+
'application/json' => json_decode($content, true),
429+
'application/pdf' => base64_encode($content),
430+
default => $content,
431+
};
422432

423-
if ($mimeType === 'application/json') {
424-
$responseExample['schema'] = ['example' => json_decode($content, true)];
425-
} elseif ($mimeType === 'application/pdf') {
426-
$responseExample['schema'] = ['example' => base64_encode($content)];
427-
} else {
428-
$responseExample['examples']['example'] = $content;
429-
}
430-
431-
return $responseExample;
433+
return [
434+
'description' => $description,
435+
'content' => [
436+
$mimeType => [
437+
'schema' => [
438+
'type' => 'object',
439+
],
440+
'example' => $example,
441+
],
442+
],
443+
];
432444
}
433445

434446
protected function saveParameters($request, array $annotations)
@@ -504,7 +516,9 @@ protected function saveGetRequestParameters($rules, array $attributes, array $an
504516
'in' => 'query',
505517
'name' => $parameter,
506518
'description' => $description,
507-
'type' => $this->getParameterType($validation)
519+
'schema' => [
520+
'type' => $this->getParameterType($validation),
521+
],
508522
];
509523
if (in_array('required', $validation)) {
510524
$parameterDefinition['required'] = true;
@@ -519,14 +533,18 @@ protected function savePostRequestParameters($actionName, $rules, array $attribu
519533
{
520534
if ($this->requestHasMoreProperties($actionName)) {
521535
if ($this->requestHasBody()) {
522-
$this->item['parameters'][] = [
523-
'in' => 'body',
524-
'name' => 'body',
536+
$type = $this->request->header('Content-Type') ?? 'application/json';
537+
538+
$this->item['requestBody'] = [
539+
'content' => [
540+
$type => [
541+
'schema' => [
542+
"\$ref" => "#/components/schemas/{$actionName}Object",
543+
],
544+
],
545+
],
525546
'description' => '',
526547
'required' => true,
527-
'schema' => [
528-
"\$ref" => "#/definitions/{$actionName}Object"
529-
]
530548
];
531549
}
532550

@@ -559,7 +577,7 @@ protected function saveDefinitions($objectName, $rules, $attributes, array $anno
559577
}
560578

561579
$data['example'] = $this->generateExample($data['properties']);
562-
$this->data['definitions'][$objectName . 'Object'] = $data;
580+
$this->data['components']['schemas'][$objectName . 'Object'] = $data;
563581
}
564582

565583
protected function getParameterType(array $validation): string
@@ -600,8 +618,8 @@ protected function requestHasMoreProperties($actionName): bool
600618
{
601619
$requestParametersCount = count($this->request->all());
602620

603-
if (isset($this->data['definitions'][$actionName . 'Object']['properties'])) {
604-
$objectParametersCount = count($this->data['definitions'][$actionName . 'Object']['properties']);
621+
if (isset($this->data['components']['schemas'][$actionName . 'Object']['properties'])) {
622+
$objectParametersCount = count($this->data['components']['schemas'][$actionName . 'Object']['properties']);
605623
} else {
606624
$objectParametersCount = 0;
607625
}
@@ -979,11 +997,11 @@ protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocu
979997
}
980998
}
981999

982-
$definitions = array_keys($additionalDocumentation['definitions']);
1000+
$definitions = array_keys($additionalDocumentation['components']['schemas']);
9831001

9841002
foreach ($definitions as $definition) {
985-
if (empty($documentation['definitions'][$definition])) {
986-
$documentation['definitions'][$definition] = $additionalDocumentation['definitions'][$definition];
1003+
if (empty($documentation['components']['schemas'][$definition])) {
1004+
$documentation['components']['schemas'][$definition] = $additionalDocumentation['components']['schemas'][$definition];
9871005
}
9881006
}
9891007
}

src/Validators/SwaggerSpecValidator.php

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ class SwaggerSpecValidator
4848
];
4949

5050
public const REQUIRED_FIELDS = [
51-
'definition' => ['type'],
52-
'doc' => ['swagger', 'info', 'paths'],
51+
'components' => ['type'],
52+
'doc' => ['openapi', 'info', 'paths'],
5353
'info' => ['title', 'version'],
5454
'item' => ['type'],
5555
'header' => ['type'],
5656
'operation' => ['responses'],
5757
'parameter' => ['in', 'name'],
58+
'requestBody' => ['content'],
5859
'response' => ['description'],
5960
'security_definition' => ['type'],
6061
'tag' => ['name'],
@@ -76,6 +77,7 @@ class SwaggerSpecValidator
7677

7778
public const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data';
7879
public const MIME_TYPE_APPLICATION_URLENCODED = 'application/x-www-form-urlencoded';
80+
public const MIME_TYPE_APPLICATION_JSON = 'application/json';
7981

8082
protected $doc;
8183

@@ -96,9 +98,9 @@ public function validate(array $doc): void
9698

9799
protected function validateVersion(): void
98100
{
99-
$version = Arr::get($this->doc, 'swagger', '');
101+
$version = Arr::get($this->doc, 'openapi', '');
100102

101-
if (version_compare($version, SwaggerService::SWAGGER_VERSION, '!=')) {
103+
if (version_compare($version, SwaggerService::OPEN_API_VERSION, '!=')) {
102104
throw new InvalidSwaggerVersionException($version);
103105
}
104106
}
@@ -128,6 +130,10 @@ protected function validatePaths(): void
128130

129131
$this->validateParameters($operation, $path, $operationId);
130132

133+
if (!empty($operation['requestBody'])) {
134+
$this->validateRequestBody($operation, $path, $operationId);
135+
}
136+
131137
foreach ($operation['responses'] as $statusCode => $response) {
132138
$this->validateResponse($response, $statusCode, $operationId);
133139
}
@@ -139,10 +145,10 @@ protected function validatePaths(): void
139145

140146
protected function validateDefinitions(): void
141147
{
142-
$definitions = Arr::get($this->doc, 'definitions', []);
148+
$definitions = Arr::get($this->doc, 'components.schemas', []);
143149

144150
foreach ($definitions as $index => $definition) {
145-
$this->validateFieldsPresent(self::REQUIRED_FIELDS['definition'], "definitions.{$index}");
151+
$this->validateFieldsPresent(self::REQUIRED_FIELDS['components'], "components.schemas.{$index}");
146152
}
147153
}
148154

@@ -196,10 +202,10 @@ protected function validateResponse(array $response, string $statusCode, string
196202
array_merge(self::SCHEMA_TYPES, ['file']),
197203
"{$responseId}.schema"
198204
);
199-
}
200205

201-
if (!empty($response['items'])) {
202-
$this->validateItems($response['items'], "{$responseId}.items");
206+
if (!empty($response['schema']['items'])) {
207+
$this->validateItems($response['schema']['items'], "{$responseId}.schema.items");
208+
}
203209
}
204210
}
205211

@@ -220,8 +226,8 @@ protected function validateParameters(array $operation, string $path, string $op
220226

221227
$this->validateParameterType($param, $operation, $paramId, $operationId);
222228

223-
if (!empty($param['items'])) {
224-
$this->validateItems($param['items'], "{$paramId}.items");
229+
if (!empty($param['schema']['items'])) {
230+
$this->validateItems($param['schema']['items'], "{$paramId}.schema.items");
225231
}
226232
}
227233

@@ -230,6 +236,38 @@ protected function validateParameters(array $operation, string $path, string $op
230236
$this->validateBodyParameters($parameters, $operationId);
231237
}
232238

239+
protected function validateRequestBody(array $operation, string $path, string $operationId): void
240+
{
241+
$requestBody = Arr::get($operation, 'requestBody', []);
242+
243+
$this->validateFieldsPresent(self::REQUIRED_FIELDS['requestBody'], "{$operationId}.requestBody");
244+
245+
$this->validateRequestBodyContent($requestBody['content'], $operationId);
246+
}
247+
248+
protected function validateRequestBodyContent(array $content, string $operationId): void
249+
{
250+
$allowedContentType = false;
251+
252+
$types = [
253+
self::MIME_TYPE_APPLICATION_URLENCODED,
254+
self::MIME_TYPE_MULTIPART_FORM_DATA,
255+
self::MIME_TYPE_APPLICATION_JSON,
256+
];
257+
258+
foreach ($types as $type) {
259+
if (!empty($content[$type])) {
260+
$allowedContentType = true;
261+
}
262+
}
263+
264+
if (!$allowedContentType) {
265+
throw new InvalidSwaggerSpecException(
266+
"Operation '{$operationId}' has body parameters. Only one or the other is allowed."
267+
);
268+
}
269+
}
270+
233271
protected function validateType(array $schema, array $validTypes, string $schemaId): void
234272
{
235273
$schemaType = Arr::get($schema, 'type');
@@ -313,12 +351,12 @@ protected function validateParameterType(array $param, array $operation, string
313351
case 'formData':
314352
$this->validateFormDataConsumes($operation, $operationId);
315353

316-
$requiredFields = ['type'];
354+
$requiredFields = ['schema'];
317355
$validTypes = array_merge(self::PRIMITIVE_TYPES, ['file']);
318356

319357
break;
320358
default:
321-
$requiredFields = ['type'];
359+
$requiredFields = ['schema'];
322360
$validTypes = self::PRIMITIVE_TYPES;
323361
}
324362

@@ -393,7 +431,7 @@ protected function validateRefs(): void
393431
!empty($refFilename)
394432
? json_decode(file_get_contents($refFilename), true)
395433
: $this->doc,
396-
$refParentKey
434+
str_replace('/', '.', $refParentKey),
397435
);
398436

399437
if (!empty($missingRefs)) {

tests/SwaggerServiceTest.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public static function getConstructorInvalidTmpData(): array
8989
[
9090
'tmpDoc' => 'documentation/invalid_version',
9191
'exception' => InvalidSwaggerVersionException::class,
92-
'exceptionMessage' => "Unrecognized Swagger version '1.0'. Expected 2.0.",
92+
'exceptionMessage' => "Unrecognized Swagger version '1.0'. Expected 3.1.0.",
9393
],
9494
[
9595
'tmpDoc' => 'documentation/invalid_format__array_parameter__no_items',
@@ -218,7 +218,7 @@ public static function getConstructorInvalidTmpData(): array
218218
[
219219
'tmpDoc' => 'documentation/invalid_format__missing_field__definition_type',
220220
'exception' => MissingFieldException::class,
221-
'exceptionMessage' => "Validation failed. 'definitions.authloginObject' should have "
221+
'exceptionMessage' => "Validation failed. 'components.schemas.authloginObject' should have "
222222
. "required fields: type.",
223223
],
224224
[
@@ -229,7 +229,7 @@ public static function getConstructorInvalidTmpData(): array
229229
[
230230
'tmpDoc' => 'documentation/invalid_format__missing_field__items_type',
231231
'exception' => MissingFieldException::class,
232-
'exceptionMessage' => "Validation failed. 'paths./pet/findByStatus.get.parameters.0.items' "
232+
'exceptionMessage' => "Validation failed. 'paths./pet/findByStatus.get.parameters.0.schema.items' "
233233
. "should have required fields: type.",
234234
],
235235
[
@@ -289,6 +289,16 @@ public static function getConstructorInvalidTmpData(): array
289289
'exception' => InvalidSwaggerSpecException::class,
290290
'exceptionMessage' => "Validation failed. Field 'securityDefinitions.0.in' has an invalid value: invalid. Allowed values: query, header.",
291291
],
292+
[
293+
'tmpDoc' => 'documentation/invalid_format__request_body__invalid_content',
294+
'exception' => InvalidSwaggerSpecException::class,
295+
'exceptionMessage' => "Validation failed. Operation 'paths./users/{id}.post' has body parameters. Only one or the other is allowed.",
296+
],
297+
[
298+
'tmpDoc' => 'documentation/invalid_format__response__invalid_items',
299+
'exception' => InvalidSwaggerSpecException::class,
300+
'exceptionMessage' => "Validation failed. 'paths./users/{id}.post.responses.200.schema.items' should have required fields: type.",
301+
],
292302
];
293303
}
294304

0 commit comments

Comments
 (0)