Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/views/docs.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
const csrfToken = getCookieValue(CSRF_TOKEN_COOKIE_KEY);
if (csrfToken) {
const { headers = new Headers() } = options || {};
updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURI(csrfToken));
updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURIComponent(csrfToken));
return originalFetch(url, {
...options,
headers,
Expand Down
4 changes: 4 additions & 0 deletions src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
use Dedoc\Scramble\Support\OperationExtensions\ResponseExtension;
use Dedoc\Scramble\Support\ServerFactory;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\AnonymousResourceCollectionTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\CursorPaginatorTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\EloquentCollectionToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\EnumToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\JsonResourceTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\LengthAwarePaginatorTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ModelToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\PaginatorTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResponseTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\VoidTypeToSchema;
use PhpParser\ParserFactory;
Expand Down Expand Up @@ -179,6 +181,8 @@ public function configurePackage(Package $package): void
ModelToSchema::class,
EloquentCollectionToSchema::class,
AnonymousResourceCollectionTypeToSchema::class,
CursorPaginatorTypeToSchema::class,
PaginatorTypeToSchema::class,
LengthAwarePaginatorTypeToSchema::class,
ResponseTypeToSchema::class,
VoidTypeToSchema::class,
Expand Down
3 changes: 1 addition & 2 deletions src/Support/Generator/TypeTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,7 @@ public function transform(Type $type)
$openApiType->examples($examples);
}

if ($default = ExamplesExtractor::make($docNode, '@default')
->extract(preferString: $openApiType instanceof StringType)) {
if ($default = ExamplesExtractor::make($docNode, '@default')->extract(preferString: $openApiType instanceof StringType)) {
$openApiType->default($default[0]);
}

Expand Down
171 changes: 103 additions & 68 deletions src/Support/OperationExtensions/RequestBodyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
namespace Dedoc\Scramble\Support\OperationExtensions;

use Dedoc\Scramble\Extensions\OperationExtension;
use Dedoc\Scramble\Support\Generator\Combined\AllOf;
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;
use Dedoc\Scramble\Support\Generator\Types\Type;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\FormRequestRulesExtractor;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\RulesToParameters;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ParametersExtractionResult;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\RequestMethodCallsExtractor;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ValidateCallExtractor;
use Dedoc\Scramble\Support\RouteInfo;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use PhpParser\Node\Stmt\ClassMethod;
use Throwable;

class RequestBodyExtension extends OperationExtension
Expand All @@ -33,35 +35,26 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
*/
$routeInfo->getMethodType();

[$bodyParams, $schemaName, $schemaDescription] = [[], null, null];
$rulesResults = collect();

try {
[$bodyParams, $schemaName, $schemaDescription] = $this->extractParamsFromRequestValidationRules($routeInfo->route, $routeInfo->methodNode());
$rulesResults = collect($this->extractRouteRequestValidationRules($routeInfo, $routeInfo->methodNode()));
} catch (Throwable $exception) {
if (app()->environment('testing')) {
throw $exception;
}
$description = $description->append('⚠️Cannot generate request documentation: '.$exception->getMessage());
$description = $description->append('⚠️Cannot generate request documentation: ' . $exception->getMessage());
}

$operation
->summary(Str::of($routeInfo->phpDoc()->getAttribute('summary'))->rtrim('.'))
->description($description);

$bodyParamsNames = array_map(fn ($p) => $p->name, $bodyParams);
$allParams = $rulesResults->flatMap->parameters->unique('name')->values()->all();

$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();
->partition(fn(Parameter $p) => $p->getAttribute('isInQuery'))
->map->toArray();

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

Expand All @@ -77,39 +70,66 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
return;
}

$this->addRequestBody(
$operation,
$mediaType,
Schema::createFromParameters($bodyParams),
$schemaName,
$schemaDescription,
[$schemaResults, $schemalessResults] = $rulesResults->partition('schemaName');
$schemalessResults = collect([$this->mergeSchemalessRulesResults($schemalessResults->values())]);

$schemas = $schemaResults->merge($schemalessResults)
->filter(fn(ParametersExtractionResult $r) => count($r->parameters) || $r->schemaName)
->map(function (ParametersExtractionResult $r) use ($queryParams) {
$qpNames = collect($queryParams)->keyBy('name');

$r->parameters = collect($r->parameters)->filter(fn($p) => ! $qpNames->has($p->name))->values()->all();

return $r;
})
->values()
->map($this->makeSchemaFromResults(...));

if ($schemas->isEmpty()) {
return;
}

$schema = $this->makeComposedRequestBodySchema($schemas);
if (! $schema instanceof Reference) {
$schema = Schema::fromType($schema);
}

$operation->addRequestBodyObject(
RequestBodyObject::make()->setContent($mediaType, $schema),
);
}

protected function addRequestBody(Operation $operation, string $mediaType, Schema $requestBodySchema, ?string $schemaName, ?string $schemaDescription)
protected function makeSchemaFromResults(ParametersExtractionResult $result): Type
{
if (! $schemaName) {
$operation->addRequestBodyObject(RequestBodyObject::make()->setContent($mediaType, $requestBodySchema));
$requestBodySchema = Schema::createFromParameters($result->parameters);

return;
if (! $result->schemaName) {
return $requestBodySchema->type;
}

$components = $this->openApiTransformer->getComponents();
if (! $components->hasSchema($schemaName)) {
$requestBodySchema->type->setDescription($schemaDescription ?: '');
if (! $components->hasSchema($result->schemaName)) {
$requestBodySchema->type->setDescription($result->description ?: '');

$components->addSchema($schemaName, $requestBodySchema);
$components->addSchema($result->schemaName, $requestBodySchema);
}

$operation->addRequestBodyObject(
RequestBodyObject::make()->setContent(
$mediaType,
app(Reference::class, [
'referenceType' => 'schemas',
'fullName' => $schemaName,
'components' => $components,
]),
)
return new Reference('schemas', $result->schemaName, $components);
}

protected function makeComposedRequestBodySchema(Collection $schemas)
{
if ($schemas->count() === 1) {
return $schemas->first();
}

return (new AllOf())->setItems($schemas->all());
}

protected function mergeSchemalessRulesResults(Collection $schemalessResults): ParametersExtractionResult
{
return new ParametersExtractionResult(
parameters: $schemalessResults->values()->flatMap->parameters->unique('name')->values()->all(),
);
}

Expand Down Expand Up @@ -141,37 +161,52 @@ protected function hasBinary($bodyParams): bool
});
}

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

return [
(new RulesToParameters($rules, $nodesResults, $this->openApiTransformer))->handle(),
$nodesResults[0]->schemaName ?? null,
$nodesResults[0]->description ?? null,
/*
* These are the extractors that are getting types from the validation rules, so it is
* certain that a property must have the extracted type.
*/
$typeDefiningHandlers = [
new FormRequestRulesExtractor($methodNode, $this->openApiTransformer),
new ValidateCallExtractor(
$methodNode,
$this->openApiTransformer,
$routeInfo->route,
),
];
}

protected function extractRouteRequestValidationRules(Route $route, $methodNode)
{
$rules = [];
$nodesResults = [];

// Custom form request's class `validate` method
if (($formRequestRulesExtractor = new FormRequestRulesExtractor($methodNode))->shouldHandle()) {
if (count($formRequestRules = $formRequestRulesExtractor->extract($route))) {
$rules = array_merge($rules, $formRequestRules);
$nodesResults[] = $formRequestRulesExtractor->node();
}
}
$validationRulesExtractedResults = collect($typeDefiningHandlers)
->filter(static fn($h) => $h->shouldHandle())
->map(static fn($h) => $h->extract($routeInfo))
->values()
->toArray();

if (($validateCallExtractor = new ValidateCallExtractor($methodNode, $route))->shouldHandle()) {
if ($validateCallRules = $validateCallExtractor->extract()) {
$rules = array_merge($rules, $validateCallRules);
$nodesResults[] = $validateCallExtractor->node();
}
}
/*
* This is the extractor that cannot re-define the incoming type but can add new properties.
* Also, it is useful for additional details.
*/
$detailsExtractor = new RequestMethodCallsExtractor();

$methodCallsExtractedResults = $detailsExtractor->extract($routeInfo);

return $this->mergeExtractedProperties($validationRulesExtractedResults, $methodCallsExtractedResults);
}

return [$rules, array_filter($nodesResults)];
/**
* @param ParametersExtractionResult[] $rulesExtractedResults
*/
protected function mergeExtractedProperties(
array $rulesExtractedResults,
ParametersExtractionResult $methodCallsExtractedResult,
) {
$rulesParameters = collect($rulesExtractedResults)->flatMap->parameters->keyBy('name');

$methodCallsExtractedResult->parameters = collect($methodCallsExtractedResult->parameters)
->filter(fn(Parameter $p) => ! $rulesParameters->has($p->name))
->values()
->all();

return [...$rulesExtractedResults, $methodCallsExtractedResult];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

use Dedoc\Scramble\Infer;
use Dedoc\Scramble\Support\Generator\TypeTransformer;
use Dedoc\Scramble\Support\RouteInfo;
use Dedoc\Scramble\Support\SchemaClassDocReflector;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
Expand All @@ -14,16 +16,13 @@
use ReflectionClass;
use Spatie\LaravelData\Contracts\BaseData;

class FormRequestRulesExtractor
class FormRequestRulesExtractor implements RulesExtractor
{
private ?FunctionLike $handler;
use GeneratesParametersFromRules;

public function __construct(?FunctionLike $handler)
{
$this->handler = $handler;
}
public function __construct(private ?FunctionLike $handler, private TypeTransformer $typeTransformer) {}

public function shouldHandle()
public function shouldHandle(): bool
{
if (! $this->handler) {
return false;
Expand All @@ -42,7 +41,7 @@ public function shouldHandle()
return true;
}

public function node()
public function extract(RouteInfo $routeInfo): ParametersExtractionResult
{
$requestClassName = $this->getFormRequestClassName();

Expand All @@ -54,19 +53,23 @@ public function node()
? null
: $phpDocReflector->getSchemaName($requestClassName);

return new ValidationNodesResult(
(new NodeFinder)->find(
Arr::wrap($classReflector->getMethod('rules')->getAstNode()->stmts),
fn (Node $node) => $node instanceof Node\Expr\ArrayItem
&& $node->key instanceof Node\Scalar\String_
&& $node->getAttribute('parsedPhpDoc'),
return new ParametersExtractionResult(
parameters: $this->makeParameters(
node: (new NodeFinder)->find(
Arr::wrap($classReflector->getMethod('rules')->getAstNode()->stmts),
fn (Node $node) => $node instanceof Node\Expr\ArrayItem
&& $node->key instanceof Node\Scalar\String_
&& $node->getAttribute('parsedPhpDoc'),
),
rules: $this->rules($routeInfo->route),
typeTransformer: $this->typeTransformer,
),
schemaName: $schemaName,
description: $phpDocReflector->getDescription(),
);
}

public function extract(Route $route)
protected function rules(Route $route)
{
$requestClassName = $this->getFormRequestClassName();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

use Dedoc\Scramble\Support\Generator\TypeTransformer;

trait GeneratesParametersFromRules
{
private function makeParameters($node, $rules, TypeTransformer $typeTransformer)
{
return (new RulesToParameters($rules, $node, $typeTransformer))->handle();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

class ValidationNodesResult
/**
* @internal
*/
class ParametersExtractionResult
{
public function __construct(
public $node,
public array $parameters,
public ?string $schemaName = null,
public ?string $description = null,
) {}
Expand Down
Loading