From 224be1bb4a0af48ec1185c494a0afd97ac71e68a Mon Sep 17 00:00:00 2001 From: romalytvynenko Date: Tue, 3 Sep 2024 17:06:10 +0300 Subject: [PATCH] Add `withResponse` support for JSON API resources (#514) * `withResponse` method support * undo change * Fix styling --------- Co-authored-by: romalytvynenko --- src/Infer/Analyzer/MethodQuery.php | 99 +++++++++++++++++++ .../Reference/MethodCallReferenceType.php | 5 + .../Type/Reference/NewCallReferenceType.php | 5 + .../StaticMethodCallReferenceType.php | 5 + .../JsonResourceTypeToSchema.php | 31 +++++- .../JsonResourceTypeToSchemaTest.php | 74 +++++++------- 6 files changed, 179 insertions(+), 40 deletions(-) create mode 100644 src/Infer/Analyzer/MethodQuery.php diff --git a/src/Infer/Analyzer/MethodQuery.php b/src/Infer/Analyzer/MethodQuery.php new file mode 100644 index 00000000..d823d1cb --- /dev/null +++ b/src/Infer/Analyzer/MethodQuery.php @@ -0,0 +1,99 @@ +argumentsOverrides[] = [$positionName, $type]; + + return $this; + } + + public function from(Infer\Definition\ClassDefinition $classDefinition, string $methodName): static + { + $bag = new Bag; + + if (! $methodDefinition = $classDefinition->getMethodDefinition($methodName)) { + return $this; + } + + (new MethodAnalyzer($this->infer->index, $classDefinition)) + ->analyze($methodDefinition, [ + new class($bag, $this->argumentsOverrides) implements IndexBuilder + { + public function __construct(private Bag $bag, private array $argumentsOverrides = []) {} + + public function afterAnalyzedNode(Scope $scope, Node $node): void + { + $this->replaceArguments($scope, $node); + + $types = $this->bag->data['types'] ?? []; + $this->bag->set('scope', $scope); + + if ($type = $scope->getType($node)) { + $types[] = $type; + } + + $this->bag->set('types', $types); + } + + private function replaceArguments(Scope $scope, Node $node) + { + if ($this->bag->data['_hasReplaced'] ?? false) { + return; + } + + foreach ($this->argumentsOverrides as $argumentsOverride) { + [[$name, $position], $type] = $argumentsOverride; + + $targetName = $name ?: array_keys($scope->variables)[$position]; + + $scope->variables[$targetName][0]['type'] = $type; + } + + $this->bag->set('_hasReplaced', true); + } + }, + ]); + + $this->scope = $bag->data['scope'] ?? null; + $this->types = $bag->data['types'] ?? []; + + return $this; + } + + public function getTypes(callable $typesFinder) + { + return collect($this->types)->filter($typesFinder)->values(); + } + + public function getScope(): ?Scope + { + return $this->scope; + } +} diff --git a/src/Support/Type/Reference/MethodCallReferenceType.php b/src/Support/Type/Reference/MethodCallReferenceType.php index 40f5ddb6..6878823e 100644 --- a/src/Support/Type/Reference/MethodCallReferenceType.php +++ b/src/Support/Type/Reference/MethodCallReferenceType.php @@ -16,6 +16,11 @@ public function __construct( public array $arguments, ) {} + public function nodes(): array + { + return ['callee', 'arguments']; + } + public function toString(): string { $argsTypes = implode( diff --git a/src/Support/Type/Reference/NewCallReferenceType.php b/src/Support/Type/Reference/NewCallReferenceType.php index 817ea403..fd590364 100644 --- a/src/Support/Type/Reference/NewCallReferenceType.php +++ b/src/Support/Type/Reference/NewCallReferenceType.php @@ -13,6 +13,11 @@ public function __construct( public array $arguments, ) {} + public function nodes(): array + { + return ['arguments']; + } + public function isInstanceOf(string $className) { return is_a($this->name, $className, true); diff --git a/src/Support/Type/Reference/StaticMethodCallReferenceType.php b/src/Support/Type/Reference/StaticMethodCallReferenceType.php index 81f1bc30..4acc60d2 100644 --- a/src/Support/Type/Reference/StaticMethodCallReferenceType.php +++ b/src/Support/Type/Reference/StaticMethodCallReferenceType.php @@ -14,6 +14,11 @@ public function __construct( public array $arguments, ) {} + public function nodes(): array + { + return ['arguments']; + } + public function toString(): string { $argsTypes = implode( diff --git a/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php b/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php index 9b67f8ca..3778ab22 100644 --- a/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php +++ b/src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php @@ -3,11 +3,11 @@ 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\Response; use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\Types\ObjectType as OpenApiObjectType; use Dedoc\Scramble\Support\Generator\Types\UnknownType; @@ -15,10 +15,14 @@ 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; @@ -105,7 +109,9 @@ public function toResponse(Type $type) ); } - return Response::make(200) + $response = $this->openApiTransformer->toResponse($this->makeBaseResponse($type)); + + return $response ->description('`'.$this->components->uniqueSchemaName($type->name).'`') ->setContent( 'application/json', @@ -113,6 +119,27 @@ public function toResponse(Type $type) ); } + 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)); + + $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, Reference|OpenApiObjectType $openApiType, ?KeyedArrayType $withArray, ?KeyedArrayType $additional) { $resolvedOpenApiType = $openApiType instanceof Reference ? $openApiType->resolve() : $openApiType; diff --git a/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php b/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php index 975012ff..da0f2354 100644 --- a/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php +++ b/tests/Support/TypeToSchemaExtensions/JsonResourceTypeToSchemaTest.php @@ -6,38 +6,27 @@ use Dedoc\Scramble\Support\Type\Generic; use Dedoc\Scramble\Support\Type\UnknownType; use Dedoc\Scramble\Support\TypeToSchemaExtensions\JsonResourceTypeToSchema; +use Dedoc\Scramble\Support\TypeToSchemaExtensions\ResponseTypeToSchema; -it('documents the response type when return is not array node', function () { - $type = new Generic(JsonResourceTypeToSchemaTest_Sample::class, [new UnknownType]); +it('supports parent toArray class', function (string $className, array $expectedSchemaArray) { + $type = new Generic($className, [new UnknownType]); $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [ JsonResourceTypeToSchema::class, ]); $extension = new JsonResourceTypeToSchema($infer, $transformer, $components); - $schema = $extension->toSchema($type); - - expect($schema->toArray())->toBe([ + expect($extension->toSchema($type)->toArray())->toBe($expectedSchemaArray); +})->with([ + [JsonResourceTypeToSchemaTest_Sample::class, [ 'type' => 'object', 'properties' => [ 'id' => ['type' => 'integer'], 'name' => ['type' => 'string'], ], 'required' => ['id', 'name'], - ]); -}); - -it('documents spread parent toArray calls', function () { - $type = new Generic(JsonResourceTypeToSchemaTest_SpreadSample::class, [new UnknownType]); - - $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [ - JsonResourceTypeToSchema::class, - ]); - $extension = new JsonResourceTypeToSchema($infer, $transformer, $components); - - $schema = $extension->toSchema($type); - - expect($schema->toArray())->toBe([ + ]], + [JsonResourceTypeToSchemaTest_SpreadSample::class, [ 'type' => 'object', 'properties' => [ 'id' => ['type' => 'integer'], @@ -45,34 +34,20 @@ 'foo' => ['type' => 'string', 'example' => 'bar'], ], 'required' => ['id', 'name', 'foo'], - ]); -}); - -it('documents json resources when no toArray is defined', function () { - $type = new Generic(JsonResourceTypeToSchemaTest_NoToArraySample::class, [new UnknownType]); - - $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [ - JsonResourceTypeToSchema::class, - ]); - $extension = new JsonResourceTypeToSchema($infer, $transformer, $components); - - $schema = $extension->toSchema($type); - - expect($schema->toArray())->toBe([ + ]], + [JsonResourceTypeToSchemaTest_NoToArraySample::class, [ 'type' => 'object', 'properties' => [ 'id' => ['type' => 'integer'], 'name' => ['type' => 'string'], ], 'required' => ['id', 'name'], - ]); -}); - + ]], +]); /** * @property JsonResourceTypeToSchemaTest_User $resource */ class JsonResourceTypeToSchemaTest_NoToArraySample extends \Illuminate\Http\Resources\Json\JsonResource {} - /** * @property JsonResourceTypeToSchemaTest_User $resource */ @@ -86,7 +61,6 @@ public function toArray($request) ]; } } - /** * @property JsonResourceTypeToSchemaTest_User $resource */ @@ -97,6 +71,30 @@ public function toArray($request) return parent::toArray($request); } } + +it('handles withResponse for json api resource', function () { + $type = new Generic(JsonResourceTypeToSchemaTest_WithResponseSample::class, [new UnknownType]); + + $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [ + JsonResourceTypeToSchema::class, + ResponseTypeToSchema::class, + ]); + $extension = new JsonResourceTypeToSchema($infer, $transformer, $components); + + expect($extension->toResponse($type)->code)->toBe(429); +}); + +/** + * @property JsonResourceTypeToSchemaTest_User $resource + */ +class JsonResourceTypeToSchemaTest_WithResponseSample extends \Illuminate\Http\Resources\Json\JsonResource +{ + public function withResponse(\Illuminate\Http\Request $request, \Illuminate\Http\JsonResponse $response) + { + $response->setStatusCode(429); + } +} + class JsonResourceTypeToSchemaTest_User extends \Illuminate\Database\Eloquent\Model { protected $table = 'users';