diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a1432..ae8095f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [10.9.0] - 2024.08.16 +### Added +- `INCLUDE_TAGS` and `EXCLUDE_TAGS` to whitelist/blacklist client generation based on operation tags + ## [10.8.1] - 2024.06.04 ### Fixed - Enums for path parameters diff --git a/README.md b/README.md index a1dfc32..13048ce 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ API client generator is a console application capable of auto-generating a [PSR1 - It is base client independent, you are free to choose any [existing PSR-18 compliant client](https://packagist.org/providers/psr/http-client-implementation). Just choose the one which you already use, so generated client would not cause any conflicts with your dependencies. Although not recommended, you can also use or build your own PSR-18 implementation, as the generated client depends on PSR interfaces only. - Applies code style rules to generated code, you can specify your own. - Generates README and composer.json files with possibility to use your own template. -- Supports `allOf` OpenAPI parameter. +- Supports `allOf`, `oneOf`, `anyOf` OpenAPI parameters. - Supports nullable optional scheme property. ## Example @@ -68,8 +68,8 @@ OPENAPI={path-to-specification}/openapi.yaml NAMESPACE=Group\SomeApiClient PACKA ## Configuration The following environment variables are available: -| Variable | Required | Default | Enum | Example | -|------------|---------|------------------|---------|---------------------------| +| Variable | Required | Default | Enum | Example | Description | +|------------|---------|------------------|---------|---------------------------|----------------------------| | `NAMESPACE` | yes | | | Group\\SomeApiClient | | `PACKAGE` | yes | | | group/some-api-client | | `OPENAPI ` | yes | | | /api/openapi.yaml | @@ -81,6 +81,8 @@ The following environment variables are available: | `README_MD_TEMPLATE_DIR` | no | {path-to-repository}/template/README.md.twig | | /path/README.md.twig | | `HTTP_MESSAGE` | no | guzzle | guzzle, nyholm | nyholm | | `CONTAINER` | no | pimple | pimple | pimple | +| `INCLUDE_TAGS` | no | | | tag1,tag2,tag3 | tag whitelist to select generated operations | +| `EXCLUDE_TAGS` | no | | | tag1,tag2,tag3 | tag blacklist to select generated operations | ## Running tests diff --git a/src/Generator/ClientGenerator.php b/src/Generator/ClientGenerator.php index 7447f23..3782889 100644 --- a/src/Generator/ClientGenerator.php +++ b/src/Generator/ClientGenerator.php @@ -4,9 +4,11 @@ namespace DoclerLabs\ApiClientGenerator\Generator; +use DoclerLabs\ApiClientGenerator\Ast\Builder\CodeBuilder; use DoclerLabs\ApiClientGenerator\Ast\Builder\MethodBuilder; use DoclerLabs\ApiClientGenerator\Ast\Builder\ParameterBuilder; use DoclerLabs\ApiClientGenerator\Ast\ParameterNode; +use DoclerLabs\ApiClientGenerator\Ast\PhpVersion; use DoclerLabs\ApiClientGenerator\Entity\Field; use DoclerLabs\ApiClientGenerator\Entity\Operation; use DoclerLabs\ApiClientGenerator\Entity\Response; @@ -33,6 +35,16 @@ class ClientGenerator extends GeneratorAbstract { + public function __construct( + string $baseNamespace, + CodeBuilder $builder, + PhpVersion $phpVersion, + private array $includeTags, + private array $excludeTags + ) { + parent::__construct($baseNamespace, $builder, $phpVersion); + } + public function generate(Specification $specification, PhpFileCollection $fileRegistry): void { $classBuilder = $this->builder @@ -42,6 +54,19 @@ public function generate(Specification $specification, PhpFileCollection $fileRe ->addStmt($this->generateSendRequestMethod()); foreach ($specification->getOperations() as $operation) { + if ( + ( + !empty($this->includeTags) + && empty(array_intersect($operation->tags, $this->includeTags)) + ) + || ( + !empty($this->excludeTags) + && !empty(array_intersect($operation->tags, $this->excludeTags)) + ) + ) { + continue; + } + $classBuilder->addStmt($this->generateAction($operation)); } diff --git a/src/Input/Configuration.php b/src/Input/Configuration.php index 6a89eff..ff99f38 100644 --- a/src/Input/Configuration.php +++ b/src/Input/Configuration.php @@ -27,6 +27,10 @@ class Configuration public const STATIC_PHP_FILE_DIRECTORY = __DIR__ . '/../Output/Copy'; + /** + * @param string[] $includeTags + * @param string[] $excludeTags + */ public function __construct( public readonly string $specificationFilePath, public readonly string $baseNamespace, @@ -39,7 +43,9 @@ public function __construct( public readonly string $composerJsonTemplateDir, public readonly string $readmeMdTemplateDir, public readonly string $httpMessage, - public readonly string $container + public readonly string $container, + public readonly array $includeTags, + public readonly array $excludeTags ) { Assert::notEmpty($specificationFilePath, 'Specification file path is not provided.'); Assert::notEmpty($baseNamespace, 'Namespace for generated code is not provided.'); diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index f99ad21..7b4754f 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -94,6 +94,8 @@ public function register(Container $pimple): void getenv('README_MD_TEMPLATE_DIR') ?: Configuration::DEFAULT_TEMPLATE_DIRECTORY, getenv('HTTP_MESSAGE') ?: Configuration::DEFAULT_HTTP_MESSAGE, getenv('CONTAINER') ?: Configuration::DEFAULT_CONTAINER, + array_filter(explode(',', (string)getenv('INCLUDE_TAGS'))), + array_filter(explode(',', (string)getenv('EXCLUDE_TAGS'))) ); $pimple[GenerateCommand::class] = static fn (Container $container) => new GenerateCommand( @@ -204,7 +206,9 @@ public function register(Container $pimple): void $pimple[ClientGenerator::class] = static fn (Container $container) => new ClientGenerator( $container[Configuration::class]->baseNamespace, $container[CodeBuilder::class], - $container[PhpVersion::class] + $container[PhpVersion::class], + $container[Configuration::class]->includeTags, + $container[Configuration::class]->excludeTags ); $pimple[SchemaGenerator::class] = static fn (Container $container) => new SchemaGenerator( diff --git a/test/suite/functional/ConfigurationBuilder.php b/test/suite/functional/ConfigurationBuilder.php index 69c7be3..ecd5ecb 100644 --- a/test/suite/functional/ConfigurationBuilder.php +++ b/test/suite/functional/ConfigurationBuilder.php @@ -32,6 +32,10 @@ class ConfigurationBuilder private string $container; + private array $includeTags; + + private array $excludeTags; + private function __construct() { $this->specificationFilePath = '/dir/path/openapi.yaml'; @@ -46,6 +50,8 @@ private function __construct() $this->readmeMdTemplateDir = Configuration::DEFAULT_TEMPLATE_DIRECTORY; $this->httpMessage = Configuration::DEFAULT_HTTP_MESSAGE; $this->container = Configuration::DEFAULT_CONTAINER; + $this->includeTags = []; + $this->excludeTags = []; } public static function fake(): self @@ -130,6 +136,20 @@ public function withContainer(string $container): self return $this; } + public function withIncludeTags(array $includeTags): self + { + $this->includeTags = $includeTags; + + return $this; + } + + public function withExcludeTags(array $excludeTags): self + { + $this->excludeTags = $excludeTags; + + return $this; + } + public function build(): Configuration { return new Configuration( @@ -144,7 +164,9 @@ public function build(): Configuration $this->composerJsonTemplateDir, $this->readmeMdTemplateDir, $this->httpMessage, - $this->container + $this->container, + $this->includeTags, + $this->excludeTags ); } } diff --git a/test/suite/functional/Generator/Client/SwaggerPetstoreClientWithTags.php b/test/suite/functional/Generator/Client/SwaggerPetstoreClientWithTags.php new file mode 100644 index 0000000..c935557 --- /dev/null +++ b/test/suite/functional/Generator/Client/SwaggerPetstoreClientWithTags.php @@ -0,0 +1,61 @@ +client->sendRequest($this->container->get(RequestMapperInterface::class)->map($request)); + } + + public function findPets(FindPetsRequest $request): PetCollection + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $this->container->get(PetCollectionMapper::class)->toSchema($response); + } + + public function addPet(AddPetRequest $request): Pet + { + $response = $this->handleResponse($this->sendRequest($request)); + + return $this->container->get(PetMapper::class)->toSchema($response); + } + + public function countPets(CountPetsRequest $request): void + { + $this->handleResponse($this->sendRequest($request)); + } + + protected function handleResponse(ResponseInterface $response) + { + return $this->container->get(ResponseHandler::class)->handle($response); + } +} diff --git a/test/suite/functional/Generator/Client/petstore.yaml b/test/suite/functional/Generator/Client/petstore.yaml index ef678bc..8bb1105 100644 --- a/test/suite/functional/Generator/Client/petstore.yaml +++ b/test/suite/functional/Generator/Client/petstore.yaml @@ -9,6 +9,8 @@ paths: /pets: get: operationId: find-pets + tags: + - tag1 parameters: - name: tags in: query @@ -43,6 +45,8 @@ paths: $ref: '#/components/schemas/Error' post: operationId: addPet + tags: + - tag1 requestBody: description: Pet to add to the store required: true @@ -66,7 +70,7 @@ paths: head: operationId: countPets tags: - - Pet + - tag2 responses: 200: description: Count pets @@ -78,6 +82,8 @@ paths: /pets/{id}: get: operationId: findPetById + tags: + - tag3 parameters: - name: id in: path @@ -102,6 +108,8 @@ paths: /pets/{id}/pet-food/{food_id}: delete: description: no operation id provided + tags: + - tag4 parameters: - name: id in: path diff --git a/test/suite/functional/Generator/ClientGeneratorTest.php b/test/suite/functional/Generator/ClientGeneratorTest.php index 1fd6278..a46dca8 100644 --- a/test/suite/functional/Generator/ClientGeneratorTest.php +++ b/test/suite/functional/Generator/ClientGeneratorTest.php @@ -52,6 +52,18 @@ public function exampleProvider(): array self::BASE_NAMESPACE . '\\MultipleResponsesClient', ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->build(), ], + 'Basic schema with php 8.1 and include tags' => [ + '/Client/petstore.yaml', + '/Client/SwaggerPetstoreClientWithTags.php', + self::BASE_NAMESPACE . '\\SwaggerPetstoreClient', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->withIncludeTags(['tag1', 'tag2'])->build(), + ], + 'Basic schema with php 8.1 and exclude tags' => [ + '/Client/petstore.yaml', + '/Client/SwaggerPetstoreClientWithTags.php', + self::BASE_NAMESPACE . '\\SwaggerPetstoreClient', + ConfigurationBuilder::fake()->withPhpVersion(PhpVersion::VERSION_PHP81)->withExcludeTags(['tag3', 'tag4'])->build(), + ], ]; } diff --git a/test/suite/unit/Input/ConfigurationTest.php b/test/suite/unit/Input/ConfigurationTest.php index 4fcfac0..3da6709 100644 --- a/test/suite/unit/Input/ConfigurationTest.php +++ b/test/suite/unit/Input/ConfigurationTest.php @@ -27,7 +27,9 @@ public function testValidConfiguration( string $composerJsonTemplateDir, string $readmeMdTemplateDir, string $httpMessage, - string $container + string $container, + array $includeTags, + array $excludeTags ): void { $sut = new Configuration( $openapiFilePath, @@ -41,7 +43,9 @@ public function testValidConfiguration( $composerJsonTemplateDir, $readmeMdTemplateDir, $httpMessage, - $container + $container, + $includeTags, + $excludeTags ); self::assertEquals($openapiFilePath, $sut->specificationFilePath); @@ -56,6 +60,8 @@ public function testValidConfiguration( self::assertEquals($readmeMdTemplateDir, $sut->readmeMdTemplateDir); self::assertEquals($httpMessage, $sut->httpMessage); self::assertEquals($container, $sut->container); + self::assertEquals($includeTags, $sut->includeTags); + self::assertEquals($excludeTags, $sut->excludeTags); } public function validConfigurationOptions(): array @@ -74,6 +80,8 @@ public function validConfigurationOptions(): array __DIR__, Configuration::DEFAULT_HTTP_MESSAGE, Configuration::DEFAULT_CONTAINER, + ['tag1', 'tag2'], + ['tag3', 'tag4'], ], ]; }