Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom validation and aliases #102

Merged
merged 10 commits into from
Dec 29, 2023
6 changes: 2 additions & 4 deletions src/ConfirmPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace Laravel\Prompts;

use Closure;

class ConfirmPrompt extends Prompt
{
/**
Expand All @@ -20,8 +18,8 @@ public function __construct(
public string $yes = 'Yes',
public string $no = 'No',
public bool|string $required = false,
public ?Closure $validate = null,
public string $hint = ''
public mixed $validate = null,
public string $hint = '',
) {
$this->confirmed = $default;

Expand Down
2 changes: 1 addition & 1 deletion src/MultiSearchPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function __construct(
public string $placeholder = '',
public int $scroll = 5,
public bool|string $required = false,
public ?Closure $validate = null,
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue(submit: false, ignore: fn ($key) => Key::oneOf([Key::SPACE, Key::HOME, Key::END, Key::CTRL_A, Key::CTRL_E], $key) && $this->highlighted !== null);
Expand Down
5 changes: 2 additions & 3 deletions src/MultiSelectPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Laravel\Prompts;

use Closure;
use Illuminate\Support\Collection;

class MultiSelectPrompt extends Prompt
Expand Down Expand Up @@ -42,8 +41,8 @@ public function __construct(
array|Collection $default = [],
public int $scroll = 5,
public bool|string $required = false,
public ?Closure $validate = null,
public string $hint = ''
public mixed $validate = null,
public string $hint = '',
) {
$this->options = $options instanceof Collection ? $options->all() : $options;
$this->default = $default instanceof Collection ? $default->all() : $default;
Expand Down
6 changes: 2 additions & 4 deletions src/PasswordPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace Laravel\Prompts;

use Closure;

class PasswordPrompt extends Prompt
{
use Concerns\TypedValue;
Expand All @@ -15,8 +13,8 @@ public function __construct(
public string $label,
public string $placeholder = '',
public bool|string $required = false,
public ?Closure $validate = null,
public string $hint = ''
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue();
}
Expand Down
27 changes: 22 additions & 5 deletions src/Prompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ abstract class Prompt
public bool|string $required;

/**
* The validator callback.
* The validator callback or rules.
*/
protected ?Closure $validate;
public mixed $validate;

/**
* The cancellation callback.
Expand All @@ -59,6 +59,11 @@ abstract class Prompt
*/
protected bool $validated = false;

/**
* The custom validation callback.
*/
protected static ?Closure $validateUsing;

/**
* The output instance.
*/
Expand Down Expand Up @@ -190,6 +195,14 @@ public static function terminal(): Terminal
return static::$terminal ??= new Terminal();
}

/**
* Set the custom validation callback.
*/
public static function validateUsing(Closure $callback): void
{
static::$validateUsing = $callback;
}

/**
* Render the prompt.
*/
Expand Down Expand Up @@ -331,14 +344,18 @@ private function validate(mixed $value): void
return;
}

if (! isset($this->validate)) {
if (! isset($this->validate) && ! isset(static::$validateUsing)) {
return;
}

$error = ($this->validate)($value);
$error = match (true) {
is_callable($this->validate) => ($this->validate)($value),
isset(static::$validateUsing) => (static::$validateUsing)($this),
default => throw new RuntimeException('The validation logic is missing.'),
};

if (! is_string($error) && ! is_null($error)) {
throw new \RuntimeException('The validator must return a string or null.');
throw new RuntimeException('The validator must return a string or null.');
}

if (is_string($error) && strlen($error) > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/SearchPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(
public Closure $options,
public string $placeholder = '',
public int $scroll = 5,
public ?Closure $validate = null,
public mixed $validate = null,
public string $hint = '',
public bool|string $required = true,
) {
Expand Down
3 changes: 1 addition & 2 deletions src/SelectPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Laravel\Prompts;

use Closure;
use Illuminate\Support\Collection;
use InvalidArgumentException;

Expand All @@ -27,7 +26,7 @@ public function __construct(
array|Collection $options,
public int|string|null $default = null,
public int $scroll = 5,
public ?Closure $validate = null,
public mixed $validate = null,
public string $hint = '',
public bool|string $required = true,
) {
Expand Down
4 changes: 2 additions & 2 deletions src/SuggestPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public function __construct(
public string $default = '',
public int $scroll = 5,
public bool|string $required = false,
public ?Closure $validate = null,
public string $hint = ''
public mixed $validate = null,
public string $hint = '',
) {
$this->options = $options instanceof Collection ? $options->all() : $options;

Expand Down
6 changes: 2 additions & 4 deletions src/TextPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace Laravel\Prompts;

use Closure;

class TextPrompt extends Prompt
{
use Concerns\TypedValue;
Expand All @@ -16,8 +14,8 @@ public function __construct(
public string $placeholder = '',
public string $default = '',
public bool|string $required = false,
public ?Closure $validate = null,
public string $hint = ''
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue($default);
}
Expand Down
32 changes: 16 additions & 16 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
/**
* Prompt the user for text input.
*/
function text(string $label, string $placeholder = '', string $default = '', bool|string $required = false, ?Closure $validate = null, string $hint = ''): string
function text(string $label, string $placeholder = '', string $default = '', bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new TextPrompt($label, $placeholder, $default, $required, $validate, $hint))->prompt();
return (new TextPrompt(...func_get_args()))->prompt();
}

/**
* Prompt the user for input, hiding the value.
*/
function password(string $label, string $placeholder = '', bool|string $required = false, ?Closure $validate = null, string $hint = ''): string
function password(string $label, string $placeholder = '', bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new PasswordPrompt($label, $placeholder, $required, $validate, $hint))->prompt();
return (new PasswordPrompt(...func_get_args()))->prompt();
}

/**
Expand All @@ -27,9 +27,9 @@ function password(string $label, string $placeholder = '', bool|string $required
* @param array<int|string, string>|Collection<int|string, string> $options
* @param true|string $required
*/
function select(string $label, array|Collection $options, int|string|null $default = null, int $scroll = 5, ?Closure $validate = null, string $hint = '', bool|string $required = true): int|string
function select(string $label, array|Collection $options, int|string|null $default = null, int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SelectPrompt($label, $options, $default, $scroll, $validate, $hint, $required))->prompt();
return (new SelectPrompt(...func_get_args()))->prompt();
}

/**
Expand All @@ -39,27 +39,27 @@ function select(string $label, array|Collection $options, int|string|null $defau
* @param array<int|string>|Collection<int, int|string> $default
* @return array<int|string>
*/
function multiselect(string $label, array|Collection $options, array|Collection $default = [], int $scroll = 5, bool|string $required = false, ?Closure $validate = null, string $hint = 'Use the space bar to select options.'): array
function multiselect(string $label, array|Collection $options, array|Collection $default = [], int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.'): array
{
return (new MultiSelectPrompt($label, $options, $default, $scroll, $required, $validate, $hint))->prompt();
return (new MultiSelectPrompt(...func_get_args()))->prompt();
}

/**
* Prompt the user to confirm an action.
*/
function confirm(string $label, bool $default = true, string $yes = 'Yes', string $no = 'No', bool|string $required = false, ?Closure $validate = null, string $hint = ''): bool
function confirm(string $label, bool $default = true, string $yes = 'Yes', string $no = 'No', bool|string $required = false, mixed $validate = null, string $hint = ''): bool
{
return (new ConfirmPrompt($label, $default, $yes, $no, $required, $validate, $hint))->prompt();
return (new ConfirmPrompt(...func_get_args()))->prompt();
}

/**
* Prompt the user for text input with auto-completion.
*
* @param array<string>|Collection<int, string>|Closure(string): array<string> $options
*/
function suggest(string $label, array|Collection|Closure $options, string $placeholder = '', string $default = '', int $scroll = 5, bool|string $required = false, ?Closure $validate = null, string $hint = ''): string
function suggest(string $label, array|Collection|Closure $options, string $placeholder = '', string $default = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new SuggestPrompt($label, $options, $placeholder, $default, $scroll, $required, $validate, $hint))->prompt();
return (new SuggestPrompt(...func_get_args()))->prompt();
}

/**
Expand All @@ -68,9 +68,9 @@ function suggest(string $label, array|Collection|Closure $options, string $place
* @param Closure(string): array<int|string, string> $options
* @param true|string $required
*/
function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, ?Closure $validate = null, string $hint = '', bool|string $required = true): int|string
function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SearchPrompt($label, $options, $placeholder, $scroll, $validate, $hint, $required))->prompt();
return (new SearchPrompt(...func_get_args()))->prompt();
}

/**
Expand All @@ -79,9 +79,9 @@ function search(string $label, Closure $options, string $placeholder = '', int $
* @param Closure(string): array<int|string, string> $options
* @return array<int|string>
*/
function multisearch(string $label, Closure $options, string $placeholder = '', int $scroll = 5, bool|string $required = false, ?Closure $validate = null, string $hint = 'Use the space bar to select options.'): array
function multisearch(string $label, Closure $options, string $placeholder = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.'): array
{
return (new MultiSearchPrompt($label, $options, $placeholder, $scroll, $required, $validate, $hint))->prompt();
return (new MultiSearchPrompt(...func_get_args()))->prompt();
}

/**
Expand Down
18 changes: 18 additions & 0 deletions tests/Feature/ConfirmPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,21 @@
required: true,
);
})->throws(NonInteractiveValidationException::class, 'Required.');

it('supports custom validation', function () {
Prompt::validateUsing(function (Prompt $prompt) {
expect($prompt)
->label->toBe('Are you sure?')
->validate->toBe('confirmed');

return $prompt->validate === 'confirmed' && ! $prompt->value() ? 'Need to be sure!' : null;
});

Prompt::fake([Key::DOWN, Key::ENTER, Key::UP, Key::ENTER]);

confirm(label: 'Are you sure?', validate: 'confirmed');

Prompt::assertOutputContains('Need to be sure!');

Prompt::validateUsing(fn () => null);
});
28 changes: 28 additions & 0 deletions tests/Feature/MultiSearchPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,31 @@

expect($result)->toBe(['result']);
});

it('supports custom validation', function () {
Prompt::fake(['a', Key::DOWN, Key::SPACE, Key::ENTER, Key::DOWN, Key::SPACE, Key::ENTER]);

Prompt::validateUsing(function (Prompt $prompt) {
expect($prompt)
->label->toBe('What are your favorite colors?')
->validate->toBe('in:green');

return $prompt->validate === 'in:green' && ! in_array('green', $prompt->value()) ? 'And green?' : null;
});

$result = multisearch(
label: 'What are your favorite colors?',
options: fn () => [
'red' => 'Red',
'green' => 'Green',
'blue' => 'Blue',
],
validate: 'in:green',
);

expect($result)->toBe(['red', 'green']);

Prompt::assertOutputContains('And green?');

Prompt::validateUsing(fn () => null);
});
28 changes: 28 additions & 0 deletions tests/Feature/MultiSelectPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,31 @@
'Blue',
], required: true);
})->throws(NonInteractiveValidationException::class, 'Required.');

it('supports custom validation', function () {
Prompt::fake([Key::SPACE, Key::ENTER, Key::DOWN, Key::SPACE, Key::ENTER]);

Prompt::validateUsing(function (Prompt $prompt) {
expect($prompt)
->label->toBe('What are your favorite colors?')
->validate->toBe('in:green');

return $prompt->validate === 'in:green' && ! in_array('green', $prompt->value()) ? 'And green?' : null;
});

$result = multiselect(
label: 'What are your favorite colors?',
options: [
'red' => 'Red',
'green' => 'Green',
'blue' => 'Blue',
],
validate: 'in:green',
);

expect($result)->toBe(['red', 'green']);

Prompt::assertOutputContains('And green?');

Prompt::validateUsing(fn () => null);
});
23 changes: 23 additions & 0 deletions tests/Feature/PasswordPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,26 @@

password('What is the password?', required: true);
})->throws(NonInteractiveValidationException::class, 'Required.');

it('supports custom validation', function () {
Prompt::validateUsing(function (Prompt $prompt) {
expect($prompt)
->label->toBe('What is the password?')
->validate->toBe('min:8');

return $prompt->validate === 'min:8' && strlen($prompt->value()) < 8 ? 'Minimum 8 chars!' : null;
});

Prompt::fake(['p', Key::ENTER, 'a', 's', 's', 'w', 'o', 'r', 'd', Key::ENTER]);

$result = password(
label: 'What is the password?',
validate: 'min:8',
);

expect($result)->toBe('password');

Prompt::assertOutputContains('Minimum 8 chars!');

Prompt::validateUsing(fn () => null);
});
Loading