From 9f01a1a1a05e27d61f50cdc65657861802e78ec5 Mon Sep 17 00:00:00 2001 From: James <61766491+lioneaglesolutions@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:39:58 +1000 Subject: [PATCH 1/2] Add support for defining Groups and Subgroups as enums (#932) * add support for enum groups * wording --------- Co-authored-by: Shalvah --- src/Attributes/Group.php | 19 +++++++++++++-- src/Attributes/Subgroup.php | 19 +++++++++++++-- tests/Fixtures/TestGroupBackedEnum.php | 9 ++++++++ .../GetFromMetadataAttributesTest.php | 23 +++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/TestGroupBackedEnum.php diff --git a/src/Attributes/Group.php b/src/Attributes/Group.php index 3421efd3..e0484831 100644 --- a/src/Attributes/Group.php +++ b/src/Attributes/Group.php @@ -8,17 +8,32 @@ class Group { public function __construct( - public string $name, + public mixed $name, public ?string $description = '', /** You can use the separate #[Authenticated] attribute, or pass authenticated: false to this. */ public ?bool $authenticated = null, ){ } + protected function getName(): string + { + if (is_string($this->name)) { + return $this->name; + } + + if (interface_exists('BackedEnum') && is_a($this->name, 'BackedEnum')) { + return $this->name->value; + } + + throw new \InvalidArgumentException( + 'The name property of a group must be either a PHP Backed Enum or a string' + ); + } + public function toArray() { $data = [ - "groupName" => $this->name, + "groupName" => $this->getName(), "groupDescription" => $this->description, ]; diff --git a/src/Attributes/Subgroup.php b/src/Attributes/Subgroup.php index a7edf1f2..86497220 100644 --- a/src/Attributes/Subgroup.php +++ b/src/Attributes/Subgroup.php @@ -8,15 +8,30 @@ class Subgroup { public function __construct( - public string $name, + public mixed $name, public ?string $description = '', ){ } + protected function getName(): string + { + if (is_string($this->name)) { + return $this->name; + } + + if (interface_exists('BackedEnum') && is_a($this->name, 'BackedEnum')) { + return $this->name->value; + } + + throw new \InvalidArgumentException( + 'The name property of a subgroup must be either a PHP Backed Enum or a string' + ); + } + public function toArray() { return [ - "subgroup" => $this->name, + "subgroup" => $this->getName(), "subgroupDescription" => $this->description, ]; } diff --git a/tests/Fixtures/TestGroupBackedEnum.php b/tests/Fixtures/TestGroupBackedEnum.php new file mode 100644 index 00000000..e5b64543 --- /dev/null +++ b/tests/Fixtures/TestGroupBackedEnum.php @@ -0,0 +1,9 @@ + false, ], $results); + $endpoint = $this->endpoint(function (ExtractedEndpointData $e) { + $e->controller = new ReflectionClass(MetadataAttributesTestController::class); + $e->method = $e->controller->getMethod('b2'); + }); + $results = $this->fetch($endpoint); + $this->assertArraySubset([ + "groupName" => "Users", + "groupDescription" => "", + "subgroup" => "Admins", + "subgroupDescription" => "", + "title" => "Endpoint B2", + "description" => "", + "authenticated" => false, + ], $results); + $endpoint = $this->endpoint(function (ExtractedEndpointData $e) { $e->controller = new ReflectionClass(MetadataAttributesTestController2::class); $e->method = $e->controller->getMethod('c1'); @@ -159,6 +175,13 @@ public function a3() public function b1() { } + + #[Group(TestGroupBackedEnum::Users)] + #[Subgroup(TestGroupBackedEnum::Admins)] + #[Endpoint("Endpoint B2")] + public function b2() + { + } } #[Authenticated] From 8a2884e1a2b47bf469a73b9516173c393d2b6b11 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 18 Jan 2025 15:42:26 -0500 Subject: [PATCH 2/2] Add withCount support (#920) * Add withCount support * add comment * add withCount to transformer * add resource tests; add transformer tests * remove unrelated things and focus only on factoryCreate * check if method exists * use app version * last cleanup * Update TestUserApiResource.php Remove extra line --------- Co-authored-by: Sergey Antonets Co-authored-by: Shalvah --- src/Attributes/ResponseFromApiResource.php | 1 + src/Extracting/InstantiatesExampleModels.php | 16 ++- .../Responses/UseResponseAttributes.php | 2 +- tests/Fixtures/TestUserApiResource.php | 5 + .../Responses/UseResponseAttributesTest.php | 104 +++++++++++++++++- 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/Attributes/ResponseFromApiResource.php b/src/Attributes/ResponseFromApiResource.php index a36e7dbc..80f37006 100644 --- a/src/Attributes/ResponseFromApiResource.php +++ b/src/Attributes/ResponseFromApiResource.php @@ -24,6 +24,7 @@ public function __construct( public ?int $simplePaginate = null, public ?int $cursorPaginate = null, public array $additional = [], + public array $withCount = [], ) { } diff --git a/src/Extracting/InstantiatesExampleModels.php b/src/Extracting/InstantiatesExampleModels.php index 8a73fabd..1f17b59f 100644 --- a/src/Extracting/InstantiatesExampleModels.php +++ b/src/Extracting/InstantiatesExampleModels.php @@ -21,7 +21,7 @@ trait InstantiatesExampleModels */ protected function instantiateExampleModel( ?string $type = null, array $factoryStates = [], - array $relations = [], ?ReflectionFunctionAbstract $transformationMethod = null + array $relations = [], ?ReflectionFunctionAbstract $transformationMethod = null, array $withCount = [], ) { // If the API Resource uses an empty resource, there won't be an example model @@ -42,7 +42,7 @@ protected function instantiateExampleModel( $configuredStrategies = $this->config->get('examples.models_source', ['factoryCreate', 'factoryMake', 'databaseFirst']); $strategies = [ - 'factoryCreate' => fn() => $this->getExampleModelFromFactoryCreate($type, $factoryStates, $relations), + 'factoryCreate' => fn() => $this->getExampleModelFromFactoryCreate($type, $factoryStates, $relations, $withCount), 'factoryMake' => fn() => $this->getExampleModelFromFactoryMake($type, $factoryStates, $relations), 'databaseFirst' => fn() => $this->getExampleModelFromDatabaseFirst($type, $relations), ]; @@ -63,13 +63,19 @@ protected function instantiateExampleModel( /** * @param class-string $type * @param string[] $factoryStates + * @param string[] $relations + * @param string[] $withCount * * @return \Illuminate\Database\Eloquent\Model|null */ - protected function getExampleModelFromFactoryCreate(string $type, array $factoryStates = [], array $relations = []) + protected function getExampleModelFromFactoryCreate(string $type, array $factoryStates = [], array $relations = [], array $withCount = []) { - $factory = Utils::getModelFactory($type, $factoryStates, $relations); - return $factory->create()->refresh()->load($relations); + // Since $relations and $withCount refer to the same underlying relationships in the model, + // combining them ensures that all required relationships are initialized when passed to the factory. + $allRelations = array_unique(array_merge($relations, $withCount)); + + $factory = Utils::getModelFactory($type, $factoryStates, $allRelations); + return $factory->create()->refresh()->load($relations)->loadCount($withCount); } /** diff --git a/src/Extracting/Strategies/Responses/UseResponseAttributes.php b/src/Extracting/Strategies/Responses/UseResponseAttributes.php index 9e19d40d..9fc7534f 100644 --- a/src/Extracting/Strategies/Responses/UseResponseAttributes.php +++ b/src/Extracting/Strategies/Responses/UseResponseAttributes.php @@ -60,7 +60,7 @@ protected function getApiResourceResponse(ResponseFromApiResource $attributeInst ); $modelInstantiator = null; } else { - $modelInstantiator = fn() => $this->instantiateExampleModel($modelToBeTransformed, $attributeInstance->factoryStates, $attributeInstance->with); + $modelInstantiator = fn() => $this->instantiateExampleModel($modelToBeTransformed, $attributeInstance->factoryStates, $attributeInstance->with, null, $attributeInstance->withCount); } $pagination = []; diff --git a/tests/Fixtures/TestUserApiResource.php b/tests/Fixtures/TestUserApiResource.php index 0fe5d8b5..5380eef3 100644 --- a/tests/Fixtures/TestUserApiResource.php +++ b/tests/Fixtures/TestUserApiResource.php @@ -2,6 +2,7 @@ namespace Knuckles\Scribe\Tests\Fixtures; +use Illuminate\Foundation\Application; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -34,6 +35,10 @@ public function toArray($request) }), ]; + if (version_compare(Application::VERSION, '9', '>=')) { + $result['children_count'] = $this->whenCounted('children_count'); + } + if ($this['state1'] && $this['random-state']) { $result['state1'] = $this['state1']; $result['random-state'] = $this['random-state']; diff --git a/tests/Strategies/Responses/UseResponseAttributesTest.php b/tests/Strategies/Responses/UseResponseAttributesTest.php index 18a98390..8e1693c2 100644 --- a/tests/Strategies/Responses/UseResponseAttributesTest.php +++ b/tests/Strategies/Responses/UseResponseAttributesTest.php @@ -2,8 +2,10 @@ namespace Knuckles\Scribe\Tests\Strategies\Responses; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Application; use Illuminate\Routing\Route; +use Illuminate\Support\Facades\Schema; use Knuckles\Camel\Extraction\ExtractedEndpointData; use Knuckles\Scribe\Attributes\Response; use Knuckles\Scribe\Attributes\ResponseFromApiResource; @@ -285,7 +287,95 @@ public function can_parse_apiresource_attributes_with_cursor_pagination() ], $results); } - protected function fetch($endpoint): array + /** @test */ + public function can_parse_apiresource_attributes_and_load_children_using_factory_create() + { + Schema::create('test_users', function (Blueprint $table) { + $table->id(); + $table->string('first_name'); + $table->string('last_name'); + $table->string('email'); + $table->integer('parent_id')->nullable(); + }); + + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterCreating(TestUser::class, function (TestUser $user, $faker) { + if ($user->id === 4) { + Utils::getModelFactory(TestUser::class)->create(['id' => 5, 'parent_id' => 4]); + } + }); + $documentationConfig = ['examples' => ['models_source' => ['factoryCreate']]]; + + $results = $this->fetch($this->endpoint("apiResourceAttributesIncludeChildren"), $documentationConfig); + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + "data" => [ + "id" => 4, + "name" => "Tested Again", + "email" => "a@b.com", + "children" => [ + [ + "id" => 5, + "name" => "Tested Again", + "email" => "a@b.com", + ] + ], + ], + ]), + ], + ], $results); + } + + + /** @test */ + public function can_parse_apiresource_attributes_and_load_children_and_children_count_using_factory_create() + { + if (version_compare(Application::VERSION, '9', '<')) { + $this->markTestSkipped('The whenCounted method in JsonResource requires Laravel 9 or higher.'); + } + + Schema::create('test_users', function (Blueprint $table) { + $table->id(); + $table->string('first_name'); + $table->string('last_name'); + $table->string('email'); + $table->integer('parent_id')->nullable(); + }); + + $factory = app(\Illuminate\Database\Eloquent\Factory::class); + $factory->afterCreating(TestUser::class, function (TestUser $user, $faker) { + if ($user->id === 4) { + Utils::getModelFactory(TestUser::class)->create(['id' => 5, 'parent_id' => 4]); + } + }); + $documentationConfig = ['examples' => ['models_source' => ['factoryCreate']]]; + + $results = $this->fetch($this->endpoint("apiResourceAttributesIncludeChildrenAndChildrenCount"), $documentationConfig); + $this->assertArraySubset([ + [ + 'status' => 200, + 'content' => json_encode([ + "data" => [ + "id" => 4, + "name" => "Tested Again", + "email" => "a@b.com", + "children" => [ + [ + "id" => 5, + "name" => "Tested Again", + "email" => "a@b.com", + ] + ], + 'children_count' => 1, + ], + ]), + ], + ], $results); + } + + protected function fetch($endpoint, array $documentationConfig = []): array { $strategy = new UseResponseAttributes(new DocumentationConfig([])); return $strategy($endpoint, []); @@ -345,4 +435,16 @@ public function apiResourceAttributesWithCursorPaginate() { } + + #[ResponseFromApiResource(TestUserApiResource::class, with: ['children'], withCount: ['children'])] + public function apiResourceAttributesIncludeChildrenAndChildrenCount() + { + + } + + #[ResponseFromApiResource(TestUserApiResource::class, with: ['children'])] + public function apiResourceAttributesIncludeChildren() + { + + } }