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
84 changes: 82 additions & 2 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\Reference;
use Dedoc\Scramble\Support\Generator\RequestBodyObject;
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\TypeTransformer;
Expand Down Expand Up @@ -63,7 +64,7 @@ public function handle(Operation $operation, RouteInfo $routeInfo)

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

return;
Expand All @@ -73,7 +74,9 @@ public function handle(Operation $operation, RouteInfo $routeInfo)
->partition(fn (Parameter $p) => $p->in !== 'body' || $p->getAttribute('isInQuery') || $p->getAttribute('nonBody'))
->map->toArray();

$operation->addParameters($this->convertDotNamedParamsToComplexStructures($nonBodyParams));
$operation->addParameters(
$this->convertDotNamedParamsToQueryParams($nonBodyParams)
);

if (! $bodyParams) {
return;
Expand Down Expand Up @@ -169,6 +172,83 @@ protected function convertDotNamedParamsToComplexStructures($params)
return (new DeepParametersMerger(collect($params)))->handle();
}

/**
* @param Parameter[] $params
* @return Parameter[]
*/
protected function convertDotNamedParamsToQueryParams(array $params): array
{
/** @var Collection<string, Parameter> $paramsByName */
$paramsByName = collect($params)->keyBy->name;

[$convertableParameters, $deepParameters] = collect($params)
/*
* Rejecting array "container" parameters for cases when there are properties specified. For example:
* ['filter' => 'array', 'filter.accountable' => 'integer']
* In this ruleset `filter` should not be documented at all as the accountable is enough.
*/
->reject(fn (Parameter $p) => $paramsByName->keys()->some(fn (string $key) => Str::startsWith($key, $p->name.'.')))
->partition(function (Parameter $p) {
if ($p->getAttribute('isFlat')) {
return true;
}

$isScalar = ! in_array($p->schema->type->type ?? null, ['array', 'object', null], strict: true);

$isArrayOfScalar = ($p->schema->type ?? null) instanceof ArrayType
&& ! in_array($p->schema->type->items->type ?? null, ['array', 'object', null], strict: true);

if (! Str::contains($p->name, '*')) { // no nested arrays
return $isScalar || $isArrayOfScalar;
}

if (Str::endsWith($p->name, '*') && (Str::substrCount($p->name, '*') === 1)) {
return $isScalar;
}

return false;
});

$deepParameters = array_map(
fn (Parameter $p) => tap($p, fn (Parameter $p) => $p->setExtensionProperty('deepObject-style', 'qs')),
$this->convertDotNamedParamsToComplexStructures($deepParameters->all()),
);

return collect($convertableParameters)
->map(function (Parameter $originalParameter) use ($paramsByName) {
$parameter = clone $originalParameter;

$parameter->name = Str::of($parameter->name)
->explode('.')
->map(fn ($str, $i) => $i === 0 ? $str : ($str === '*' ? '[]' : "[$str]"))
->join('');

if ($parameter->schema->type instanceof ArrayType) {
$parameter->name .= '[]';
}

if (
$parameter->name !== $originalParameter->name
&& ($sameNameParam = $paramsByName->get($parameter->name))
&& $sameNameParam !== $originalParameter
) {
return null;
}

if (Str::endsWith($parameter->name, '[]') && ! $parameter->schema->type instanceof ArrayType) {
$parameter->schema->type = (new ArrayType)
->setItems($parameter->schema->type)
->addProperties($parameter->schema->type);
}

return $parameter;
})
->filter()
->values()
->merge($deepParameters)
->all();
}

protected function getMediaType(Operation $operation, RouteInfo $routeInfo, array $bodyParams): string
{
if (
Expand Down
103 changes: 103 additions & 0 deletions tests/Support/OperationExtensions/RequestBodyExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,106 @@ public function __invoke(Request $request)
$request->validate(['foo' => 'integer']);
}
}

it('documents deep query parameters according to how they can be read by laravel api', function () {
$document = generateForRoute(fn () => RouteFacade::get('test', RequestBodyExtensionTest_DeepQueryParametersController::class));

expect($parameters = $document['paths']['/test']['get']['parameters'])
->toHaveCount(1)
->and($parameters[0])
->toBe([
'name' => 'filter[accountable]',
'in' => 'query',
'schema' => [
'type' => 'integer',
],
]);
});
class RequestBodyExtensionTest_DeepQueryParametersController
{
public function __invoke(Request $request)
{
$request->validate([
'filter.accountable' => 'integer',
]);
}
}

it('documents deep query parameters with container according to how they can be read by laravel api', function () {
$document = generateForRoute(fn () => RouteFacade::get('test', RequestBodyExtensionTest_DeepQueryParametersWithContainerController::class));

expect($parameters = $document['paths']['/test']['get']['parameters'])
->toHaveCount(1)
->and($parameters[0])
->toBe([
'name' => 'filter[accountable]',
'in' => 'query',
'schema' => [
'type' => 'integer',
],
]);
});
class RequestBodyExtensionTest_DeepQueryParametersWithContainerController
{
public function __invoke(Request $request)
{
$request->validate([
'filter' => 'array',
'filter.accountable' => 'integer',
]);
}
}

it('documents array query parameters as arrays of some type', function () {
$document = generateForRoute(fn () => RouteFacade::get('test', RequestBodyExtensionTest_ArrayQueryParametersController::class));

expect($parameters = $document['paths']['/test']['get']['parameters'])
->toHaveCount(1)
->and($parameters[0])
->toBe([
'name' => 'tags[]',
'in' => 'query',
'schema' => [
'type' => 'array',
'items' => [
'type' => 'string',
],
],
]);
});
class RequestBodyExtensionTest_ArrayQueryParametersController
{
public function __invoke(Request $request)
{
$request->validate([
'tags' => 'array',
]);
}
}

it('documents array query parameters as arrays of specific type', function () {
$document = generateForRoute(fn () => RouteFacade::get('test', RequestBodyExtensionTest_ArraySpecificQueryParametersController::class));

expect($parameters = $document['paths']['/test']['get']['parameters'])
->toHaveCount(1)
->and($parameters[0])
->toBe([
'name' => 'tags[]',
'in' => 'query',
'schema' => [
'type' => 'array',
'items' => [
'type' => 'integer',
],
],
]);
});
class RequestBodyExtensionTest_ArraySpecificQueryParametersController
{
public function __invoke(Request $request)
{
$request->validate([
'tags.*' => 'integer',
]);
}
}