diff --git a/src/Infer/Scope/Scope.php b/src/Infer/Scope/Scope.php index 09b5b438..87ca74ae 100644 --- a/src/Infer/Scope/Scope.php +++ b/src/Infer/Scope/Scope.php @@ -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; @@ -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 @@ -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()) { @@ -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) { diff --git a/src/Support/InferExtensions/JsonResourceExtension.php b/src/Support/InferExtensions/JsonResourceExtension.php index 8717bc54..e75a3008 100644 --- a/src/Support/InferExtensions/JsonResourceExtension.php +++ b/src/Support/InferExtensions/JsonResourceExtension.php @@ -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; @@ -49,8 +56,11 @@ 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([ @@ -58,7 +68,7 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type $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))), ]), @@ -68,11 +78,66 @@ 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([ @@ -80,6 +145,8 @@ public function getMethodReturnType(MethodCallEvent $event): ?Type $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), @@ -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); @@ -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() + ), + ]); + } } diff --git a/tests/Files/SamplePostModel.php b/tests/Files/SamplePostModel.php index c4a35f39..b447b16c 100644 --- a/tests/Files/SamplePostModel.php +++ b/tests/Files/SamplePostModel.php @@ -15,6 +15,7 @@ class SamplePostModel extends Model protected $with = ['parent', 'children', 'user']; protected $casts = [ + 'read_time' => 'int', 'status' => Status::class, 'settings' => 'array', ]; diff --git a/tests/InferExtensions/JsonResourceExtensionTest.php b/tests/InferExtensions/JsonResourceExtensionTest.php new file mode 100644 index 00000000..8cd360e8 --- /dev/null +++ b/tests/InferExtensions/JsonResourceExtensionTest.php @@ -0,0 +1,78 @@ +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'), + ]; + } +}