Skip to content

Commit

Permalink
Fixed resource wrapping logic is not working when using with response…
Browse files Browse the repository at this point in the history
… method calls (#675)

* wip: introduced the resource response

* json resource handing

* added test for additional and status code and wrapping

* Fix styling

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Jan 8, 2025
1 parent df6e90e commit 3c44e2e
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 110 deletions.
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

0 comments on commit 3c44e2e

Please sign in to comment.