Skip to content

Commit

Permalink
Improved merging of dot-notated params originated from method calls (#…
Browse files Browse the repository at this point in the history
…583)

* marked method calls

* fixed issue with nester rules inference

* converting dot notated form request fields

* not merging request params optioally

* Fix styling

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Oct 17, 2024
1 parent 9193ff1 commit b52caaf
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 185 deletions.
19 changes: 18 additions & 1 deletion src/Support/IndexBuilders/RequestParametersBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public function afterAnalyzedNode(Scope $scope, Node $node): void
'boolean' => $this->makeBooleanParameter($scope, $methodCallNode),
'enum' => $this->makeEnumParameter($scope, $methodCallNode),
'query' => $this->makeQueryParameter($scope, $methodCallNode, $parameter),
'string', 'str', 'get', 'input', 'post' => $this->makeStringParameter($scope, $methodCallNode),
'string', 'str', 'input' => $this->makeStringParameter($scope, $methodCallNode),
'get', 'post' => $this->makeFlatParameter($scope, $methodCallNode),
default => [null, null],
};

Expand All @@ -99,6 +100,10 @@ public function afterAnalyzedNode(Scope $scope, Node $node): void
->default($parameterDefault ?? new MissingExample)
));

if ($parameterType->getAttribute('isFlat')) {
$parameter->setAttribute('isFlat', true);
}

$this->bag->set($parameterName, $parameter);
}

Expand Down Expand Up @@ -148,6 +153,18 @@ private function makeStringParameter(Scope $scope, Node $node)
];
}

private function makeFlatParameter(Scope $scope, Node $node)
{
$type = new StringType;

$type->setAttribute('isFlat', true);

return [
$type,
TypeHelper::getArgType($scope, $node->args, ['default', 1])->value ?? null,
];
}

private function makeEnumParameter(Scope $scope, Node $node)
{
if (! $className = TypeHelper::getArgType($scope, $node->args, ['default', 1])->value ?? null) {
Expand Down
27 changes: 19 additions & 8 deletions src/Support/OperationExtensions/RequestBodyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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\DeepParametersMerger;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\FormRequestRulesExtractor;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\ParametersExtractionResult;
use Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\RequestMethodCallsExtractor;
Expand Down Expand Up @@ -53,23 +54,26 @@ public function handle(Operation $operation, RouteInfo $routeInfo)

$allParams = $rulesResults->flatMap->parameters->unique('name')->values()->all();

[$queryParams, $bodyParams] = collect($allParams)
->partition(fn (Parameter $p) => $p->getAttribute('isInQuery'))
->map->toArray();

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

if (empty($allParams)) {
return;
}

$operation->addParameters($queryParams);
if (in_array($operation->method, static::HTTP_METHODS_WITHOUT_REQUEST_BODY)) {
$operation->addParameters($bodyParams);
$operation->addParameters(
$this->convertDotNamedParamsToComplexStructures($allParams)
);

return;
}

[$queryParams, $bodyParams] = collect($allParams)
->partition(fn (Parameter $p) => $p->getAttribute('isInQuery'))
->map->toArray();

$operation->addParameters($this->convertDotNamedParamsToComplexStructures($queryParams));

[$schemaResults, $schemalessResults] = $rulesResults->partition('schemaName');
$schemalessResults = collect([$this->mergeSchemalessRulesResults($schemalessResults->values())]);

Expand Down Expand Up @@ -118,7 +122,9 @@ protected function isSchemaRequired(Reference|Schema $schema): bool

protected function makeSchemaFromResults(ParametersExtractionResult $result): Type
{
$requestBodySchema = Schema::createFromParameters($result->parameters);
$requestBodySchema = Schema::createFromParameters(
$this->convertDotNamedParamsToComplexStructures($result->parameters),
);

if (! $result->schemaName) {
return $requestBodySchema->type;
Expand Down Expand Up @@ -146,10 +152,15 @@ protected function makeComposedRequestBodySchema(Collection $schemas)
protected function mergeSchemalessRulesResults(Collection $schemalessResults): ParametersExtractionResult
{
return new ParametersExtractionResult(
parameters: $schemalessResults->values()->flatMap->parameters->unique('name')->values()->all(),
parameters: $this->convertDotNamedParamsToComplexStructures($schemalessResults->values()->flatMap->parameters->unique('name')->values()->all()),
);
}

protected function convertDotNamedParamsToComplexStructures($params)
{
return (new DeepParametersMerger(collect($params)))->handle();
}

protected function getMediaType(Operation $operation, RouteInfo $routeInfo, array $bodyParams): string
{
if (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

use Dedoc\Scramble\Support\Generator\Parameter;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types\ArrayType;
use Dedoc\Scramble\Support\Generator\Types\ObjectType;
use Dedoc\Scramble\Support\Generator\Types\Type;
use Dedoc\Scramble\Support\Generator\Types\UnknownType;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class DeepParametersMerger
{
public function __construct(private Collection $parameters) {}

public function handle()
{
return $this->handleNested($this->parameters->keyBy('name'))
->values()
->all();
}

private function handleNested(Collection $parameters)
{
[$forcedFlatParameters, $maybeDeepParameters] = $parameters->partition(fn (Parameter $p) => $p->getAttribute('isFlat') === true);

[$nested, $parameters] = $maybeDeepParameters
->sortBy(fn ($_, $key) => count(explode('.', $key)))
->partition(fn ($_, $key) => Str::contains($key, '.'));

$nestedParentsKeys = $nested->keys()->map(fn ($key) => explode('.', $key)[0]);

[$nestedParents, $parameters] = $parameters->partition(fn ($_, $key) => $nestedParentsKeys->contains($key));

/** @var Collection $nested */
$nested = $nested->merge($nestedParents);

$nested = $nested
->groupBy(fn ($_, $key) => explode('.', $key)[0])
->map(function (Collection $params, $groupName) {
$params = $params->keyBy('name');

$baseParam = $params->get(
$groupName,
Parameter::make($groupName, $params->first()->in)
->setSchema(Schema::fromType(
$params->keys()->contains(fn ($k) => Str::contains($k, "$groupName.*"))
? new ArrayType
: new ObjectType
))
);

$params->offsetUnset($groupName);

foreach ($params as $param) {
$this->setDeepType(
$baseParam->schema->type,
$param->name,
$this->extractTypeFromParameter($param),
);
}

return $baseParam;
});

return $parameters
->merge($forcedFlatParameters)
->merge($nested);
}

private function setDeepType(Type &$base, string $key, Type $typeToSet)
{
$containingType = $this->getOrCreateDeepTypeContainer(
$base,
collect(explode('.', $key))->splice(1)->values()->all(),
);

if (! $containingType) {
return;
}

$isSettingArrayItems = ($settingKey = collect(explode('.', $key))->last()) === '*';

if ($containingType === $base && $base instanceof UnknownType) {
$containingType = ($isSettingArrayItems ? new ArrayType : new ObjectType)
->addProperties($base);

$base = $containingType;
}

if (! ($containingType instanceof ArrayType || $containingType instanceof ObjectType)) {
return;
}

if ($isSettingArrayItems && $containingType instanceof ArrayType) {
$containingType->items = $typeToSet;

return;
}

if (! $isSettingArrayItems && $containingType instanceof ObjectType) {
$containingType
->addProperty($settingKey, $typeToSet)
->addRequired($typeToSet->getAttribute('required') ? [$settingKey] : []);
}
}

private function getOrCreateDeepTypeContainer(Type &$base, array $path)
{
$key = $path[0];

if (count($path) === 1) {
if ($key !== '*' && $base instanceof ArrayType) {
$base = new ObjectType;
}

return $base;
}

if ($key === '*') {
if (! $base instanceof ArrayType) {
$base = new ArrayType;
}

$next = $path[1];
if ($next === '*') {
if (! $base->items instanceof ArrayType) {
$base->items = new ArrayType;
}
} else {
if (! $base->items instanceof ObjectType) {
$base->items = new ObjectType;
}
}

return $this->getOrCreateDeepTypeContainer(
$base->items,
collect($path)->splice(1)->values()->all(),
);
} else {
if (! $base instanceof ObjectType) {
$base = new ObjectType;
}

$next = $path[1];

if (! $base->hasProperty($key)) {
$base = $base->addProperty(
$key,
$next === '*' ? new ArrayType : new ObjectType,
);
}
if (($existingType = $base->getProperty($key)) instanceof UnknownType) {
$base = $base->addProperty(
$key,
($next === '*' ? new ArrayType : new ObjectType)->addProperties($existingType),
);
}

if ($next === '*' && ! $existingType instanceof ArrayType) {
$base->addProperty($key, (new ArrayType)->addProperties($existingType));
}
if ($next !== '*' && $existingType instanceof ArrayType) {
$base->addProperty($key, (new ObjectType)->addProperties($existingType));
}

return $this->getOrCreateDeepTypeContainer(
$base->properties[$key],
collect($path)->splice(1)->values()->all(),
);
}
}

private function extractTypeFromParameter($parameter)
{
$paramType = $parameter->schema->type;

$paramType->setDescription($parameter->description);
$paramType->example($parameter->example);

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

namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor;

use Dedoc\Scramble\Support\Generator\Parameter;
use Dedoc\Scramble\Support\RouteInfo;

class RequestMethodCallsExtractor implements RulesExtractor
Expand All @@ -14,7 +15,14 @@ public function shouldHandle(): bool
public function extract(RouteInfo $routeInfo): ParametersExtractionResult
{
return new ParametersExtractionResult(
parameters: array_values($routeInfo->requestParametersFromCalls->data),
parameters: array_map(
function (Parameter $p) {
$p->setAttribute('isFromMethodCall', true);

return $p;
},
array_values($routeInfo->requestParametersFromCalls->data),
),
);
}
}
Loading

0 comments on commit b52caaf

Please sign in to comment.