Skip to content

Commit c21f24e

Browse files
authored
feat(console): accept BackedEnum as command arguments (#722)
1 parent bdf5efc commit c21f24e

File tree

8 files changed

+156
-2
lines changed

8 files changed

+156
-2
lines changed

src/Tempest/Console/src/Actions/RenderConsoleCommand.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tempest\Console\Actions;
66

7+
use BackedEnum;
78
use Tempest\Console\Console;
89
use Tempest\Console\ConsoleCommand;
910
use Tempest\Console\Input\ConsoleArgumentDefinition;
@@ -32,6 +33,10 @@ public function __invoke(ConsoleCommand $consoleCommand): void
3233

3334
private function renderArgument(ConsoleArgumentDefinition $argument): string
3435
{
36+
if ($argument->isBackedEnum()) {
37+
return $this->renderEnumArgument($argument);
38+
}
39+
3540
$name = str($argument->name)
3641
->prepend('<em>')
3742
->append('</em>');
@@ -53,4 +58,21 @@ private function renderArgument(ConsoleArgumentDefinition $argument): string
5358
default => "[{$asString}={$argument->default}]"
5459
};
5560
}
61+
62+
private function renderEnumArgument(ConsoleArgumentDefinition $argument): string
63+
{
64+
$parts = array_map(
65+
callback: fn (BackedEnum $case) => $case->value,
66+
array: $argument->type::cases()
67+
);
68+
69+
$partsAsString = ' {<em>' . implode('|', $parts) . '</em>}';
70+
$line = "<em>{$argument->name}</em>";
71+
72+
if ($argument->hasDefault) {
73+
return "[{$line}={$argument->default->value}{$partsAsString}]";
74+
}
75+
76+
return "<{$line}{$partsAsString}>";
77+
}
5678
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Console\Exceptions;
6+
7+
use function array_map;
8+
use BackedEnum;
9+
use function gettype;
10+
use function implode;
11+
use function is_string;
12+
use Tempest\Console\Console;
13+
14+
final class InvalidEnumArgument extends ConsoleException
15+
{
16+
/**
17+
* @param class-string<BackedEnum> $argumentType
18+
*/
19+
public function __construct(
20+
private string $argumentName,
21+
private string $argumentType,
22+
private mixed $value,
23+
) {
24+
}
25+
26+
public function render(Console $console): void
27+
{
28+
if (is_string($this->value) || is_numeric($this->value)) {
29+
$value = "`{$this->value}`";
30+
} else {
31+
$value = 'of type `' . gettype($this->value) . '`';
32+
}
33+
34+
$cases = array_map(
35+
callback: fn (BackedEnum $case) => $case->value,
36+
array: $this->argumentType::cases(),
37+
);
38+
$console->error("Invalid argument {$value} for `{$this->argumentName}` argument, valid values are: " . implode(', ', $cases));
39+
}
40+
}

src/Tempest/Console/src/Input/ConsoleArgumentBag.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Tempest\Console\Input;
66

7+
use Tempest\Console\Exceptions\InvalidEnumArgument;
8+
79
final class ConsoleArgumentBag
810
{
911
/** @var ConsoleInputArgument[] */
@@ -93,7 +95,7 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console
9395
{
9496
foreach ($this->arguments as $argument) {
9597
if ($argumentDefinition->matchesArgument($argument)) {
96-
return $argument;
98+
return $this->resolveArgumentValue($argumentDefinition, $argument);
9799
}
98100
}
99101

@@ -108,6 +110,31 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console
108110
return null;
109111
}
110112

113+
private function resolveArgumentValue(
114+
ConsoleArgumentDefinition $argumentDefinition,
115+
ConsoleInputArgument $argument,
116+
): ConsoleInputArgument {
117+
if (! $argumentDefinition->isBackedEnum()) {
118+
return $argument;
119+
}
120+
121+
$resolved = $argumentDefinition->type::tryFrom($argument->value);
122+
123+
if ($resolved === null) {
124+
throw new InvalidEnumArgument(
125+
$argumentDefinition->name,
126+
$argumentDefinition->type,
127+
$argument->value,
128+
);
129+
}
130+
131+
return new ConsoleInputArgument(
132+
name: $argumentDefinition->name,
133+
position: $argumentDefinition->position,
134+
value: $resolved,
135+
);
136+
}
137+
111138
public function findArrayFor(ConsoleArgumentDefinition $argumentDefinition): ?ConsoleInputArgument
112139
{
113140
$values = [];

src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tempest\Console\Input;
66

7+
use BackedEnum;
78
use Tempest\Console\ConsoleArgument;
89
use Tempest\Reflection\ParameterReflector;
910
use function Tempest\Support\str;
@@ -70,4 +71,9 @@ private static function normalizeName(string $name, bool $boolean): string
7071

7172
return $normalizedName->toString();
7273
}
74+
75+
public function isBackedEnum(): bool
76+
{
77+
return is_subclass_of($this->type, BackedEnum::class);
78+
}
7379
}

tests/Integration/Console/ConsoleArgumentBagTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace Tests\Tempest\Integration\Console;
66

77
use PHPUnit\Framework\Attributes\TestWith;
8+
use Tempest\Console\Exceptions\InvalidEnumArgument;
89
use Tempest\Console\Input\ConsoleArgumentBag;
910
use Tempest\Console\Input\ConsoleArgumentDefinition;
11+
use Tests\Tempest\Integration\Console\Fixtures\TestStringEnum;
1012
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
1113

1214
/**
@@ -138,6 +140,49 @@ public function test_negative_input(string $name, bool $expected): void
138140
$this->assertSame($expected, $bag->findFor($definition)->value);
139141
}
140142

143+
public function test_backed_enum_input(): void
144+
{
145+
$argv = [
146+
'tempest',
147+
'test',
148+
'--type=a',
149+
];
150+
151+
$bag = new ConsoleArgumentBag($argv);
152+
153+
$definition = new ConsoleArgumentDefinition(
154+
name: 'type',
155+
type: TestStringEnum::class,
156+
default: null,
157+
hasDefault: false,
158+
position: 0,
159+
);
160+
161+
$this->assertSame(TestStringEnum::A, $bag->findFor($definition)->value);
162+
}
163+
164+
public function test_invalid_backed_enum_input(): void
165+
{
166+
$argv = [
167+
'tempest',
168+
'test',
169+
'--type=invalid',
170+
];
171+
172+
$bag = new ConsoleArgumentBag($argv);
173+
174+
$definition = new ConsoleArgumentDefinition(
175+
name: 'type',
176+
type: TestStringEnum::class,
177+
default: null,
178+
hasDefault: false,
179+
position: 0,
180+
);
181+
182+
$this->expectException(InvalidEnumArgument::class);
183+
$bag->findFor($definition);
184+
}
185+
141186
public function test_name_mapping(): void
142187
{
143188
$this->console

tests/Integration/Console/Fixtures/MyConsole.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ final class MyConsole
1414
)]
1515
public function handle(
1616
string $path,
17+
TestStringEnum $type,
18+
TestStringEnum $fallback = TestStringEnum::A,
1719
int $times = 1,
1820
bool $force = false,
1921
): void {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Console\Fixtures;
6+
7+
enum TestStringEnum: string
8+
{
9+
case A = 'a';
10+
case B = 'b';
11+
case C = 'c';
12+
}

tests/Integration/Console/RenderConsoleCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function test_render(): void
4646
(new RenderConsoleCommand($console))($consoleCommand);
4747

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

0 commit comments

Comments
 (0)