Skip to content

Commit

Permalink
feat(console): add name parameter to #[ConsoleArgument] (#617)
Browse files Browse the repository at this point in the history
Co-authored-by: Brent Roose <[email protected]>
Co-authored-by: Enzo Innocenzi <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 04000ac commit 2a73033
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 22 deletions.
32 changes: 19 additions & 13 deletions src/Tempest/Console/src/Actions/RenderConsoleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;
use Tempest\Reflection\ParameterReflector;
use Tempest\Console\Input\ConsoleArgumentDefinition;
use function Tempest\Support\str;

final readonly class RenderConsoleCommand
{
Expand All @@ -18,8 +19,8 @@ public function __invoke(ConsoleCommand $consoleCommand): void
{
$parts = ["<em><strong>{$consoleCommand->getName()}</strong></em>"];

foreach ($consoleCommand->handler->getParameters() as $parameter) {
$parts[] = $this->renderParameter($parameter);
foreach ($consoleCommand->getArgumentDefinitions() as $argument) {
$parts[] = $this->renderArgument($argument);
}

if ($consoleCommand->description !== null && $consoleCommand->description !== '') {
Expand All @@ -29,22 +30,27 @@ public function __invoke(ConsoleCommand $consoleCommand): void
$this->console->writeln(' ' . implode(' ', $parts));
}

private function renderParameter(ParameterReflector $parameter): string
private function renderArgument(ConsoleArgumentDefinition $argument): string
{
/** @phpstan-ignore-next-line */
$type = $parameter->getType()?->getName();
$optional = $parameter->isOptional();
$defaultValue = strtolower(var_export($optional ? $parameter->getDefaultValue() : null, true));
$name = "<em>{$parameter->getName()}</em>";
$name = str($argument->name)
->prepend('<em>')
->append('</em>');

$asString = match($type) {
$asString = match($argument->type) {
'bool' => "<em>--</em>{$name}",
default => $name,
};

return match($optional) {
true => "[{$asString}={$defaultValue}]",
false => "<{$asString}>",
if (! $argument->hasDefault) {
return "<{$asString}>";
}

return match (true) {
$argument->default === true => "[{$asString}=true]",
$argument->default === false => "[{$asString}=false]",
is_null($argument->default) => "[{$asString}=null]",
is_array($argument->default) => "[{$asString}=array]",
default => "[{$asString}={$argument->default}]"
};
}
}
1 change: 1 addition & 0 deletions src/Tempest/Console/src/ConsoleArgument.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
final readonly class ConsoleArgument
{
public function __construct(
public ?string $name = null,
public ?string $description = null,
public string $help = '',
public array $aliases = [],
Expand Down
21 changes: 17 additions & 4 deletions src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Tempest\Console\ConsoleArgument;
use Tempest\Reflection\ParameterReflector;
use function Tempest\Support\str;

final readonly class ConsoleArgumentDefinition
{
Expand All @@ -24,13 +25,14 @@ public function __construct(
public static function fromParameter(ParameterReflector $parameter): ConsoleArgumentDefinition
{
$attribute = $parameter->getAttribute(ConsoleArgument::class);

$type = $parameter->getType();
$default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
$boolean = $type->getName() === 'bool' || is_bool($default);

return new ConsoleArgumentDefinition(
name: $parameter->getName(),
name: static::normalizeName($attribute?->name ?? $parameter->getName(), boolean: $boolean),
type: $type->getName(),
default: $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
default: $default,
hasDefault: $parameter->isDefaultValueAvailable(),
position: $parameter->getPosition(),
description: $attribute?->description,
Expand All @@ -50,11 +52,22 @@ public function matchesArgument(ConsoleInputArgument $argument): bool
}

foreach ([$this->name, ...$this->aliases] as $match) {
if ($argument->matches($match)) {
if ($argument->matches(static::normalizeName($match, $this->type === 'bool'))) {
return true;
}
}

return false;
}

private static function normalizeName(string $name, bool $boolean): string
{
$normalizedName = str($name)->kebab();

if ($boolean) {
$normalizedName->replaceStart('no-', '');
}

return $normalizedName->toString();
}
}
25 changes: 25 additions & 0 deletions tests/Fixtures/Console/CommandWithArgumentName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Fixtures\Console;

use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\HasConsole;

final readonly class CommandWithArgumentName
{
use HasConsole;

#[ConsoleCommand(name: 'command-with-argument-name')]
public function __invoke(
#[ConsoleArgument(name: 'new-name')]
string $input,
#[ConsoleArgument(name: 'new-flag')]
bool $flag = false,
): void {
$this->writeln($input);
$this->writeln($flag ? 'true' : 'false');
}
}
26 changes: 26 additions & 0 deletions tests/Fixtures/Console/CommandWithDifferentArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Fixtures\Console;

use Tempest\Console\ConsoleArgument;

// tests/Integration/Console/Input/ConsoleArgumentDefinitionTest.php
final readonly class CommandWithDifferentArguments
{
public function __invoke(
string $string,
string $camelCaseString,
#[ConsoleArgument(name: 'my-kebab-string')]
string $renamedKebabString,
#[ConsoleArgument(name: 'myCamelString')]
string $renamedCamelString,
bool $bool,
bool $camelCaseBool,
string $camelCaseStringWithDefault = 'foo',
bool $camelCaseBoolWithTrueDefault = true,
bool $camelCaseBoolWithFalseDefault = false,
): void {
}
}
11 changes: 9 additions & 2 deletions tests/Fixtures/Console/Hello.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tests\Tempest\Fixtures\Console;

use Tempest\Console\Console;
use Tempest\Console\ConsoleArgument;
use Tempest\Console\ConsoleCommand;

final readonly class Hello
Expand All @@ -23,8 +24,14 @@ public function world(string $input): void
}

#[ConsoleCommand]
public function test(?int $optionalValue = null, bool $flag = false): void
{
public function test(
#[ConsoleArgument]
?int $optionalValue = null,
#[ConsoleArgument(
name: 'custom-flag',
)]
bool $flag = false
): void {
$value = $optionalValue ?? 'null';

$this->console->info("{$value}");
Expand Down
11 changes: 9 additions & 2 deletions tests/Integration/Console/ConsoleArgumentBagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ public function test_positional_vs_named_input(): void
$this->console
->call('complex a --c=c --b=b --flag')
->assertContains('abc')
->assertContains('true')
;
->assertContains('true');
}

public function test_combined_flags(): void
Expand Down Expand Up @@ -138,4 +137,12 @@ public function test_negative_input(string $name, bool $expected): void

$this->assertSame($expected, $bag->findFor($definition)->value);
}

public function test_name_mapping(): void
{
$this->console
->call('command-with-argument-name --new-name=foo --new-flag')
->assertSee('foo')
->assertSee('true');
}
}
50 changes: 50 additions & 0 deletions tests/Integration/Console/Input/ConsoleArgumentDefinitionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\Console\Input;

use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Tempest\Console\Input\ConsoleArgumentDefinition;
use Tempest\Reflection\ClassReflector;
use Tempest\Reflection\ParameterReflector;
use Tests\Tempest\Fixtures\Console\CommandWithDifferentArguments;

/**
* @internal
*/
final class ConsoleArgumentDefinitionTest extends TestCase
{
#[TestWith(['string', 'string', 'string', null])]
#[TestWith(['bool', 'bool', 'bool', null])]
#[TestWith(['camelCaseString', 'camel-case-string', 'string', null])]
#[TestWith(['camelCaseBool', 'camel-case-bool', 'bool', null])]
#[TestWith(['renamedCamelString', 'my-camel-string', 'string', null])]
#[TestWith(['renamedKebabString', 'my-kebab-string', 'string', null])]
#[TestWith(['camelCaseBool', 'camel-case-bool', 'bool', null])]
#[TestWith(['camelCaseStringWithDefault', 'camel-case-string-with-default', 'string', 'foo'])]
#[TestWith(['camelCaseBoolWithTrueDefault', 'camel-case-bool-with-true-default', 'bool', true])]
#[TestWith(['camelCaseBoolWithFalseDefault', 'camel-case-bool-with-false-default', 'bool', false])]
public function test_parse_named_arguments_with_types_and_defaults(string $originalParameter, string $expectedName, string $expectedType, mixed $expectedDefault): void
{
$definition = ConsoleArgumentDefinition::fromParameter($this->getParameter($originalParameter));
$this->assertSame($expectedName, $definition->name);
$this->assertSame($expectedType, $definition->type);
$this->assertSame($expectedDefault, $definition->default);
}

private function getParameter(string $name): ParameterReflector
{
$reflector = new ClassReflector(CommandWithDifferentArguments::class);

foreach ($reflector->getMethod('__invoke')->getParameters() as $parameter) {
if ($parameter->getName() === $name) {
return $parameter;
}
}

throw new RuntimeException("Parameter not found: {$name}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function test_overview(): void
->assertContains('Hello')
->assertDoesNotContain('hidden')
->assertContains('hello:world <input>')
->assertContains('hello:test [optionalValue=null] [--flag=false] - description')
->assertContains('hello:test [optional-value=null] [--flag=false] - description')
->assertContains('testcommand:test');
}

Expand Down

0 comments on commit 2a73033

Please sign in to comment.