Skip to content

Commit

Permalink
Added all ConditionallyLoadsAttributes methods support (#652)
Browse files Browse the repository at this point in the history
* added all ConditionallyLoadsAttributes methods support

* added basic test

* Fix styling

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Dec 5, 2024
1 parent 51aa306 commit 43d8f75
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 20 deletions.
13 changes: 5 additions & 8 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use Dedoc\Scramble\Support\Type\CallableStringType;
use Dedoc\Scramble\Support\Type\KeyedArrayType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Reference\AbstractReferenceType;
use Dedoc\Scramble\Support\Type\Reference\CallableCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\MethodCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\NewCallReferenceType;
Expand Down Expand Up @@ -119,14 +118,10 @@ public function getType(Node $node): Type
? new MethodCallEvent($calleeType, $node->name->name, $this, $this->getArgsTypes($node->args), $calleeType->name)
: null;

$type = ($event ? app(ExtensionsBroker::class)->getMethodReturnType($event) : null)
?: new MethodCallReferenceType($calleeType, $node->name->name, $this->getArgsTypes($node->args));

$exceptions = $event ? app(ExtensionsBroker::class)->getMethodCallExceptions($event) : [];

if (
$calleeType instanceof TemplateType
&& $type instanceof AbstractReferenceType
&& ! $exceptions
) {
// @todo
Expand All @@ -136,12 +131,14 @@ public function getType(Node $node): Type
return $this->setType($node, new UnknownType("Cannot infer type of method [{$node->name->name}] call on template type: not supported yet."));
}

$referenceType = new MethodCallReferenceType($calleeType, $node->name->name, $this->getArgsTypes($node->args));

/*
* When inside a constructor, we want to add a side effect to the constructor definition, so we can track
* how the properties are being set.
*/
if ($this->functionDefinition()?->type->name === '__construct' && $type instanceof AbstractReferenceType) {
$this->functionDefinition()->sideEffects[] = $type;
if ($this->functionDefinition()?->type->name === '__construct') {
$this->functionDefinition()->sideEffects[] = $referenceType;
}

if ($this->functionDefinition()) {
Expand All @@ -151,7 +148,7 @@ public function getType(Node $node): Type
);
}

return $this->setType($node, $type);
return $this->setType($node, $referenceType);
}

if ($node instanceof Node\Expr\StaticCall) {
Expand Down
137 changes: 125 additions & 12 deletions src/Support/InferExtensions/JsonResourceExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,33 @@

namespace Dedoc\Scramble\Support\InferExtensions;

use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
use Dedoc\Scramble\Infer\Extensions\Event\PropertyFetchEvent;
use Dedoc\Scramble\Infer\Extensions\MethodReturnTypeExtension;
use Dedoc\Scramble\Infer\Extensions\PropertyTypeExtension;
use Dedoc\Scramble\Infer\Scope\GlobalScope;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Support\Helpers\JsonResourceHelper;
use Dedoc\Scramble\Support\Type\ArrayItemType_;
use Dedoc\Scramble\Support\Type\ArrayType;
use Dedoc\Scramble\Support\Type\BooleanType;
use Dedoc\Scramble\Support\Type\FloatType;
use Dedoc\Scramble\Support\Type\FunctionType;
use Dedoc\Scramble\Support\Type\Generic;
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;
use Dedoc\Scramble\Support\Type\Reference\MethodCallReferenceType;
use Dedoc\Scramble\Support\Type\Reference\PropertyFetchReferenceType;
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\MergeValue;
Expand Down Expand Up @@ -49,16 +56,19 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type

'whenLoaded' => count($event->arguments) === 1
? Union::wrap([
// Relationship type which does not really matter
new UnknownType('Skipped real relationship type extracting'),
$this->getModelPropertyType(
$event->getDefinition(),
$event->getArg('attribute', 0)->value ?? '',
$event->scope
),
new ObjectType(MissingValue::class),
])
: Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'when' => Union::wrap([
'when', 'unless', 'whenPivotLoaded' => Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),
Expand All @@ -68,18 +78,75 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type
$this->value($event->getArg('value', 0)),
]),

'mergeWhen' => new Generic(MergeValue::class, [
'mergeWhen', 'mergeUnless' => new Generic(MergeValue::class, [
new BooleanType,
$this->value($event->getArg('value', 1)),
]),

'whenHas', 'whenAppended' => count($event->arguments) === 1
? Union::wrap([$this->getModelPropertyType(
$event->getDefinition(),
$event->getArg('attribute', 0)->value ?? '',
$event->scope
), new ObjectType(MissingValue::class)])
: Union::wrap([
($valueType = $event->getArg('value', 1, new NullType)) instanceof NullType
? $this->getModelPropertyType(
$event->getDefinition(),
$event->getArg('attribute', 0)->value ?? '',
$event->scope
)
: $this->value($valueType),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'whenNotNull' => Union::wrap([
$this->value($this->removeNullFromUnion($event->getArg('value', 0))),
$this->value($event->getArg('default', 1, new ObjectType(MissingValue::class))),
]),

'whenNull' => Union::wrap([
new NullType,
$this->value($event->getArg('default', 1, new ObjectType(MissingValue::class))),
]),

'whenAggregated' => count($event->arguments) <= 3
? Union::wrap([
match ($event->getArg('aggregate', 2)?->value ?? '') {
'count' => new IntegerType,
'avg', 'sum' => new FloatType,
default => new StringType,
},
$this->value($event->getArg('default', 4, new ObjectType(MissingValue::class))),
])
: Union::wrap([
$this->value($event->getArg('value', 3)),
$this->value($event->getArg('default', 4, new ObjectType(MissingValue::class))),
]),

'whenExistsLoaded' => count($event->arguments) === 1
? Union::wrap([new BooleanType, new ObjectType(MissingValue::class)])
: Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'whenPivotLoadedAs' => Union::wrap([
$this->value($event->getArg('value', 2)),
$this->value($event->getArg('default', 3, new ObjectType(MissingValue::class))),
]),

'hasPivotLoaded', 'hasPivotLoadedAs' => new BooleanType,

'whenCounted' => count($event->arguments) === 1
? Union::wrap([new IntegerType, new ObjectType(MissingValue::class)])
: Union::wrap([
$this->value($event->getArg('value', 1)),
$this->value($event->getArg('default', 2, new ObjectType(MissingValue::class))),
]),

'attributes' => $this->getAttributesMethodReturnType($event),

default => ! $event->getDefinition() || $event->getDefinition()->hasMethodDefinition($event->name)
? null
: $this->proxyMethodCallToModel($event),
Expand All @@ -92,16 +159,21 @@ public function getPropertyType(PropertyFetchEvent $event): ?Type
'resource' => JsonResourceHelper::modelType($event->getDefinition(), $event->scope),
default => ! $event->getDefinition() || $event->getDefinition()->hasPropertyDefinition($event->name)
? null
: ReferenceTypeResolver::getInstance()->resolve(
$event->scope,
new PropertyFetchReferenceType(
JsonResourceHelper::modelType($event->getDefinition(), $event->scope),
$event->name,
),
),
: $this->getModelPropertyType($event->getDefinition(), $event->name, $event->scope),
};
}

private function getModelPropertyType(ClassDefinition $jsonResourceDefinition, string $name, Scope $scope)
{
return ReferenceTypeResolver::getInstance()->resolve(
$scope,
new PropertyFetchReferenceType(
JsonResourceHelper::modelType($jsonResourceDefinition, $scope),
$name,
),
);
}

private function proxyMethodCallToModel(MethodCallEvent $event)
{
return $this->getModelMethodReturn($event->getInstance()->name, $event->name, $event->arguments, $event->scope);
Expand All @@ -121,4 +193,45 @@ private function value(Type $type)
{
return $type instanceof FunctionType ? $type->getReturnType() : $type;
}

private function removeNullFromUnion(Type $type)
{
$type = Union::wrap(
ReferenceTypeResolver::getInstance()->resolve(new GlobalScope, $type)
);

$types = $type instanceof Union ? $type->types : [$type];

return Union::wrap(
collect($types)->filter(fn ($t) => ! $t instanceof NullType)->values()->all()
);
}

private function getAttributesMethodReturnType(MethodCallEvent $event)
{
$argument = $event->getArg('attributes', 0);

$value = $argument instanceof KeyedArrayType
? collect($argument->items)->map(fn (ArrayItemType_ $t) => $t->value instanceof LiteralStringType ? $t->value->value : null)->filter()->values()->all()
: ($argument instanceof LiteralStringType ? $argument->value : []);

$modelToArrayReturn = $this->getModelMethodReturn($event->getInstance()->name, 'toArray', $event->arguments, $event->scope);

if (! $modelToArrayReturn instanceof KeyedArrayType) {
return new Generic(MergeValue::class, [
new LiteralBooleanType(true),
new KeyedArrayType([]),
]);
}

return new Generic(MergeValue::class, [
new LiteralBooleanType(true),
new KeyedArrayType(
collect($modelToArrayReturn->items)
->filter(fn (ArrayItemType_ $t) => in_array($t->key, $value))
->values()
->all()
),
]);
}
}
1 change: 1 addition & 0 deletions tests/Files/SamplePostModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SamplePostModel extends Model
protected $with = ['parent', 'children', 'user'];

protected $casts = [
'read_time' => 'int',
'status' => Status::class,
'settings' => 'array',
];
Expand Down
78 changes: 78 additions & 0 deletions tests/InferExtensions/JsonResourceExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

use Dedoc\Scramble\Infer;
use Dedoc\Scramble\Support\Generator\Components;
use Dedoc\Scramble\Support\Generator\TypeTransformer;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\JsonResourceTypeToSchema;
use Dedoc\Scramble\Support\TypeToSchemaExtensions\ModelToSchema;
use Dedoc\Scramble\Tests\Files\SamplePostModel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

uses(RefreshDatabase::class);

beforeEach(function () {
$this->infer = app(Infer::class);
});

/**
* @return array{0: \Dedoc\Scramble\Support\Generator\Types\Type, 1: Components}
*/
function JsonResourceExtensionTest_analyze(Infer $infer, string $class)
{
$transformer = new TypeTransformer($infer, $components = new Components, [
ModelToSchema::class,
JsonResourceTypeToSchema::class,
]);
$extension = new JsonResourceTypeToSchema($infer, $transformer, $components);

$type = new ObjectType($class);

$openApiType = $extension->toSchema($type);

return [$openApiType, $components];
}

it('supports whenHas', function () {
[$schema] = JsonResourceExtensionTest_analyze($this->infer, JsonResourceExtensionTest_WhenHas::class);

expect($schema->toArray())->toBe([
'type' => 'object',
'properties' => [
'user' => [
'$ref' => '#/components/schemas/SampleUserModel',
],
'value' => [
'type' => 'integer',
'example' => 42,
],
'default' => [
'anyOf' => [
[
'type' => 'string',
'enum' => ['foo'],
],
[
'type' => 'integer',
'enum' => [42],
],
],
],
],
'required' => ['default'],
]);
});
/** @mixin SamplePostModel */
class JsonResourceExtensionTest_WhenHas extends JsonResource
{
public function toArray(Request $request)
{
return [
'user' => $this->whenHas('user'),
'value' => $this->whenHas('user', 42),
'default' => $this->whenHas('user', 42, 'foo'),
];
}
}

0 comments on commit 43d8f75

Please sign in to comment.