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

@ -2,13 +2,17 @@
namespace jrosset\CliProgram\Validation\Validators; namespace jrosset\CliProgram\Validation\Validators;
use Closure;
use FilesystemIterator;
use InvalidArgumentException; use InvalidArgumentException;
use SplFileInfo;
use Stringable; use Stringable;
use Symfony\Component\Console\Completion\CompletionInput;
/** /**
* An argument/option value validator for a directory path * An argument/option value validator for a directory path
*/ */
class DirectoryValidator implements IValidator { class DirectoryValidator implements IValidatorWithSuggestions {
use TInternalValueValidator; use TInternalValueValidator;
/** /**
@ -43,12 +47,15 @@ class DirectoryValidator implements IValidator {
throw new InvalidArgumentException('The default value must be a string or null'); throw new InvalidArgumentException('The default value must be a string or null');
} }
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable) && !is_readable($default)) { if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable) && !is_readable($default)) {
throw new InvalidArgumentException('The default value is not readable'); throw new InvalidArgumentException('The default value is not readable');
} }
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable) && !is_writable($default)) { if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable) && !is_writable($default)) {
throw new InvalidArgumentException('The default value is not writable'); throw new InvalidArgumentException('The default value is not writable');
} }
/** @noinspection PhpParamsInspection */
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists) && !file_exists($default)) { if ($this->getOptions()->contains(FilesystemValidationOption::MustExists) && !file_exists($default)) {
throw new InvalidArgumentException('The default value doesn\'t exist'); throw new InvalidArgumentException('The default value doesn\'t exist');
} }
@ -101,4 +108,65 @@ class DirectoryValidator implements IValidator {
}; };
return $this; 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; namespace jrosset\CliProgram\Validation\Validators;
use Arrayy\Type\StringCollection; use Arrayy\Type\StringCollection;
use Closure;
use ReflectionEnum; use ReflectionEnum;
use ReflectionEnumUnitCase;
use ReflectionException; use ReflectionException;
use UnitEnum; use UnitEnum;
@ -14,7 +16,7 @@ use UnitEnum;
* @template-implements IValidator<TEnum> * @template-implements IValidator<TEnum>
* @template-implements TInternalValueValidator<TEnum> * @template-implements TInternalValueValidator<TEnum>
*/ */
class EnumValidator extends BasedValidator { class EnumValidator extends BasedValidator implements IValidatorWithSuggestions {
/** /**
* @var ReflectionEnum The enumeration * @var ReflectionEnum The enumeration
*/ */
@ -57,4 +59,16 @@ class EnumValidator extends BasedValidator {
$enumCase = $this->getInternalValidator()->getValue(); $enumCase = $this->getInternalValidator()->getValue();
return $this->enum->getCase($enumCase)->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; namespace jrosset\CliProgram\Validation\Validators;
use InvalidArgumentException; use InvalidArgumentException;
use SplFileInfo;
/** /**
* An argument/option value validator for a file path * An argument/option value validator for a file path
@ -68,4 +69,11 @@ class FileValidator extends DirectoryValidator {
$this->extensionsPattern = $extensionsPattern; $this->extensionsPattern = $extensionsPattern;
return $this; 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 * An argument/option value validator
* *
* @template TValue * @template TValue The Type of the returned value
*/ */
interface IValidator { 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; namespace jrosset\CliProgram\Validation\Validators;
use Arrayy\Type\StringCollection; use Arrayy\Type\StringCollection;
use Closure;
/** /**
* An argument/option value validator based on a list of value * 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 IValidator<TValue>
* @template-implements TInternalValueValidator<TValue> * @template-implements TInternalValueValidator<TValue>
*/ */
class ListValidator implements IValidator { class ListValidator implements IValidatorWithSuggestions {
use TIdenticalValidDefaultValidator; use TIdenticalValidDefaultValidator;
use TInternalValueValidator; use TInternalValueValidator;
@ -59,4 +60,11 @@ class ListValidator implements IValidator {
$this->allowedValues = $allowedValues; $this->allowedValues = $allowedValues;
return $this; return $this;
} }
/**
* @inheritDoc
*/
public function getSuggestions (): Closure|array {
return $this->getAllowedValues()->toArray();
}
} }

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

Loading…
Cancel
Save