Skip to content

Commit

Permalink
inferring schema name from validation rules
Browse files Browse the repository at this point in the history
  • Loading branch information
romalytvynenko committed Jun 6, 2024
1 parent 008dcef commit fa06f0d
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 53 deletions.
21 changes: 18 additions & 3 deletions src/Infer/Visitors/PhpDocResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,26 @@ public function enterNode(Node $node)
$doc = new Doc("/** $docText */");
}

if ($doc) {
$node->setAttribute('parsedPhpDoc', $this->parseDocs($doc));
if (! $doc) {
return null;
}

$parsedDoc = $this->parseDocs($doc);

$node->setAttribute('parsedPhpDoc', $parsedDoc);

/*
* Parsed doc is propagated to the child expressions nodes, so it is easier for the consumer
* to get to the php doc when needed. For example, when some method call is annotated with phpdoc,
* we'd want to get this doc from the method call node, not an expression one.
*/
if ($node instanceof Node\Stmt\Expression) {
$node->expr->setAttribute('parsedPhpDoc', $parsedDoc);
}

return null;
if ($node instanceof Node\Stmt\Expression && $node->expr instanceof Expr\Assign) {
$node->expr->expr->setAttribute('parsedPhpDoc', $parsedDoc);
}
}

private function parseDocs(Doc $doc)
Expand Down
110 changes: 70 additions & 40 deletions src/Support/OperationExtensions/RequestBodyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Dedoc\Scramble\Extensions\OperationExtension;
use Dedoc\Scramble\Support\Generator\Operation;
use Dedoc\Scramble\Support\Generator\Parameter;
use Dedoc\Scramble\Support\Generator\Reference;
use Dedoc\Scramble\Support\Generator\RequestBodyObject;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types\ObjectType;
Expand Down Expand Up @@ -32,46 +33,9 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
*/
$routeInfo->getMethodType();

[$bodyParams, $schemaName] = [[], null];
try {
$bodyParams = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode());

$bodyParamsNames = array_map(fn ($p) => $p->name, $bodyParams);

$allParams = [
...$bodyParams,
...array_filter(
array_values($routeInfo->requestParametersFromCalls->data),
fn ($p) => ! in_array($p->name, $bodyParamsNames),
),
];
[$queryParams, $bodyParams] = collect($allParams)
->partition(function (Parameter $parameter) {
return $parameter->getAttribute('isInQuery');
});
$queryParams = $queryParams->toArray();
$bodyParams = $bodyParams->toArray();

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

if (count($allParams)) {
if (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
$operation->addRequestBodyObject(
RequestBodyObject::make()->setContent($mediaType, Schema::createFromParameters($bodyParams))
);
} else {
$operation->addParameters($bodyParams);
}
$operation->addParameters($queryParams);
} elseif (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
$operation
->addRequestBodyObject(
RequestBodyObject::make()
->setContent(
$mediaType,
Schema::fromType(new ObjectType)
)
);
}
[$bodyParams, $schemaName] = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode());
} catch (Throwable $exception) {
if (app()->environment('testing')) {
throw $exception;
Expand All @@ -82,6 +46,69 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
$operation
->summary(Str::of($routeInfo->phpDoc()->getAttribute('summary'))->rtrim('.'))
->description($description);

$bodyParamsNames = array_map(fn ($p) => $p->name, $bodyParams);

$allParams = [
...$bodyParams,
...array_filter(
array_values($routeInfo->requestParametersFromCalls->data),
fn ($p) => ! in_array($p->name, $bodyParamsNames),
),
];
[$queryParams, $bodyParams] = collect($allParams)
->partition(function (Parameter $parameter) {
return $parameter->getAttribute('isInQuery');
});
$queryParams = $queryParams->toArray();
$bodyParams = $bodyParams->toArray();

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

if (empty($allParams)) {
if (! in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
$operation
->addRequestBodyObject(
RequestBodyObject::make()->setContent($mediaType, Schema::fromType(new ObjectType))
);
}

return;
}

$operation->addParameters($queryParams);
if (in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
$operation->addParameters($bodyParams);
return;
}

$this->addRequestBody(
$operation,
$mediaType,
Schema::createFromParameters($bodyParams),
$schemaName,
);
}

protected function addRequestBody(Operation $operation, string $mediaType, Schema $requestBodySchema, ?string $schemaName)
{
if (! $schemaName) {
$operation->addRequestBodyObject(RequestBodyObject::make()->setContent($mediaType, $requestBodySchema));

return;
}

$components = $this->openApiTransformer->getComponents();
if (! $components->hasSchema($schemaName)) {
$components->addSchema($schemaName, $requestBodySchema);
}

$operation->addRequestBodyObject(
RequestBodyObject::make()->setContent(
$mediaType,
new Reference('schemas', $schemaName, $components),
)
);
}

protected function getMediaType(Operation $operation, RouteInfo $routeInfo, array $bodyParams): string
Expand Down Expand Up @@ -116,7 +143,10 @@ protected function extractParamsFromRequestValidationRules(Route $route, ?ClassM
{
[$rules, $nodesResults] = $this->extractRouteRequestValidationRules($route, $methodNode);

return (new RulesToParameters($rules, $nodesResults, $this->openApiTransformer))->handle();
return [
(new RulesToParameters($rules, $nodesResults, $this->openApiTransformer))->handle(),
$nodesResults[0]->schemaName ?? null,
];
}

protected function extractRouteRequestValidationRules(Route $route, $methodNode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

use Dedoc\Scramble\Support\PhpDoc;
use Illuminate\Http\Request;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;

class ValidateCallExtractor
{
Expand Down Expand Up @@ -71,6 +73,7 @@ public function node(): ?ValidationNodesResult

return new ValidationNodesResult(
$validationRules instanceof Node\Arg ? $validationRules->value : $validationRules,
($callToValidate->getAttribute('parsedPhpDoc', new PhpDocNode([])))->getTagsByName('@schemaName')[0]->value->value ?? null,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

class ValidationNodesResult
{
public $node;

public function __construct($node)
{
$this->node = $node;
}
public function __construct(
public $node,
public ?string $schemaName,
)
{}
}
12 changes: 8 additions & 4 deletions tests/Generator/Request/ReusableSchemaNamesTest.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<?php

// @todo: move the tests to Support/... file (corresponding to the file being tested)

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

it('makes reusable request body from marked validation rules', function () {
$document = generateForRoute(function () {
return Route::get('test', Validation_ReusableSchemaNamesTest_Controller::class);
return Route::post('test', Validation_ReusableSchemaNamesTest_Controller::class);
});

// assert document has request body and a reference to it
expect($document)->toHaveKey('components.schemas.FooObject')
->and($document['paths']['/test']['post']['requestBody']['content']['application/json']['schema'])
->toBe(['$ref' => '#/components/schemas/FooObject']);
});
class Validation_ReusableSchemaNamesTest_Controller
{
Expand All @@ -17,13 +21,13 @@ public function __invoke(Request $request)
/**
* @schemaName FooObject
*/
$request->validate(['foo' => 'integer']);
$data = $request->validate(['foo' => 'integer']);
}
}

it('makes reusable request body from form request', function () {
$document = generateForRoute(function () {
return Route::get('test', FormRequest_ReusableSchemaNamesTest_Controller::class);
return Route::post('test', FormRequest_ReusableSchemaNamesTest_Controller::class);
});

// assert document has request body and a reference to it
Expand Down

0 comments on commit fa06f0d

Please sign in to comment.