Merge pull request #992 from acelaya-forks/feature/kebab-case-cli

Feature/kebab case cli
This commit is contained in:
Alejandro Celaya 2021-01-30 11:37:24 +01:00 committed by GitHub
commit 56a2253535
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 89 deletions

View file

@ -13,7 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#986](https://github.com/shlinkio/shlink/issues/986) Updated official docker image to use PHP 8.
### Deprecated
* *Nothing*
* [#959](https://github.com/shlinkio/shlink/issues/959) Deprecated all command flags using camelCase format (like `--expirationDate`), adding kebab-case replacements for all of them (like `--expiration-date`).
All the existing camelCase flags will continue working for now, but will be removed in Shlink 3.0.0
### Removed
* *Nothing*

View file

@ -6,11 +6,11 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Command\BaseCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -19,7 +19,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use function Shlinkio\Shlink\Core\arrayToString;
use function sprintf;
class GenerateKeyCommand extends Command
class GenerateKeyCommand extends BaseCommand
{
public const NAME = 'api-key:generate';
@ -42,9 +42,9 @@ class GenerateKeyCommand extends Command
<info>%command.full_name%</info>
You can optionally set its expiration date with <comment>--expirationDate</comment> or <comment>-e</comment>:
You can optionally set its expiration date with <comment>--expiration-date</comment> or <comment>-e</comment>:
<info>%command.full_name% --expirationDate 2020-01-01</info>
<info>%command.full_name% --expiration-date 2020-01-01</info>
You can also set roles to the API key:
@ -56,8 +56,8 @@ class GenerateKeyCommand extends Command
$this
->setName(self::NAME)
->setDescription('Generates a new valid API key.')
->addOption(
'expirationDate',
->addOptionWithDeprecatedFallback(
'expiration-date',
'e',
InputOption::VALUE_REQUIRED,
'The date in which the API key should expire. Use any valid PHP format.',
@ -79,7 +79,7 @@ class GenerateKeyCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$expirationDate = $input->getOption('expirationDate');
$expirationDate = $this->getOptionWithDeprecatedFallback($input, 'expiration-date');
$apiKey = $this->apiKeyService->create(
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
...$this->roleResolver->determineRoles($input),

View file

@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Command\BaseCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -19,7 +19,7 @@ use function Functional\map;
use function implode;
use function sprintf;
class ListKeysCommand extends Command
class ListKeysCommand extends BaseCommand
{
private const ERROR_STRING_PATTERN = '<fg=red>%s</>';
private const SUCCESS_STRING_PATTERN = '<info>%s</info>';
@ -40,8 +40,8 @@ class ListKeysCommand extends Command
$this
->setName(self::NAME)
->setDescription('Lists all the available API keys.')
->addOption(
'enabledOnly',
->addOptionWithDeprecatedFallback(
'enabled-only',
'e',
InputOption::VALUE_NONE,
'Tells if only enabled API keys should be returned.',
@ -50,7 +50,7 @@ class ListKeysCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$enabledOnly = $input->getOption('enabledOnly');
$enabledOnly = $this->getOptionWithDeprecatedFallback($input, 'enabled-only');
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->getExpirationDate();

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use function method_exists;
use function Shlinkio\Shlink\Core\kebabCaseToCamelCase;
use function sprintf;
use function str_contains;
abstract class BaseCommand extends Command
{
/**
* @param mixed|null $default
*/
protected function addOptionWithDeprecatedFallback(
string $name,
?string $shortcut = null,
?int $mode = null,
string $description = '',
$default = null
): self {
$this->addOption($name, $shortcut, $mode, $description, $default);
if (str_contains($name, '-')) {
$camelCaseName = kebabCaseToCamelCase($name);
$this->addOption($camelCaseName, null, $mode, sprintf('[DEPRECATED] Same as "%s".', $name), $default);
}
return $this;
}
/**
* @return bool|string|string[]|null
*/
protected function getOptionWithDeprecatedFallback(InputInterface $input, string $name)
{
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
$camelCaseName = kebabCaseToCamelCase($name);
if (str_contains($rawInput, $camelCaseName)) {
return $input->getOption($camelCaseName);
}
return $input->getOption($name);
}
}

View file

@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\BaseCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -23,9 +23,9 @@ use function Functional\flatten;
use function Functional\unique;
use function method_exists;
use function sprintf;
use function strpos;
use function str_contains;
class GenerateShortUrlCommand extends Command
class GenerateShortUrlCommand extends BaseCommand
{
public const NAME = 'short-url:generate';
@ -53,34 +53,34 @@ class GenerateShortUrlCommand extends Command
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'Tags to apply to the new short URL',
)
->addOption(
'validSince',
->addOptionWithDeprecatedFallback(
'valid-since',
's',
InputOption::VALUE_REQUIRED,
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.',
)
->addOption(
'validUntil',
->addOptionWithDeprecatedFallback(
'valid-until',
'u',
InputOption::VALUE_REQUIRED,
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.',
)
->addOption(
'customSlug',
->addOptionWithDeprecatedFallback(
'custom-slug',
'c',
InputOption::VALUE_REQUIRED,
'If provided, this slug will be used instead of generating a short code',
)
->addOption(
'maxVisits',
->addOptionWithDeprecatedFallback(
'max-visits',
'm',
InputOption::VALUE_REQUIRED,
'This will limit the number of visits for this short URL.',
)
->addOption(
'findIfExists',
->addOptionWithDeprecatedFallback(
'find-if-exists',
'f',
InputOption::VALUE_NONE,
'This will force existing matching URL to be returned if found, instead of creating a new one.',
@ -91,11 +91,11 @@ class GenerateShortUrlCommand extends Command
InputOption::VALUE_REQUIRED,
'The domain to which this short URL will be attached.',
)
->addOption(
'shortCodeLength',
->addOptionWithDeprecatedFallback(
'short-code-length',
'l',
InputOption::VALUE_REQUIRED,
'The length for generated short code (it will be ignored if --customSlug was provided).',
'The length for generated short code (it will be ignored if --custom-slug was provided).',
)
->addOption(
'validate-url',
@ -136,18 +136,24 @@ class GenerateShortUrlCommand extends Command
$explodeWithComma = curry('explode')(',');
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$customSlug = $input->getOption('customSlug');
$maxVisits = $input->getOption('maxVisits');
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
$customSlug = $this->getOptionWithDeprecatedFallback($input, 'custom-slug');
$maxVisits = $this->getOptionWithDeprecatedFallback($input, 'max-visits');
$shortCodeLength = $this->getOptionWithDeprecatedFallback(
$input,
'short-code-length',
) ?? $this->defaultShortCodeLength;
$doValidateUrl = $this->doValidateUrl($input);
try {
$shortUrl = $this->urlShortener->shorten($longUrl, $tags, ShortUrlMeta::fromRawData([
ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'),
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
ShortUrlMetaInputFilter::VALID_SINCE => $this->getOptionWithDeprecatedFallback($input, 'valid-since'),
ShortUrlMetaInputFilter::VALID_UNTIL => $this->getOptionWithDeprecatedFallback($input, 'valid-until'),
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $this->getOptionWithDeprecatedFallback(
$input,
'find-if-exists',
),
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlMetaInputFilter::VALIDATE_URL => $doValidateUrl,
@ -168,10 +174,10 @@ class GenerateShortUrlCommand extends Command
{
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
if (strpos($rawInput, '--no-validate-url') !== false) {
if (str_contains($rawInput, '--no-validate-url')) {
return false;
}
if (strpos($rawInput, '--validate-url') !== false) {
if (str_contains($rawInput, '--validate-url')) {
return true;
}

View file

@ -21,6 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use function Functional\map;
use function Functional\select_keys;
use function sprintf;
class GetVisitsCommand extends AbstractWithDateRangeCommand
{
@ -39,18 +40,18 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
$this
->setName(self::NAME)
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code');
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.')
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.');
}
protected function getStartDateDesc(): string
protected function getStartDateDesc(string $optionName): string
{
return 'Allows to filter visits, returning only those older than start date';
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
}
protected function getEndDateDesc(): string
protected function getEndDateDesc(string $optionName): string
{
return 'Allows to filter visits, returning only those newer than end date';
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
}
protected function interact(InputInterface $input, OutputInterface $output): void
@ -70,8 +71,8 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$identifier = ShortUrlIdentifier::fromCli($input);
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$paginator = $this->visitsTracker->info($identifier, new VisitsParams(new DateRange($startDate, $endDate)));

View file

@ -60,28 +60,33 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
'page',
'p',
InputOption::VALUE_REQUIRED,
'The first page to list (10 items per page unless "--all" is provided)',
'The first page to list (10 items per page unless "--all" is provided).',
'1',
)
->addOption(
'searchTerm',
->addOptionWithDeprecatedFallback(
'search-term',
'st',
InputOption::VALUE_REQUIRED,
'A query used to filter results by searching for it on the longUrl and shortCode fields',
'A query used to filter results by searching for it on the longUrl and shortCode fields.',
)
->addOption(
'tags',
't',
InputOption::VALUE_REQUIRED,
'A comma-separated list of tags to filter results',
'A comma-separated list of tags to filter results.',
)
->addOption(
'orderBy',
->addOptionWithDeprecatedFallback(
'order-by',
'o',
InputOption::VALUE_REQUIRED,
'The field from which we want to order by. Pass ASC or DESC separated by a comma',
'The field from which we want to order by. Pass ASC or DESC separated by a comma.',
)
->addOptionWithDeprecatedFallback(
'show-tags',
null,
InputOption::VALUE_NONE,
'Whether to display the tags or not.',
)
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not')
->addOption(
'all',
'a',
@ -91,14 +96,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
);
}
protected function getStartDateDesc(): string
protected function getStartDateDesc(string $optionName): string
{
return 'Allows to filter short URLs, returning only those created after "startDate"';
return sprintf('Allows to filter short URLs, returning only those created after "%s".', $optionName);
}
protected function getEndDateDesc(): string
protected function getEndDateDesc(string $optionName): string
{
return 'Allows to filter short URLs, returning only those created before "endDate"';
return sprintf('Allows to filter short URLs, returning only those created before "%s".', $optionName);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
@ -106,13 +111,13 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$io = new SymfonyStyle($input, $output);
$page = (int) $input->getOption('page');
$searchTerm = $input->getOption('searchTerm');
$searchTerm = $this->getOptionWithDeprecatedFallback($input, 'search-term');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = (bool) $input->getOption('showTags');
$all = (bool) $input->getOption('all');
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$showTags = $this->getOptionWithDeprecatedFallback($input, 'show-tags');
$all = $input->getOption('all');
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$orderBy = $this->processOrderBy($input);
$data = [
@ -178,7 +183,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
*/
private function processOrderBy(InputInterface $input)
{
$orderBy = $input->getOption('orderBy');
$orderBy = $this->getOptionWithDeprecatedFallback($input, 'order-by');
if (empty($orderBy)) {
return null;
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Shlinkio\Shlink\CLI\Command\BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -13,19 +13,42 @@ use Throwable;
use function sprintf;
abstract class AbstractWithDateRangeCommand extends Command
abstract class AbstractWithDateRangeCommand extends BaseCommand
{
private const START_DATE = 'start-date';
private const END_DATE = 'end-date';
final protected function configure(): void
{
$this->doConfigure();
$this
->addOption('startDate', 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc())
->addOption('endDate', 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc());
->addOptionWithDeprecatedFallback(
self::START_DATE,
's',
InputOption::VALUE_REQUIRED,
$this->getStartDateDesc(self::START_DATE),
)
->addOptionWithDeprecatedFallback(
self::END_DATE,
'e',
InputOption::VALUE_REQUIRED,
$this->getEndDateDesc(self::END_DATE),
);
}
protected function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
protected function getStartDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
$value = $input->getOption($key);
return $this->getDateOption($input, $output, self::START_DATE);
}
protected function getEndDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->getDateOption($input, $output, self::END_DATE);
}
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
{
$value = $this->getOptionWithDeprecatedFallback($input, $key);
if (empty($value)) {
return null;
}
@ -49,6 +72,7 @@ abstract class AbstractWithDateRangeCommand extends Command
abstract protected function doConfigure(): void;
abstract protected function getStartDateDesc(): string;
abstract protected function getEndDateDesc(): string;
abstract protected function getStartDateDesc(string $optionName): string;
abstract protected function getEndDateDesc(string $optionName): string;
}

View file

@ -55,7 +55,7 @@ class GenerateKeyCommandTest extends TestCase
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()
->willReturn(new ApiKey());
$this->commandTester->execute([
'--expirationDate' => '2016-01-01',
'--expiration-date' => '2016-01-01',
]);
}
}

View file

@ -39,7 +39,7 @@ class ListKeysCommandTest extends TestCase
{
$listKeys = $this->apiKeyService->listKeys($enabledOnly)->willReturn($keys);
$this->commandTester->execute(['--enabledOnly' => $enabledOnly]);
$this->commandTester->execute(['--enabled-only' => $enabledOnly]);
$output = $this->commandTester->getDisplay();
self::assertEquals($expected, $output);

View file

@ -48,7 +48,7 @@ class GenerateShortUrlCommandTest extends TestCase
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
'--maxVisits' => '3',
'--max-visits' => '3',
]);
$output = $this->commandTester->getDisplay();
@ -78,7 +78,7 @@ class GenerateShortUrlCommandTest extends TestCase
NonUniqueSlugException::fromSlug('my-slug'),
);
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
$output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());

View file

@ -71,8 +71,8 @@ class GetVisitsCommandTest extends TestCase
$this->commandTester->execute([
'shortCode' => $shortCode,
'--startDate' => $startDate,
'--endDate' => $endDate,
'--start-date' => $startDate,
'--end-date' => $endDate,
]);
}
@ -86,13 +86,13 @@ class GetVisitsCommandTest extends TestCase
$this->commandTester->execute([
'shortCode' => $shortCode,
'--startDate' => $startDate,
'--start-date' => $startDate,
]);
$output = $this->commandTester->getDisplay();
$info->shouldHaveBeenCalledOnce();
self::assertStringContainsString(
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
sprintf('Ignored provided "start-date" since its value "%s" is not a valid date', $startDate),
$output,
);
}

View file

@ -104,7 +104,7 @@ class ListShortUrlsCommandTest extends TestCase
->shouldBeCalledOnce();
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--showTags' => true]);
$this->commandTester->execute(['--show-tags' => true]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString('Tags', $output);
}
@ -139,22 +139,22 @@ class ListShortUrlsCommandTest extends TestCase
{
yield [[], 1, null, []];
yield [['--page' => $page = 3], $page, null, []];
yield [['--searchTerm' => $searchTerm = 'search this'], 1, $searchTerm, []];
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, []];
yield [
['--page' => $page = 3, '--searchTerm' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
$page,
$searchTerm,
explode(',', $tags),
];
yield [
['--startDate' => $startDate = '2019-01-01'],
['--start-date' => $startDate = '2019-01-01'],
1,
null,
[],
$startDate,
];
yield [
['--endDate' => $endDate = '2020-05-23'],
['--end-date' => $endDate = '2020-05-23'],
1,
null,
[],
@ -162,7 +162,7 @@ class ListShortUrlsCommandTest extends TestCase
$endDate,
];
yield [
['--startDate' => $startDate = '2019-01-01', '--endDate' => $endDate = '2020-05-23'],
['--start-date' => $startDate = '2019-01-01', '--end-date' => $endDate = '2020-05-23'],
1,
null,
[],
@ -191,9 +191,9 @@ class ListShortUrlsCommandTest extends TestCase
public function provideOrderBy(): iterable
{
yield [[], null];
yield [['--orderBy' => 'foo'], 'foo'];
yield [['--orderBy' => 'foo,ASC'], ['foo' => 'ASC']];
yield [['--orderBy' => 'bar,DESC'], ['bar' => 'DESC']];
yield [['--order-by' => 'foo'], 'foo'];
yield [['--order-by' => 'foo,ASC'], ['foo' => 'ASC']];
yield [['--order-by' => 'bar,DESC'], ['bar' => 'DESC']];
}
/** @test */

View file

@ -12,9 +12,12 @@ use PUGX\Shortid\Factory as ShortIdFactory;
use function Functional\reduce_left;
use function is_array;
use function lcfirst;
use function print_r;
use function sprintf;
use function str_repeat;
use function str_replace;
use function ucwords;
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
const DEFAULT_SHORT_CODES_LENGTH = 5;
@ -97,3 +100,8 @@ function arrayToString(array $array, int $indentSize = 4): string
);
}, '');
}
function kebabCaseToCamelCase(string $name): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $name))));
}