From c21f24e7816567dbdc93ea064857b653966ac3d0 Mon Sep 17 00:00:00 2001 From: samir Date: Wed, 13 Nov 2024 23:09:26 +0100 Subject: [PATCH] feat(console): accept `BackedEnum` as command arguments (#722) --- .../src/Actions/RenderConsoleCommand.php | 22 +++++++++ .../src/Exceptions/InvalidEnumArgument.php | 40 +++++++++++++++++ .../Console/src/Input/ConsoleArgumentBag.php | 29 +++++++++++- .../src/Input/ConsoleArgumentDefinition.php | 6 +++ .../Console/ConsoleArgumentBagTest.php | 45 +++++++++++++++++++ .../Console/Fixtures/MyConsole.php | 2 + .../Console/Fixtures/TestStringEnum.php | 12 +++++ .../Console/RenderConsoleCommandTest.php | 2 +- 8 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php create mode 100644 tests/Integration/Console/Fixtures/TestStringEnum.php diff --git a/src/Tempest/Console/src/Actions/RenderConsoleCommand.php b/src/Tempest/Console/src/Actions/RenderConsoleCommand.php index a5c8ab22f..f650398cd 100644 --- a/src/Tempest/Console/src/Actions/RenderConsoleCommand.php +++ b/src/Tempest/Console/src/Actions/RenderConsoleCommand.php @@ -4,6 +4,7 @@ namespace Tempest\Console\Actions; +use BackedEnum; use Tempest\Console\Console; use Tempest\Console\ConsoleCommand; use Tempest\Console\Input\ConsoleArgumentDefinition; @@ -32,6 +33,10 @@ public function __invoke(ConsoleCommand $consoleCommand): void private function renderArgument(ConsoleArgumentDefinition $argument): string { + if ($argument->isBackedEnum()) { + return $this->renderEnumArgument($argument); + } + $name = str($argument->name) ->prepend('') ->append(''); @@ -53,4 +58,21 @@ private function renderArgument(ConsoleArgumentDefinition $argument): string default => "[{$asString}={$argument->default}]" }; } + + private function renderEnumArgument(ConsoleArgumentDefinition $argument): string + { + $parts = array_map( + callback: fn (BackedEnum $case) => $case->value, + array: $argument->type::cases() + ); + + $partsAsString = ' {' . implode('|', $parts) . '}'; + $line = "{$argument->name}"; + + if ($argument->hasDefault) { + return "[{$line}={$argument->default->value}{$partsAsString}]"; + } + + return "<{$line}{$partsAsString}>"; + } } diff --git a/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php b/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php new file mode 100644 index 000000000..a060d49f8 --- /dev/null +++ b/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php @@ -0,0 +1,40 @@ + $argumentType + */ + public function __construct( + private string $argumentName, + private string $argumentType, + private mixed $value, + ) { + } + + public function render(Console $console): void + { + if (is_string($this->value) || is_numeric($this->value)) { + $value = "`{$this->value}`"; + } else { + $value = 'of type `' . gettype($this->value) . '`'; + } + + $cases = array_map( + callback: fn (BackedEnum $case) => $case->value, + array: $this->argumentType::cases(), + ); + $console->error("Invalid argument {$value} for `{$this->argumentName}` argument, valid values are: " . implode(', ', $cases)); + } +} diff --git a/src/Tempest/Console/src/Input/ConsoleArgumentBag.php b/src/Tempest/Console/src/Input/ConsoleArgumentBag.php index 24331cd22..73764aa2a 100644 --- a/src/Tempest/Console/src/Input/ConsoleArgumentBag.php +++ b/src/Tempest/Console/src/Input/ConsoleArgumentBag.php @@ -4,6 +4,8 @@ namespace Tempest\Console\Input; +use Tempest\Console\Exceptions\InvalidEnumArgument; + final class ConsoleArgumentBag { /** @var ConsoleInputArgument[] */ @@ -93,7 +95,7 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console { foreach ($this->arguments as $argument) { if ($argumentDefinition->matchesArgument($argument)) { - return $argument; + return $this->resolveArgumentValue($argumentDefinition, $argument); } } @@ -108,6 +110,31 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console return null; } + private function resolveArgumentValue( + ConsoleArgumentDefinition $argumentDefinition, + ConsoleInputArgument $argument, + ): ConsoleInputArgument { + if (! $argumentDefinition->isBackedEnum()) { + return $argument; + } + + $resolved = $argumentDefinition->type::tryFrom($argument->value); + + if ($resolved === null) { + throw new InvalidEnumArgument( + $argumentDefinition->name, + $argumentDefinition->type, + $argument->value, + ); + } + + return new ConsoleInputArgument( + name: $argumentDefinition->name, + position: $argumentDefinition->position, + value: $resolved, + ); + } + public function findArrayFor(ConsoleArgumentDefinition $argumentDefinition): ?ConsoleInputArgument { $values = []; diff --git a/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php b/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php index 751d70988..f9e8a4474 100644 --- a/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php +++ b/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php @@ -4,6 +4,7 @@ namespace Tempest\Console\Input; +use BackedEnum; use Tempest\Console\ConsoleArgument; use Tempest\Reflection\ParameterReflector; use function Tempest\Support\str; @@ -70,4 +71,9 @@ private static function normalizeName(string $name, bool $boolean): string return $normalizedName->toString(); } + + public function isBackedEnum(): bool + { + return is_subclass_of($this->type, BackedEnum::class); + } } diff --git a/tests/Integration/Console/ConsoleArgumentBagTest.php b/tests/Integration/Console/ConsoleArgumentBagTest.php index 5935f3c79..12e815e92 100644 --- a/tests/Integration/Console/ConsoleArgumentBagTest.php +++ b/tests/Integration/Console/ConsoleArgumentBagTest.php @@ -5,8 +5,10 @@ namespace Tests\Tempest\Integration\Console; use PHPUnit\Framework\Attributes\TestWith; +use Tempest\Console\Exceptions\InvalidEnumArgument; use Tempest\Console\Input\ConsoleArgumentBag; use Tempest\Console\Input\ConsoleArgumentDefinition; +use Tests\Tempest\Integration\Console\Fixtures\TestStringEnum; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** @@ -138,6 +140,49 @@ public function test_negative_input(string $name, bool $expected): void $this->assertSame($expected, $bag->findFor($definition)->value); } + public function test_backed_enum_input(): void + { + $argv = [ + 'tempest', + 'test', + '--type=a', + ]; + + $bag = new ConsoleArgumentBag($argv); + + $definition = new ConsoleArgumentDefinition( + name: 'type', + type: TestStringEnum::class, + default: null, + hasDefault: false, + position: 0, + ); + + $this->assertSame(TestStringEnum::A, $bag->findFor($definition)->value); + } + + public function test_invalid_backed_enum_input(): void + { + $argv = [ + 'tempest', + 'test', + '--type=invalid', + ]; + + $bag = new ConsoleArgumentBag($argv); + + $definition = new ConsoleArgumentDefinition( + name: 'type', + type: TestStringEnum::class, + default: null, + hasDefault: false, + position: 0, + ); + + $this->expectException(InvalidEnumArgument::class); + $bag->findFor($definition); + } + public function test_name_mapping(): void { $this->console diff --git a/tests/Integration/Console/Fixtures/MyConsole.php b/tests/Integration/Console/Fixtures/MyConsole.php index a1d278810..55fc16bb1 100644 --- a/tests/Integration/Console/Fixtures/MyConsole.php +++ b/tests/Integration/Console/Fixtures/MyConsole.php @@ -14,6 +14,8 @@ final class MyConsole )] public function handle( string $path, + TestStringEnum $type, + TestStringEnum $fallback = TestStringEnum::A, int $times = 1, bool $force = false, ): void { diff --git a/tests/Integration/Console/Fixtures/TestStringEnum.php b/tests/Integration/Console/Fixtures/TestStringEnum.php new file mode 100644 index 000000000..73d2947f1 --- /dev/null +++ b/tests/Integration/Console/Fixtures/TestStringEnum.php @@ -0,0 +1,12 @@ +assertSame( - 'test [times=1] [--force=false] - description', + 'test [fallback=a {a|b|c}] [times=1] [--force=false] - description', trim($output->getBufferWithoutFormatting()[0]), ); }