From 37c46ebaef2a1b7562c2bf05bd6d7c73eab0dcfb Mon Sep 17 00:00:00 2001 From: Brandon Ferens Date: Tue, 29 Oct 2024 14:05:46 -0700 Subject: [PATCH] Generate As feature (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Generate As feature Ability to set preferred language Also some type coverage enhancements and cleanup * L10 support * l10 support * Fixed test on 8.1 - L10 * Fixed code style --------- Co-authored-by: Luís Dalmolin --- .github/workflows/run-tests.yml | 4 +- composer.json | 3 +- config/paragon.php | 28 +++++++ src/Commands/GenerateEnumsCommand.php | 26 ++++-- src/Commands/MakeEnumMethodCommand.php | 2 +- src/Concerns/Builders/EnumJsBuilder.php | 5 +- src/Concerns/Builders/EnumTsBuilder.php | 5 +- src/Concerns/DiscoverEnums.php | 19 +++-- src/Concerns/GenerateAs.php | 25 ++++++ src/Generators/AbstractEnumGenerator.php | 4 +- src/Generators/EnumGenerator.php | 22 +++--- .../Commands/GenerateEnumsCommandTest.php | 79 +++++++++++++++++++ 12 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 src/Concerns/GenerateAs.php diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 32cd2b6..5797e5a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,4 @@ -name: run-tests +name: CI on: push: @@ -62,7 +62,7 @@ jobs: run: | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev - composer require "pestphp/pest:${{ matrix.pest }}" "pestphp/pest-plugin-laravel:${{ matrix.pest }}" --no-interaction --no-update --dev + composer require "pestphp/pest:${{ matrix.pest }}" "pestphp/pest-plugin-laravel:${{ matrix.pest }}" "pestphp/pest-plugin-type-coverage:${{ matrix.pest }}" --no-interaction --no-update --dev composer update --prefer-dist --no-interaction --no-suggest --dev composer dump diff --git a/composer.json b/composer.json index 6f1cb73..9281f45 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "laravel/pint": "^1.18", "orchestra/testbench": "^9.5", "pestphp/pest": "^3.2", - "pestphp/pest-plugin-laravel": "^3.0" + "pestphp/pest-plugin-laravel": "^3.0", + "pestphp/pest-plugin-type-coverage": "^3.1" }, "autoload": { "psr-4": { diff --git a/config/paragon.php b/config/paragon.php index 514e003..3675a95 100644 --- a/config/paragon.php +++ b/config/paragon.php @@ -1,6 +1,34 @@ 'typescript', + + /* + |-------------------------------------------------------------------------- + | Paragon Enums + |-------------------------------------------------------------------------- + | + | Here you may specify the settings for enum code generation. You have the ability to change + | file paths for locating php enums, what to ignore, and where the generated files should + | be placed. By default, Paragon will look in your entire app/ directory for all enums. + | + */ + 'enums' => [ 'abstract-class' => 'Enum', diff --git a/src/Commands/GenerateEnumsCommand.php b/src/Commands/GenerateEnumsCommand.php index 3fcbe98..a09e8da 100644 --- a/src/Commands/GenerateEnumsCommand.php +++ b/src/Commands/GenerateEnumsCommand.php @@ -10,12 +10,14 @@ use Kirschbaum\Paragon\Concerns\Builders\EnumJsBuilder; use Kirschbaum\Paragon\Concerns\Builders\EnumTsBuilder; use Kirschbaum\Paragon\Concerns\DiscoverEnums; +use Kirschbaum\Paragon\Concerns\GenerateAs; use Kirschbaum\Paragon\Concerns\IgnoreParagon; use Kirschbaum\Paragon\Generators\AbstractEnumGenerator; use Kirschbaum\Paragon\Generators\EnumGenerator; use ReflectionEnum; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; +use UnitEnum; #[AsCommand(name: 'paragon:generate-enums', description: 'Generate Typescript versions of existing PHP enums')] class GenerateEnumsCommand extends Command @@ -28,7 +30,7 @@ public function handle(): int $builder = $this->builder(); $generatedEnums = $this->enums() - ->map(fn ($enum) => app(EnumGenerator::class, ['enum' => $enum, 'builder' => $builder])()) + ->map(fn (string $enum) => app(EnumGenerator::class, ['enum' => $enum, 'builder' => $builder])()) ->filter(); $this->components->info("{$generatedEnums->count()} enums have been (re)generated."); @@ -43,7 +45,7 @@ public function handle(): int /** * Gather all enum namespaces for searching. * - * @return Collection> + * @return Collection> */ protected function enums(): Collection { @@ -51,7 +53,7 @@ protected function enums(): Collection $phpPath = config('paragon.enums.paths.php'); return DiscoverEnums::within(app_path($phpPath)) - ->reject(function ($enum) { + ->reject(function (string $enum) { if (! enum_exists($enum)) { return true; } @@ -71,10 +73,16 @@ protected function enums(): Collection protected function builder(): EnumBuilder { - return $this->option('javascript') - ? app(EnumJsBuilder::class) - : app(EnumTsBuilder::class); + /** @var string */ + $generateAs = config('paragon.generate-as'); + + $builder = match (true) { + $this->option('javascript') => EnumJsBuilder::class, + $this->option('typescript') => EnumTsBuilder::class, + default => GenerateAs::from($generateAs)->builder() + }; + return app($builder); } /** @@ -91,6 +99,12 @@ protected function getOptions(): array mode: InputOption::VALUE_NONE, description: 'Output Javascript files', ), + new InputOption( + name: 'typescript', + shortcut: 't', + mode: InputOption::VALUE_NONE, + description: 'Output TypeScript files', + ), ]; } } diff --git a/src/Commands/MakeEnumMethodCommand.php b/src/Commands/MakeEnumMethodCommand.php index 3673197..f0d9c94 100644 --- a/src/Commands/MakeEnumMethodCommand.php +++ b/src/Commands/MakeEnumMethodCommand.php @@ -21,8 +21,8 @@ class MakeEnumMethodCommand extends GeneratorCommand /** * Execute the console command. * - * @throws FileNotFoundException * @throws Exception + * @throws FileNotFoundException */ public function handle(): ?bool { diff --git a/src/Concerns/Builders/EnumJsBuilder.php b/src/Concerns/Builders/EnumJsBuilder.php index 53d98bf..11faa06 100644 --- a/src/Concerns/Builders/EnumJsBuilder.php +++ b/src/Concerns/Builders/EnumJsBuilder.php @@ -3,6 +3,7 @@ namespace Kirschbaum\Paragon\Concerns\Builders; use BackedEnum; +use ReflectionEnumBackedCase; use ReflectionEnumUnitCase; use ReflectionMethod; @@ -35,7 +36,7 @@ public function fileExtension(): string /** * Prepare the method and its respective values so it can get injected into the case object. */ - public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase $case): string + public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string { $value = $case->getValue()->{$method->getName()}(); $class = class_basename($method->getDeclaringClass()->getName()); @@ -53,7 +54,7 @@ public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase $cas /** * Assemble the static getter method code for the enum case object. */ - public function assembleCaseGetter(ReflectionEnumUnitCase $case): string + public function assembleCaseGetter(ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string { return <<name}() { diff --git a/src/Concerns/Builders/EnumTsBuilder.php b/src/Concerns/Builders/EnumTsBuilder.php index e3dc99f..9354de1 100644 --- a/src/Concerns/Builders/EnumTsBuilder.php +++ b/src/Concerns/Builders/EnumTsBuilder.php @@ -3,6 +3,7 @@ namespace Kirschbaum\Paragon\Concerns\Builders; use BackedEnum; +use ReflectionEnumBackedCase; use ReflectionEnumUnitCase; use ReflectionMethod; @@ -35,7 +36,7 @@ public function fileExtension(): string /** * Prepare the method and its respective values so it can get injected into the case object. */ - public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase $case): string + public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string { $value = $case->getValue()->{$method->getName()}(); $class = class_basename($method->getDeclaringClass()->getName()); @@ -53,7 +54,7 @@ public function caseMethod(ReflectionMethod $method, ReflectionEnumUnitCase $cas /** * Assemble the static getter method code for the enum case object. */ - public function assembleCaseGetter(ReflectionEnumUnitCase $case): string + public function assembleCaseGetter(ReflectionEnumUnitCase|ReflectionEnumBackedCase $case): string { $class = class_basename($case->getDeclaringClass()->name); diff --git a/src/Concerns/DiscoverEnums.php b/src/Concerns/DiscoverEnums.php index cc1b980..1ccf98d 100644 --- a/src/Concerns/DiscoverEnums.php +++ b/src/Concerns/DiscoverEnums.php @@ -7,15 +7,16 @@ use ReflectionException; use SplFileInfo; use Symfony\Component\Finder\Finder; +use UnitEnum; class DiscoverEnums { /** * Get all the enums by searching the given directory. * - * @param array|string $path + * @param array|string $path * - * @return Collection,class-string<\UnitEnum>> + * @return Collection, class-string> */ public static function within(array|string $path): Collection { @@ -25,13 +26,15 @@ public static function within(array|string $path): Collection /** * Filter the files down to only enums. * - * @param Finder $files + * @param Finder $files * - * @return Collection,class-string<\UnitEnum>> + * @return Collection, class-string> */ protected static function getEnums(Finder $files): Collection { - /** @var Collection $fileCollection */ + /** + * @var Collection $fileCollection + */ $fileCollection = collect($files); return $fileCollection @@ -56,11 +59,13 @@ protected static function getEnums(Finder $files): Collection /** * Extract the class name from the given file path. * - * @return class-string<\UnitEnum> + * @return class-string */ protected static function classFromFile(SplFileInfo $file): string { - /** @var class-string<\UnitEnum> */ + /** + * @var class-string + */ return str($file->getRealPath()) ->replaceFirst(base_path(), '') ->trim(DIRECTORY_SEPARATOR) diff --git a/src/Concerns/GenerateAs.php b/src/Concerns/GenerateAs.php new file mode 100644 index 0000000..3a338fb --- /dev/null +++ b/src/Concerns/GenerateAs.php @@ -0,0 +1,25 @@ + + */ + public function builder(): string + { + return match ($this) { + self::Javascript => EnumJsBuilder::class, + self::TypeScript => EnumTsBuilder::class, + }; + } +} diff --git a/src/Generators/AbstractEnumGenerator.php b/src/Generators/AbstractEnumGenerator.php index 9169558..2226580 100644 --- a/src/Generators/AbstractEnumGenerator.php +++ b/src/Generators/AbstractEnumGenerator.php @@ -64,7 +64,9 @@ protected function imports(): Collection return collect(); } - /** @var Collection $fileCollection */ + /** + * @var Collection $fileCollection + */ $fileCollection = collect($files); return $fileCollection diff --git a/src/Generators/EnumGenerator.php b/src/Generators/EnumGenerator.php index f47dc82..da3b975 100644 --- a/src/Generators/EnumGenerator.php +++ b/src/Generators/EnumGenerator.php @@ -7,12 +7,14 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Fluent; +use Illuminate\Support\Stringable; use Kirschbaum\Paragon\Concerns\Builders\EnumBuilder; use Kirschbaum\Paragon\Concerns\IgnoreParagon; use ReflectionEnum; use ReflectionEnumBackedCase; use ReflectionEnumUnitCase; use ReflectionMethod; +use UnitEnum; class EnumGenerator { @@ -25,7 +27,7 @@ class EnumGenerator /** * Create new EnumGenerator instance. * - * @param class-string<\UnitEnum> $enum + * @param class-string $enum */ public function __construct(protected string $enum, protected EnumBuilder $builder) { @@ -112,11 +114,11 @@ protected function relativePath(): string protected function buildTypeDefinition(): string { return $this->methods() - ->map(fn ($method) => PHP_EOL . " {$method->getName()}();") + ->map(fn (ReflectionMethod $method) => PHP_EOL . " {$method->getName()}();") ->sortDesc() ->when( $this->reflector->isBacked(), - fn ($collection) => $collection->push(PHP_EOL . " value: {$this->valueReturnType()};") + fn (Collection $collection) => $collection->push(PHP_EOL . " value: {$this->valueReturnType()};") ) ->reverse() ->join(''); @@ -147,12 +149,12 @@ protected function valueReturnType(): string /** * Build all the case objects. * - * @param Collection $cases + * @param Collection $cases */ protected function buildCases(Collection $cases): string { return $cases - ->map(function (ReflectionEnumUnitCase $case) { + ->map(function (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) { $value = $this->caseValueProperty($case); $methodValues = $this->methods() @@ -173,10 +175,10 @@ protected function caseValueProperty(ReflectionEnumUnitCase|ReflectionEnumBacked ->prepend(PHP_EOL . ' ') ->when( $this->reflector->getBackingType()->getName() === 'int', - fn ($string) => $case->getValue() instanceof BackedEnum + fn (Stringable $string) => $case->getValue() instanceof BackedEnum ? $string->append("{$case->getValue()->value}") : $string, - fn ($string) => $case->getValue() instanceof BackedEnum + fn (Stringable $string) => $case->getValue() instanceof BackedEnum ? $string->append("'{$case->getValue()->value}'") : $string, ) @@ -189,7 +191,7 @@ protected function caseValueProperty(ReflectionEnumUnitCase|ReflectionEnumBacked /** * Assemble the actual enum case object code including the name, value if needed, and any public methods. * - * @param Collection $methodValues + * @param Collection $methodValues */ protected function assembleCaseObject( ReflectionEnumUnitCase|ReflectionEnumBackedCase $case, @@ -208,12 +210,12 @@ protected function assembleCaseObject( /** * Build all case object getter methods. * - * @param Collection $cases + * @param Collection $cases */ protected function buildGetters(Collection $cases): string { return $cases - ->map(fn ($case) => $this->builder->assembleCaseGetter($case)) + ->map(fn (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => $this->builder->assembleCaseGetter($case)) ->join(PHP_EOL . PHP_EOL); } diff --git a/tests/Unit/Commands/GenerateEnumsCommandTest.php b/tests/Unit/Commands/GenerateEnumsCommandTest.php index a62e345..0b1eaf3 100644 --- a/tests/Unit/Commands/GenerateEnumsCommandTest.php +++ b/tests/Unit/Commands/GenerateEnumsCommandTest.php @@ -144,3 +144,82 @@ expect(resource_path(config('paragon.enums.paths.generated') . '/Ignore/Ignore.ts')) ->not->toBeFile(); }); + +describe('command flags and config settings for typescript', function () { + it('builds typescript by default', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.ts'))->toBeFile(); + }); + + it('builds javascript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--javascript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.js'))->toBeFile(); + }); + + it('builds typescript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--typescript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.ts'))->toBeFile(); + }); +}); + +describe('command flags and config settings for javascript', function () { + beforeEach(fn () => $this->app['config']->set('paragon.generate-as', 'javascript')); + + it('builds javascript by default', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.js'))->toBeFile(); + }); + + it('builds typescript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--typescript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.ts'))->toBeFile(); + }); + + it('builds javascript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--javascript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.js'))->toBeFile(); + }); +}); + +describe('command flags and config settings exceptions', function () { + beforeEach(fn () => $this->app['config']->set('paragon.generate-as', 'exception')); + + it('throws exception with bad default', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class); + })->throws(ValueError::class, '"exception" is not a valid backing value for enum'); + + it('builds javascript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--javascript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.js'))->toBeFile(); + }); + + it('builds typescript via flag', function () { + // Act. + $this->artisan(GenerateEnumsCommand::class, ['--typescript' => true]); + + // Assert. + expect(resource_path(config('paragon.enums.paths.generated') . '/StringBacked.ts'))->toBeFile(); + }); +});