Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed resource wrapping logic is not working when using with response method calls #675

Merged
merged 4 commits into from
Jan 8, 2025
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
4 changes: 4 additions & 0 deletions src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Dedoc\Scramble\Support\InferExtensions\ModelExtension;
use Dedoc\Scramble\Support\InferExtensions\PossibleExceptionInfer;
use Dedoc\Scramble\Support\InferExtensions\ResourceCollectionTypeInfer;
use Dedoc\Scramble\Support\InferExtensions\ResourceResponseMethodReturnTypeExtension;
use Dedoc\Scramble\Support\InferExtensions\ResponseFactoryTypeInfer;
use Dedoc\Scramble\Support\InferExtensions\ResponseMethodReturnTypeExtension;
use Dedoc\Scramble\Support\InferExtensions\TypeTraceInfer;
Expand All @@ -49,6 +50,7 @@
use Dedoc\Scramble\Support\TypeToSchemaExtensions\LengthAwarePaginatorTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ModelToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\PaginatorTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResourceResponseTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResponseTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\VoidTypeToSchema;
use PhpParser\ParserFactory;
Expand Down Expand Up @@ -96,6 +98,7 @@ public function configurePackage(Package $package): void
$inferExtensionsClasses = array_merge([
ResponseMethodReturnTypeExtension::class,
JsonResourceExtension::class,
ResourceResponseMethodReturnTypeExtension::class,
JsonResponseMethodReturnTypeExtension::class,
ModelExtension::class,
], $inferExtensionsClasses);
Expand Down Expand Up @@ -187,6 +190,7 @@ public function configurePackage(Package $package): void
PaginatorTypeToSchema::class,
LengthAwarePaginatorTypeToSchema::class,
ResponseTypeToSchema::class,
ResourceResponseTypeToSchema::class,
VoidTypeToSchema::class,
], $typesToSchemaExtensions),
array_merge([
Expand Down
9 changes: 7 additions & 2 deletions src/Support/InferExtensions/JsonResourceExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Dedoc\Scramble\Support\Type\IntegerType;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\Literal\LiteralBooleanType;
use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
use Dedoc\Scramble\Support\Type\NullType;
use Dedoc\Scramble\Support\Type\ObjectType;
Expand All @@ -29,8 +28,10 @@
use Dedoc\Scramble\Support\Type\StringType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\Union;
use Dedoc\Scramble\Support\Type\UnknownType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceResponse;
use Illuminate\Http\Resources\MergeValue;
use Illuminate\Http\Resources\MissingValue;

Expand All @@ -52,7 +53,11 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type
? $this->getModelMethodReturn($event->getInstance()->name, 'toArray', $event->arguments, $event->scope)
: null,

'response', 'toResponse' => new Generic(JsonResponse::class, [$event->getInstance(), new LiteralIntegerType(200), new ArrayType]),
'response', 'toResponse' => new Generic(JsonResponse::class, [
new Generic(ResourceResponse::class, [$event->getInstance()]),
new UnknownType,
new ArrayType,
]),

'whenLoaded' => count($event->arguments) === 1
? Union::wrap([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Dedoc\Scramble\Support\InferExtensions;

use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\MethodReturnTypeExtension;
use Dedoc\Scramble\Support\Type\ArrayType;
use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\UnknownType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\ResourceResponse;

class ResourceResponseMethodReturnTypeExtension implements MethodReturnTypeExtension
{
public function shouldHandle(ObjectType $type): bool
{
return $type->isInstanceOf(ResourceResponse::class);
}

public function getMethodReturnType(MethodCallEvent $event): ?Type
{
if ($event->name !== 'toResponse') {
return null;
}

$resourceType = $event->getInstance()->templateTypes[0] ?? null;
if (! $resourceType) {
return new Generic(JsonResponse::class, [new UnknownType, new UnknownType, new KeyedArrayType]);
}

return new Generic(JsonResponse::class, [
new Generic(ResourceResponse::class, [$resourceType]),
new UnknownType,
new ArrayType,
]);
}
}
108 changes: 4 additions & 104 deletions src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,22 @@
namespace Dedoc\Scramble\Support\TypeToSchemaExtensions;

use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
use Dedoc\Scramble\Infer\Analyzer\MethodQuery;
use Dedoc\Scramble\Infer\Scope\GlobalScope;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Support\Generator\Combined\AllOf;
use Dedoc\Scramble\Support\Generator\Reference;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
use Dedoc\Scramble\Support\Generator\Types\UnknownType;
use Dedoc\Scramble\Support\InferExtensions\ResourceCollectionTypeInfer;
use Dedoc\Scramble\Support\Type\ArrayType;
use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Reference\AbstractReferenceType;
use Dedoc\Scramble\Support\Type\Reference\MethodCallReferenceType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\TypeHelper;
use Dedoc\Scramble\Support\Type\TypeWalker;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Resources\Json\ResourceResponse;

class JsonResourceTypeToSchema extends TypeToSchemaExtension
{
Expand Down Expand Up @@ -82,103 +75,10 @@ public function toSchema(Type $type)
*/
public function toResponse(Type $type)
{
$definition = $this->infer->analyzeClass($type->name);

$additional = $type->templateTypes[1 /* TAdditional */] ?? new UnknownType;

$openApiType = $this->openApiTransformer->transform($type);

if (($withArray = $definition->getMethodCallType('with')) instanceof KeyedArrayType) {
$withArray->items = $this->flattenMergeValues($withArray->items);
}
if ($additional instanceof KeyedArrayType) {
$additional->items = $this->flattenMergeValues($additional->items);
}

$shouldWrap = ($wrapKey = $type->name::$wrap ?? null) !== null
|| $withArray instanceof KeyedArrayType
|| $additional instanceof KeyedArrayType;
$wrapKey = $wrapKey ?: 'data';

if ($shouldWrap) {
$openApiType = $this->mergeResourceTypeAndAdditionals(
$wrapKey,
$openApiType,
$this->normalizeKeyedArrayType($withArray),
$this->normalizeKeyedArrayType($additional),
);
}

$response = $this->openApiTransformer->toResponse($this->makeBaseResponse($type));

return $response
->description('`'.$this->components->uniqueSchemaName($type->name).'`')
->setContent(
'application/json',
Schema::fromType($openApiType),
);
}

private function makeBaseResponse(Type $type)
{
$definition = $this->infer->analyzeClass($type->name);

$responseType = new Generic(JsonResponse::class, [new \Dedoc\Scramble\Support\Type\UnknownType, new LiteralIntegerType(200), new KeyedArrayType]);

$methodQuery = MethodQuery::make($this->infer)
->withArgumentType([null, 1], $responseType)
->from($definition, 'withResponse');

$effectTypes = $methodQuery->getTypes(fn ($t) => (bool) (new TypeWalker)->first($t, fn ($t) => $t === $responseType));
$resourceResponseType = new Generic(ResourceResponse::class, [$type]);

$effectTypes
->filter(fn ($t) => $t instanceof AbstractReferenceType)
->each(function (AbstractReferenceType $t) use ($methodQuery) {
ReferenceTypeResolver::getInstance()->resolve($methodQuery->getScope(), $t);
});

return $responseType;
}

private function mergeResourceTypeAndAdditionals(string $wrapKey, $openApiType, ?KeyedArrayType $withArray, ?KeyedArrayType $additional)
{
$resolvedOpenApiType = $openApiType instanceof Reference ? $openApiType->resolve() : $openApiType;
$resolvedOpenApiType = $resolvedOpenApiType instanceof Schema ? $resolvedOpenApiType->type : $resolvedOpenApiType;

// If resolved type already contains wrapKey, we don't need to wrap it again. But we still need to merge additionals.
if ($resolvedOpenApiType instanceof OpenApiObjectType && $resolvedOpenApiType->hasProperty($wrapKey)) {
$items = array_values(array_filter([
$openApiType,
$this->transformNullableType($withArray),
$this->transformNullableType($additional),
]));

return count($items) > 1 ? (new AllOf)->setItems($items) : $items[0];
}

$openApiType = (new OpenApiObjectType)
->addProperty($wrapKey, $openApiType)
->setRequired([$wrapKey]);

if ($withArray) {
$this->mergeOpenApiObjects($openApiType, $this->openApiTransformer->transform($withArray));
}

if ($additional) {
$this->mergeOpenApiObjects($openApiType, $this->openApiTransformer->transform($additional));
}

return $openApiType;
}

private function normalizeKeyedArrayType($type): ?KeyedArrayType
{
return $type instanceof KeyedArrayType ? $type : null;
}

private function transformNullableType(?KeyedArrayType $type)
{
return $type ? $this->openApiTransformer->transform($type) : null;
return (new ResourceResponseTypeToSchema($this->infer, $this->openApiTransformer, $this->components))
->toResponse($resourceResponseType);
}

public function reference(ObjectType $type)
Expand Down
136 changes: 136 additions & 0 deletions src/Support/TypeToSchemaExtensions/ResourceResponseTypeToSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace Dedoc\Scramble\Support\TypeToSchemaExtensions;

use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
use Dedoc\Scramble\Infer\Analyzer\MethodQuery;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Support\Generator\Combined\AllOf;
use Dedoc\Scramble\Support\Generator\Reference;
use Dedoc\Scramble\Support\Generator\Schema;
use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType;
use Dedoc\Scramble\Support\Type\Generic;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
use Dedoc\Scramble\Support\Type\Reference\AbstractReferenceType;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\TypeWalker;
use Dedoc\Scramble\Support\Type\UnknownType;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\ResourceResponse;

class ResourceResponseTypeToSchema extends TypeToSchemaExtension
{
use FlattensMergeValues;
use MergesOpenApiObjects;

public function shouldHandle(Type $type)
{
return $type instanceof Generic
&& $type->isInstanceOf(ResourceResponse::class)
&& count($type->templateTypes) >= 1;
}

public function toResponse(Type $type)
{
$resourceType = $type->templateTypes[0];
$openApiType = $this->openApiTransformer->transform($resourceType);

$definition = $this->infer->analyzeClass($resourceType->name);

$withArray = $definition->getMethodCallType('with');
$additional = $resourceType instanceof Generic ? ($resourceType->templateTypes[1] ?? null) : null;

if ($withArray instanceof KeyedArrayType) {
$withArray->items = $this->flattenMergeValues($withArray->items);
}
if ($additional instanceof KeyedArrayType) {
$additional->items = $this->flattenMergeValues($additional->items);
}

$shouldWrap = ($wrapKey = $resourceType->name::$wrap ?? null) !== null
|| $withArray instanceof KeyedArrayType
|| $additional instanceof KeyedArrayType;
$wrapKey = $wrapKey ?: 'data';

if ($shouldWrap) {
$openApiType = $this->mergeResourceTypeAndAdditionals(
$wrapKey,
$openApiType,
$this->normalizeKeyedArrayType($withArray),
$this->normalizeKeyedArrayType($additional),
);
}

$response = $this->openApiTransformer->toResponse($this->makeBaseResponse($resourceType));

return $response
->description('`'.$this->components->uniqueSchemaName($resourceType->name).'`')
->setContent(
'application/json',
Schema::fromType($openApiType),
);
}

private function makeBaseResponse(Type $type)
{
$definition = $this->infer->analyzeClass($type->name);

$responseType = new Generic(JsonResponse::class, [new UnknownType, new LiteralIntegerType(200), new KeyedArrayType]);

$methodQuery = MethodQuery::make($this->infer)
->withArgumentType([null, 1], $responseType)
->from($definition, 'withResponse');

$effectTypes = $methodQuery->getTypes(fn ($t) => (bool) (new TypeWalker)->first($t, fn ($t) => $t === $responseType));

$effectTypes
->filter(fn ($t) => $t instanceof AbstractReferenceType)
->each(function (AbstractReferenceType $t) use ($methodQuery) {
ReferenceTypeResolver::getInstance()->resolve($methodQuery->getScope(), $t);
});

return $responseType;
}

private function mergeResourceTypeAndAdditionals(string $wrapKey, $openApiType, ?KeyedArrayType $withArray, ?KeyedArrayType $additional)
{
$resolvedOpenApiType = $openApiType instanceof Reference ? $openApiType->resolve() : $openApiType;
$resolvedOpenApiType = $resolvedOpenApiType instanceof Schema ? $resolvedOpenApiType->type : $resolvedOpenApiType;

// If resolved type already contains wrapKey, we don't need to wrap it again. But we still need to merge additionals.
if ($resolvedOpenApiType instanceof OpenApiObjectType && $resolvedOpenApiType->hasProperty($wrapKey)) {
$items = array_values(array_filter([
$openApiType,
$this->transformNullableType($withArray),
$this->transformNullableType($additional),
]));

return count($items) > 1 ? (new AllOf)->setItems($items) : $items[0];
}

$openApiType = (new OpenApiObjectType)
->addProperty($wrapKey, $openApiType)
->setRequired([$wrapKey]);

if ($withArray) {
$this->mergeOpenApiObjects($openApiType, $this->openApiTransformer->transform($withArray));
}

if ($additional) {
$this->mergeOpenApiObjects($openApiType, $this->openApiTransformer->transform($additional));
}

return $openApiType;
}

private function normalizeKeyedArrayType($type): ?KeyedArrayType
{
return $type instanceof KeyedArrayType ? $type : null;
}

private function transformNullableType(?KeyedArrayType $type)
{
return $type ? $this->openApiTransformer->transform($type) : null;
}
}
Loading
Loading