Skip to content

Commit

Permalink
Merge pull request #232 from dedoc/pr/WildEgo-main
Browse files Browse the repository at this point in the history
Image and file validator support, request @mediaType tag support
  • Loading branch information
romalytvynenko authored Sep 24, 2023
2 parents 814a36c + 0bfd8fc commit a7e02cb
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 10 deletions.
51 changes: 42 additions & 9 deletions src/Support/OperationExtensions/RequestBodyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Dedoc\Scramble\Extensions\OperationExtension;
use Dedoc\Scramble\Support\Generator\Operation;
use Dedoc\Scramble\Support\Generator\Parameter;
use Dedoc\Scramble\Support\Generator\RequestBodyObject;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types\ObjectType;
Expand All @@ -12,6 +13,7 @@
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ValidateCallExtractor;
use Dedoc\Scramble\Support\RouteInfo;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use PhpParser\Node\Stmt\ClassMethod;
use Throwable;
Expand All @@ -20,25 +22,27 @@ class RequestBodyExtension extends OperationExtension
{
public function handle(Operation $operation, RouteInfo $routeInfo)
{
$method = $operation->method;

$description = Str::of($routeInfo->phpDoc()->getAttribute('description'));

try {
if (count($bodyParams = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode()))) {
if ($method !== 'get') {
$bodyParams = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode());

$mediaType = $this->getMediaType($operation, $routeInfo, $bodyParams);

if (count($bodyParams)) {
if ($operation->method !== 'get') {
$operation->addRequestBodyObject(
RequestBodyObject::make()->setContent('application/json', Schema::createFromParameters($bodyParams))
RequestBodyObject::make()->setContent($mediaType, Schema::createFromParameters($bodyParams))
);
} else {
$operation->addParameters($bodyParams);
}
} elseif ($method !== 'get') {
} elseif ($operation->method !== 'get') {
$operation
->addRequestBodyObject(
RequestBodyObject::make()
->setContent(
'application/json',
$mediaType,
Schema::fromType(new ObjectType)
)
);
Expand All @@ -55,14 +59,43 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
->description($description);
}

private function extractParamsFromRequestValidationRules(Route $route, ?ClassMethod $methodNode)
protected function getMediaType(Operation $operation, RouteInfo $routeInfo, array $bodyParams): string
{
if (
($mediaTags = $routeInfo->phpDoc()->getTagsByName('@requestMediaType'))
&& ($mediaType = trim(Arr::first($mediaTags)?->value?->value))
) {
return $mediaType;
}

$jsonMediaType = 'application/json';

if ($operation->method === 'get') {
return $jsonMediaType;
}

return $this->hasBinary($bodyParams) ? 'multipart/form-data' : $jsonMediaType;
}

protected function hasBinary($bodyParams): bool
{
return collect($bodyParams)->contains(function (Parameter $parameter) {
if (property_exists($parameter?->schema?->type, 'format')) {
return $parameter->schema->type->format === 'binary';
}

return false;
});
}

protected function extractParamsFromRequestValidationRules(Route $route, ?ClassMethod $methodNode)
{
[$rules, $nodesResults] = $this->extractRouteRequestValidationRules($route, $methodNode);

return (new RulesToParameters($rules, $nodesResults, $this->openApiTransformer))->handle();
}

private function extractRouteRequestValidationRules(Route $route, $methodNode)
protected function extractRouteRequestValidationRules(Route $route, $methodNode)
{
$rules = [];
$nodesResults = [];
Expand Down
14 changes: 14 additions & 0 deletions src/Support/OperationExtensions/RulesExtractor/RulesMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,18 @@ public function enum(Type $_, Enum $rule)
new ObjectType($enumName)
);
}

public function image(Type $type)
{
return $this->file($type);
}

public function file(Type $type)
{
if ($type instanceof UnknownType) {
$type = $this->string($type);
}

return $type->format('binary');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class RulesToParameter
private ?PhpDocNode $docNode;

const RULES_PRIORITY = [
'bool', 'boolean', 'numeric', 'int', 'integer', 'string', 'array', 'exists',
'bool', 'boolean', 'numeric', 'int', 'integer', 'file', 'image', 'string', 'array', 'exists',
];

public function __construct(string $name, $rules, ?PhpDocNode $docNode, TypeTransformer $openApiTransformer)
Expand Down
57 changes: 57 additions & 0 deletions tests/Support/OperationExtensions/RequestBodyExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

use Illuminate\Support\Facades\Route as RouteFacade;

it('uses application/json media type as a default request media type', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::post('api/test', [RequestBodyExtensionTest__uses_application_json_as_default::class, 'index']);
});

expect($openApiDocument['paths']['/test']['post']['requestBody']['content'])
->toHaveKey('application/json')
->toHaveLength(1);
});
class RequestBodyExtensionTest__uses_application_json_as_default
{
public function index(Illuminate\Http\Request $request)
{
$request->validate(['foo' => 'string']);
}
}

it('allows manually defining a request media type', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::post('api/test', [RequestBodyExtensionTest__allows_manual_request_media_type::class, 'index']);
});

expect($openApiDocument['paths']['/test']['post']['requestBody']['content'])
->toHaveKey('application/xml')
->toHaveLength(1);
});
class RequestBodyExtensionTest__allows_manual_request_media_type
{
/**
* @requestMediaType application/xml
*/
public function index(Illuminate\Http\Request $request)
{
$request->validate(['foo' => 'string']);
}
}

it('automatically infers multipart/form-data as request media type when some of body params is binary', function () {
$openApiDocument = generateForRoute(function () {
return RouteFacade::post('api/test', [RequestBodyExtensionTest__automaticall_infers_form_data::class, 'index']);
});

expect($openApiDocument['paths']['/test']['post']['requestBody']['content'])
->toHaveKey('multipart/form-data')
->toHaveLength(1);
});
class RequestBodyExtensionTest__automaticall_infers_form_data
{
public function index(Illuminate\Http\Request $request)
{
$request->validate(['foo' => 'file']);
}
}
22 changes: 22 additions & 0 deletions tests/ValidationRulesDocumentingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@
->and($type->format)->toBe('email');
});

it('supports image rule', function () {
$rules = [
'image' => 'required|image',
];

$type = app()->make(RulesToParameters::class, ['rules' => $rules])->handle()[0]->schema->type;

expect($type)->toBeInstanceOf(StringType::class)
->and($type->format)->toBe('binary');
});

it('supports file rule', function () {
$rules = [
'file' => 'required|file',
];

$type = app()->make(RulesToParameters::class, ['rules' => $rules])->handle()[0]->schema->type;

expect($type)->toBeInstanceOf(StringType::class)
->and($type->format)->toBe('binary');
});

it('extracts rules from request->validate call', function () {
RouteFacade::get('api/test', [ValidationRulesDocumenting_Test::class, 'index']);

Expand Down

0 comments on commit a7e02cb

Please sign in to comment.