Skip to content

Commit

Permalink
feat(console): accept BackedEnum as command arguments (#722)
Browse files Browse the repository at this point in the history
  • Loading branch information
aazsamir authored Nov 13, 2024
1 parent bdf5efc commit c21f24e
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 2 deletions.
22 changes: 22 additions & 0 deletions src/Tempest/Console/src/Actions/RenderConsoleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tempest\Console\Actions;

use BackedEnum;
use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Input\ConsoleArgumentDefinition;
Expand Down Expand Up @@ -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('<em>')
->append('</em>');
Expand All @@ -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 = ' {<em>' . implode('|', $parts) . '</em>}';
$line = "<em>{$argument->name}</em>";

if ($argument->hasDefault) {
return "[{$line}={$argument->default->value}{$partsAsString}]";
}

return "<{$line}{$partsAsString}>";
}
}
40 changes: 40 additions & 0 deletions src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Exceptions;

use function array_map;
use BackedEnum;
use function gettype;
use function implode;
use function is_string;
use Tempest\Console\Console;

final class InvalidEnumArgument extends ConsoleException
{
/**
* @param class-string<BackedEnum> $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));
}
}
29 changes: 28 additions & 1 deletion src/Tempest/Console/src/Input/ConsoleArgumentBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Tempest\Console\Input;

use Tempest\Console\Exceptions\InvalidEnumArgument;

final class ConsoleArgumentBag
{
/** @var ConsoleInputArgument[] */
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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 = [];
Expand Down
6 changes: 6 additions & 0 deletions src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tempest\Console\Input;

use BackedEnum;
use Tempest\Console\ConsoleArgument;
use Tempest\Reflection\ParameterReflector;
use function Tempest\Support\str;
Expand Down Expand Up @@ -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);
}
}
45 changes: 45 additions & 0 deletions tests/Integration/Console/ConsoleArgumentBagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/Integration/Console/Fixtures/MyConsole.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions tests/Integration/Console/Fixtures/TestStringEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\Console\Fixtures;

enum TestStringEnum: string
{
case A = 'a';
case B = 'b';
case C = 'c';
}
2 changes: 1 addition & 1 deletion tests/Integration/Console/RenderConsoleCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function test_render(): void
(new RenderConsoleCommand($console))($consoleCommand);

$this->assertSame(
'test <path> [times=1] [--force=false] - description',
'test <path> <type {a|b|c}> [fallback=a {a|b|c}] [times=1] [--force=false] - description',
trim($output->getBufferWithoutFormatting()[0]),
);
}
Expand Down

0 comments on commit c21f24e

Please # to comment.