Validation: add suggestions support (autocompletion)

master 3.12
Julien Rosset 3 weeks ago
parent 19f11dd899
commit ba27ae5963

@ -5,9 +5,13 @@ namespace jrosset\CliProgram\Validation;
use Closure;
use jrosset\CliProgram\Validation\Validators\InvalidValueException;
use jrosset\CliProgram\Validation\Validators\IValidator;
use jrosset\CliProgram\Validation\Validators\IValidatorWithSuggestions;
use ReflectionFunction;
use Stringable;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -188,12 +192,12 @@ trait TCommandWithValidation {
/**
* Adds an argument
*
* @param string $name The argument name
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param string $description The argument description
* @param IValidator|null $validator The validator or Null if none
* @param Closure|array|null $suggestedValues The function or list of suggested values when completing ; Null if none
* @param string $name The argument name
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param string $description The argument description
* @param IValidator|null $validator The validator or Null if none
* @param null|(string|Suggestion)[]|Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The function or list of suggested values when completing ; Null if none
*
* @return $this
*
@ -210,14 +214,13 @@ trait TCommandWithValidation {
$default = static::treatStringableDefaultValue($default);
if ($validator !== null) {
$default = $validator->getValidDefault($default);
if ($validator instanceof IValidatorWithSuggestions) {
$suggestedValues ??= $validator->getSuggestions();
}
}
$suggestedValues ??= [];
if ($suggestedValues !== null) {
parent::addArgument($name, $mode, $description, $default, $suggestedValues);
}
else {
parent::addArgument($name, $mode, $description, $default);
}
parent::addArgument($name, $mode, $description, $default, $suggestedValues);
return $this->setArgumentValidator($name, $validator);
}
/**
@ -284,14 +287,13 @@ trait TCommandWithValidation {
$default = static::treatStringableDefaultValue($default);
if ($validator !== null) {
$default = $validator->getValidDefault($default);
if ($validator instanceof IValidatorWithSuggestions) {
$suggestedValues ??= $validator->getSuggestions();
}
}
$suggestedValues ??= [];
if ($suggestedValues !== null) {
parent::addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
}
else {
parent::addOption($name, $shortcut, $mode, $description, $default);
}
parent::addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
return $this->setOptionValidator($name, $validator);
}
/**

@ -2,13 +2,17 @@
namespace jrosset\CliProgram\Validation\Validators;
use Closure;
use FilesystemIterator;
use InvalidArgumentException;
use SplFileInfo;
use Stringable;
use Symfony\Component\Console\Completion\CompletionInput;
/**
* An argument/option value validator for a directory path
*/
class DirectoryValidator implements IValidator {
class DirectoryValidator implements IValidatorWithSuggestions {
use TInternalValueValidator;
/**
@ -43,12 +47,15 @@ class DirectoryValidator implements IValidator {
throw new InvalidArgumentException('The default value must be a string or null');
}
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable) && !is_readable($default)) {
throw new InvalidArgumentException('The default value is not readable');
}
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable) && !is_writable($default)) {
throw new InvalidArgumentException('The default value is not writable');
}
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists) && !file_exists($default)) {
throw new InvalidArgumentException('The default value doesn\'t exist');
}
@ -101,4 +108,65 @@ class DirectoryValidator implements IValidator {
};
return $this;
}
/**
* @inheritDoc
*/
public function getSuggestions (): Closure|array {
return function (CompletionInput $input): array {
$search = str_replace('\\', '/', $input->getCompletionValue());
if (str_ends_with($search, '/')) {
$searchDirectory = $search;
$search = null;
}
else {
$searchDirectory = dirname($search);
$search = basename($search);
}
if (!str_ends_with($searchDirectory, '/')) {
$searchDirectory .= '/';
}
$searchDirectoryIterator = new FilesystemIterator($searchDirectory);
$suggestionsHighPriority = [];
$suggestionsLowPriority = [];
/** @var SplFileInfo $fileInfo */
foreach ($searchDirectoryIterator as $fileInfo) {
if (!$this->checkSuggestedFile($fileInfo)) {
continue;
}
if ($search === null) {
$suggestions = &$suggestionsLowPriority;
}
elseif (str_starts_with($fileInfo->getFilename(), $search)) {
$suggestions = &$suggestionsHighPriority;
}
elseif (str_starts_with(mb_strtolower($fileInfo->getFilename()), mb_strtolower($search))) {
$suggestions = &$suggestionsLowPriority;
}
else {
continue;
}
$suggestions[] = ($searchDirectory === './' ? '' : $searchDirectory)
. $fileInfo->getFilename()
. ($fileInfo->isDir() ? '/' : '');
}
return array_merge($suggestionsHighPriority, $suggestionsLowPriority);
};
}
/**
* Vérifie si un fichier suggéré est valide
*
* @param SplFileInfo $file Le fichier
*
* @return bool Vrai si le fichier est valide
*/
protected function checkSuggestedFile (SplFileInfo $file): bool {
return $file->isDir();
}
}

@ -3,7 +3,9 @@
namespace jrosset\CliProgram\Validation\Validators;
use Arrayy\Type\StringCollection;
use Closure;
use ReflectionEnum;
use ReflectionEnumUnitCase;
use ReflectionException;
use UnitEnum;
@ -14,7 +16,7 @@ use UnitEnum;
* @template-implements IValidator<TEnum>
* @template-implements TInternalValueValidator<TEnum>
*/
class EnumValidator extends BasedValidator {
class EnumValidator extends BasedValidator implements IValidatorWithSuggestions {
/**
* @var ReflectionEnum The enumeration
*/
@ -57,4 +59,16 @@ class EnumValidator extends BasedValidator {
$enumCase = $this->getInternalValidator()->getValue();
return $this->enum->getCase($enumCase)->getValue();
}
/**
* @inheritDoc
*/
public function getSuggestions (): Closure|array {
return array_map(
function (ReflectionEnumUnitCase $case): string {
return $case->getName();
},
$this->enum->getCases()
);
}
}

@ -3,6 +3,7 @@
namespace jrosset\CliProgram\Validation\Validators;
use InvalidArgumentException;
use SplFileInfo;
/**
* An argument/option value validator for a file path
@ -68,4 +69,11 @@ class FileValidator extends DirectoryValidator {
$this->extensionsPattern = $extensionsPattern;
return $this;
}
/**
* @inheritDoc
*/
protected function checkSuggestedFile (SplFileInfo $file): bool {
return true;
}
}

@ -5,7 +5,7 @@ namespace jrosset\CliProgram\Validation\Validators;
/**
* An argument/option value validator
*
* @template TValue
* @template TValue The Type of the returned value
*/
interface IValidator {
/**

@ -0,0 +1,23 @@
<?php
namespace jrosset\CliProgram\Validation\Validators;
use Closure;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
/**
* An argument/option value validator
*
* @template TValue The Type of the returned value
* @extends IValidator<TValue>
*/
interface IValidatorWithSuggestions extends IValidator {
/**
* The method or list of value suggestions
*
* @return (string|Suggestion)[]|Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> The method or list of value suggestions
*/
public function getSuggestions (): Closure|array;
}

@ -3,6 +3,7 @@
namespace jrosset\CliProgram\Validation\Validators;
use Arrayy\Type\StringCollection;
use Closure;
/**
* An argument/option value validator based on a list of value
@ -11,7 +12,7 @@ use Arrayy\Type\StringCollection;
* @template-implements IValidator<TValue>
* @template-implements TInternalValueValidator<TValue>
*/
class ListValidator implements IValidator {
class ListValidator implements IValidatorWithSuggestions {
use TIdenticalValidDefaultValidator;
use TInternalValueValidator;
@ -59,4 +60,11 @@ class ListValidator implements IValidator {
$this->allowedValues = $allowedValues;
return $this;
}
/**
* @inheritDoc
*/
public function getSuggestions (): Closure|array {
return $this->getAllowedValues()->toArray();
}
}

@ -13,7 +13,7 @@ class Email extends CommandWithValidation {
/**
* @inheritDoc
*/
protected function configure () {
protected function configure (): void {
parent::configure();
$this->addArgument(

Loading…
Cancel
Save