Skip to content

Commit c3cb988

Browse files
committed
Add custom responses capability
1 parent ae3dba4 commit c3cb988

File tree

12 files changed

+411
-33
lines changed

12 files changed

+411
-33
lines changed

LICENSE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# The MIT License
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ Also includes Swagger UI.
77
This package is heavily inspired by the [darki73/laravel-swagger](https://github.com/darki73/laravel-swagger) and [kevupton/laravel-swagger](https://github.com/kevupton/laravel-swagger).
88
Usage is pretty similar to the [mtrajano/laravel-swagger](https://github.com/mtrajano/laravel-swagger) with the difference being:
99
1. OAS3 support
10-
2. "Custom decorators" inspired by Nest.JS
11-
3. Automatic generation (assuming relevant configuration option is turned on)
12-
4. Inclusion of Swagger UI
13-
5. Models generations
14-
6. Generate operation tags based on route prefix or controller's name
10+
2. Custom decorators
11+
3. Custom responses
12+
4. Automatic generation (assuming relevant configuration option is turned on)
13+
5. Inclusion of Swagger UI
14+
6. Models generations
15+
7. Generate operation tags based on route prefix or controller's name
1516

1617

1718
## Installation
@@ -64,29 +65,58 @@ public function someMethod(Request $request) {}
6465

6566
### @Response() decorator
6667
You can have multiple `@Response` decorators
68+
- The `code` property is required and must be the first in propery
69+
- You can use the optional `description` property to desscribe your response
70+
- You can use the optional `ref` property to refer a model, you can also wrap that model in [] to refer an array of that model or use the full model path inside, finally you can use a schema builder
6771
```php
6872
/**
6973
* @Response({
7074
* code: 200
71-
* description: get user
75+
* description: return user model
7276
* ref: User
7377
* })
7478
* @Response({
75-
* code: 302
76-
* description: Redirect
79+
* code: 400
80+
* description: Bad Request, array of APIError model
81+
* ref: [APIError]
7782
* })
7883
* @Response({
79-
* code: 400
80-
* description: Bad Request
84+
* code: 302
85+
* description: Redirect
8186
* })
8287
* @Response({
8388
* code: 500
8489
* description: Internal Server Error
8590
* })
8691
*/
8792
public function someMethod(Request $request) {}
93+
94+
/**
95+
* You can also refer object directly
96+
*
97+
*
98+
* @Response({
99+
* code: 200
100+
* description: direct user model reference
101+
* ref: #/components/schemas/User
102+
* })
103+
*/
104+
public function someMethod2(Request $request) {}
105+
106+
/**
107+
* Using P schema builder for Laravel Pagination
108+
*
109+
* @Response({
110+
* code: 200
111+
* description: a laravel pagination instance with User model
112+
* ref: P(User)
113+
* })
114+
*/
115+
public function someMethod3(Request $request) {}
88116
```
89117

118+
##### Note: You can see all available schema builder or create your own schema builder, explore swagger.schema_builders config for more informations.
119+
90120
### Custom Validators
91121
These validators are made purely for visual purposes, however, some of them can actually do validation
92122

config/swagger.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
return [
44

5+
/**
6+
* Weather swagger is enabled
7+
* if you set false, this will
8+
* only hide swagger's route
9+
*/
10+
'enable' => env('SWAGGER_ENABLE', false),
11+
512
/**
613
* API Title
714
*/
@@ -143,4 +150,15 @@
143150
'bearerAuth' => 'http',
144151
],
145152

153+
/**
154+
* Schema builder for custom swagger responses.
155+
*
156+
* You can implement your own schema builder, see example in this existing implementation
157+
* Note that Schema builder must implement Mezatsong\SwaggerDocs\Responses\SchemaBuilder
158+
*/
159+
'schema_builders' => [
160+
'P' => \Mezatsong\SwaggerDocs\Responses\SchemaBuilders\LaravelPaginateSchemaBuilder::class,
161+
'SP' => \Mezatsong\SwaggerDocs\Responses\SchemaBuilders\LaravelSimplePaginateSchemaBuilder::class,
162+
]
163+
146164
];

src/Definitions/DefinitionGenerator.php

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
use Illuminate\Support\Collection;
55
use Illuminate\Container\Container;
66
use Illuminate\Support\Facades\DB;
7+
use Illuminate\Support\Facades\Str;
78
use Illuminate\Support\Facades\File;
89
use Illuminate\Support\Facades\Schema;
910
use Illuminate\Database\Eloquent\Model;
11+
use Illuminate\Support\Arr;
1012
use phpDocumentor\Reflection\DocBlockFactory;
1113

1214
/**
@@ -107,6 +109,8 @@ function generateSchemas(): array {
107109
$data['default'] = $default;
108110
}
109111

112+
$this->addExampleKey($data);
113+
110114
$properties[$item] = $data;
111115

112116
if ($column->getNotnull()) {
@@ -115,8 +119,9 @@ function generateSchemas(): array {
115119
}
116120

117121
foreach ($with->getValue($obj) as $item) {
118-
$class = get_class($obj->{$item}()->getModel());
122+
$class = get_class($obj->{$item}()->getModel());
119123
$properties[$item] = [
124+
'type' => 'object',
120125
'$ref' => '#/components/schemas/' . last(explode('\\', $class)),
121126
];
122127
}
@@ -147,4 +152,57 @@ function getModels(): array {
147152
private function getModelName($model): string {
148153
return last(explode('\\', get_class($model)));
149154
}
155+
156+
157+
private function addExampleKey(array & $property): void {
158+
if (Arr::has($property, 'type')) {
159+
switch ($property['type']) {
160+
case 'bigserial':
161+
case 'bigint':
162+
Arr::set($property, 'example', rand(1000000000000000000, 9200000000000000000));
163+
break;
164+
case 'serial':
165+
case 'integer':
166+
Arr::set($property, 'example', rand(1000000000, 2000000000));
167+
break;
168+
case 'mediumint':
169+
Arr::set($property, 'example', rand(1000000, 8000000));
170+
break;
171+
case 'smallint':
172+
Arr::set($property, 'example', rand(10000, 32767));
173+
break;
174+
case 'tinyint':
175+
Arr::set($property, 'example', rand(100, 127));
176+
break;
177+
case 'decimal':
178+
case 'float':
179+
case 'double':
180+
case 'real':
181+
Arr::set($property, 'example', 0.5);
182+
break;
183+
case 'date':
184+
Arr::set($property, 'example', date('Y-m-d'));
185+
break;
186+
case 'time':
187+
Arr::set($property, 'example', date('H:i:s'));
188+
break;
189+
case 'datetime':
190+
Arr::set($property, 'example', date('Y-m-d H:i:s'));
191+
break;
192+
case 'string':
193+
Arr::set($property, 'example', 'string');
194+
break;
195+
case 'text':
196+
Arr::set($property, 'example', 'a long text');
197+
break;
198+
case 'boolean':
199+
Arr::set($property, 'example', rand(0,1) == 0);
200+
break;
201+
202+
default:
203+
# code...
204+
break;
205+
}
206+
}
207+
}
150208
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php namespace Mezatsong\SwaggerDocs\Exceptions;
2+
3+
use Exception;
4+
5+
/**
6+
* Class SchemaBuilderNotFound
7+
* @package Mezatsong\SwaggerDocs\Exceptions
8+
*/
9+
class SchemaBuilderNotFound extends Exception {}

src/Generator.php

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Exception;
44
use Mezatsong\SwaggerDocs\Exceptions\InvalidDefinitionException;
5+
use ReflectionClass;
56
use ReflectionMethod;
67
use ReflectionException;
78
use Illuminate\Support\Arr;
@@ -19,6 +20,7 @@
1920
use Laravel\Passport\Http\Middleware\CheckForAnyScope;
2021
use Mezatsong\SwaggerDocs\Definitions\DefinitionGenerator;
2122
use Mezatsong\SwaggerDocs\Exceptions\InvalidAuthenticationFlow;
23+
use Mezatsong\SwaggerDocs\Exceptions\SchemaBuilderNotFound;
2224

2325
/**
2426
* Class Generator
@@ -256,11 +258,11 @@ private function generatePath(DataObjects\Route $route, string $method, ?string
256258
$actionMethodInstance = $this->getActionMethodInstance($route);
257259
$documentationBlock = $actionMethodInstance ? ($actionMethodInstance->getDocComment() ?: ''): '';
258260

259-
$documentation = $this->parseActionDocumentationBlock($documentationBlock);
261+
$documentation = $this->parseActionDocumentationBlock($documentationBlock, $route->uri());
260262

261263
$this->addActionsParameters($documentation, $route, $method, $actionMethodInstance);
262264

263-
$this->addActionsResponses($documentation, $route, $method, $actionMethodInstance);
265+
$this->addActionsResponses($documentation);
264266

265267
if ($this->hasSecurityDefinitions) {
266268
$this->addActionScopes($documentation, $route);
@@ -318,13 +320,14 @@ private function getActionMethodInstance(DataObjects\Route $route): ?ReflectionM
318320
* @param string $documentationBlock
319321
* @return array
320322
*/
321-
private function parseActionDocumentationBlock(string $documentationBlock): array {
323+
private function parseActionDocumentationBlock(string $documentationBlock, string $uri): array {
322324
$documentation = [
323325
'summary' => '',
324326
'description' => '',
325327
'deprecated' => false,
326328
'responses' => [],
327329
];
330+
328331
if (empty($documentationBlock) || !$this->fromConfig('parse.docBlock', false)) {
329332
return $documentation;
330333
}
@@ -365,25 +368,89 @@ private function parseActionDocumentationBlock(string $documentationBlock): arra
365368
} else if ($key === 'description') {
366369
$documentation['responses'][$responseCode]['description'] = $value;
367370
} else if ($key === 'ref') {
368-
$ref = $value;
369-
if (!Str::startsWith($value, '#/components/schemas/')) {
370-
foreach ($this->definitionGenerator->getModels() as $item) {
371-
if (Str::contains($item, $value)) {
372-
$ref = "#/components/schemas/$value";
373-
}
371+
372+
$value = str_replace(' ', '', $value);
373+
$matches = [];
374+
375+
if (Str::startsWith($value, '[') && Str::endsWith($value, ']')) {
376+
$modelName = trim(Str::replaceFirst('[', '' , Str::replaceLast(']', '' , $value)));
377+
$ref = $this->toSwaggerModelPath($modelName);
378+
$items = [
379+
'type' => 'array',
380+
'items' => [
381+
'type' => 'object',
382+
'$ref' => $ref
383+
]
384+
];
385+
$documentation['responses'][$responseCode]['content']['application/json']['schema'] = $items;
386+
} else if (preg_match("(([A-Za-z]{1,})\(([A-Za-z]{1,})\))", $value, $matches) === 1) {
387+
$schema = $this->generateCustomResponseSchema(
388+
$matches[1],
389+
$matches[2],
390+
$uri
391+
);
392+
if (\count($schema) > 0) {
393+
$documentation['responses'][$responseCode]['content']['application/json']['schema'] = $schema;
374394
}
395+
} else {
396+
$ref = $this->toSwaggerModelPath($value);
397+
$documentation['responses'][$responseCode]['content']['application/json']['schema']['$ref'] = $ref;
375398
}
376-
$documentation['responses'][$responseCode]['content']['application/json']['schema']['$ref'] = $ref;
399+
377400
}
378401
}
379402
}
380403
}
381404

382-
383-
return $documentation;
384405
} catch (Exception $exception) {
385-
return $documentation;
406+
if ($exception instanceof SchemaBuilderNotFound) {
407+
throw $exception;
408+
}
409+
}
410+
411+
return $documentation;
412+
}
413+
414+
415+
/**
416+
* Read schemas builder config and call the matched one
417+
*
418+
* @throws SchemaBuilderNotFound
419+
*/
420+
private function generateCustomResponseSchema(string $operation, string $model, string $uri) {
421+
$ref = $this->toSwaggerModelPath($model);
422+
$schemaBuilders = $this->fromConfig('schema_builders');
423+
424+
if (!Arr::has($schemaBuilders, $operation)) {
425+
throw new SchemaBuilderNotFound("schema builder $operation not found in swagger.schema_builders config");
386426
}
427+
428+
$actionClass = new ReflectionClass($schemaBuilders[$operation]);
429+
430+
try {
431+
return $actionClass->newInstanceWithoutConstructor()->build($ref, $uri);
432+
} catch(Exception $e) {
433+
throw new Exception("SchemaBuilder must implements Mezatsong\SwaggerDocs\Responses\SchemaBuilder interface");
434+
}
435+
}
436+
437+
438+
/**
439+
* Turn a model name to swagger path to that model or
440+
* return the same string if it's already a valide path
441+
* @param string $value
442+
* @return string
443+
*/
444+
private function toSwaggerModelPath(string $value): string {
445+
if (!Str::startsWith($value, '#/components/schemas/')) {
446+
foreach ($this->definitionGenerator->getModels() as $item) {
447+
if (Str::endsWith($item, $value)) {
448+
return "#/components/schemas/$value";
449+
}
450+
}
451+
}
452+
453+
return $value;
387454
}
388455

389456

@@ -394,9 +461,7 @@ private function parseActionDocumentationBlock(string $documentationBlock): arra
394461
* @param string $method
395462
* @param ReflectionMethod|null $actionInstance
396463
*/
397-
private function addActionsResponses(array & $information, DataObjects\Route $route, string $method, ?ReflectionMethod $actionInstance): void {
398-
399-
464+
private function addActionsResponses(array & $information): void {
400465

401466
if (\count(Arr::get($information, 'responses')) === 0) {
402467
Arr::set($information, 'responses', [

src/Parameters/BodyParametersGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ protected function getNestedParameterType(array $nameTokens): string {
130130
*/
131131
protected function createNewPropertyObject(string $type, array $rules): array {
132132
$propertyObject = [
133-
'type' => $type,
133+
'type' => $type,
134134
];
135135

136136
if ($enums = $this->getEnumValues($rules)) {

0 commit comments

Comments
 (0)