Skip to content

Commit

Permalink
Add withResponse support for JSON API resources (#514)
Browse files Browse the repository at this point in the history
* `withResponse` method support

* undo change

* Fix styling

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Sep 3, 2024
1 parent 1d2cabf commit 224be1b
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 40 deletions.
99 changes: 99 additions & 0 deletions src/Infer/Analyzer/MethodQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Dedoc\Scramble\Infer\Analyzer;

use Dedoc\Scramble\Infer;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Support\IndexBuilders\Bag;
use Dedoc\Scramble\Support\IndexBuilders\IndexBuilder;
use Dedoc\Scramble\Support\Type\Type;
use PhpParser\Node;

/**
* @internal
*/
class MethodQuery
{
private array $argumentsOverrides = [];

private array $types = [];

private ?Scope $scope = null;

public function __construct(private Infer $infer) {}

public static function make(Infer $infer): static
{
return new static($infer);
}

public function withArgumentType(array $positionName, Type $type): static
{
$this->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;
}
}
5 changes: 5 additions & 0 deletions src/Support/Type/Reference/MethodCallReferenceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public function __construct(
public array $arguments,
) {}

public function nodes(): array
{
return ['callee', 'arguments'];
}

public function toString(): string
{
$argsTypes = implode(
Expand Down
5 changes: 5 additions & 0 deletions src/Support/Type/Reference/NewCallReferenceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/Support/Type/Reference/StaticMethodCallReferenceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public function __construct(
public array $arguments,
) {}

public function nodes(): array
{
return ['arguments'];
}

public function toString(): string
{
$argsTypes = implode(
Expand Down
31 changes: 29 additions & 2 deletions src/Support/TypeToSchemaExtensions/JsonResourceTypeToSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@
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;
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;
Expand Down Expand Up @@ -105,14 +109,37 @@ 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',
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));

$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,48 @@
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'],
'name' => ['type' => 'string'],
'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
*/
Expand All @@ -86,7 +61,6 @@ public function toArray($request)
];
}
}

/**
* @property JsonResourceTypeToSchemaTest_User $resource
*/
Expand All @@ -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';
Expand Down

0 comments on commit 224be1b

Please sign in to comment.