Skip to content

Commit

Permalink
Support for custom validation and aliases (#102)
Browse files Browse the repository at this point in the history
* Add "as" and update "validate" type

* Support custom validation and alias

* Test custom validation and aliases

* Ignore new PHPStan error

* Fix code styling

* Add "as" and update "validate" type

* Remove aliases

* Update Prompt.php

---------

Co-authored-by: Jess Archer <jess@jessarcher.com>
Co-authored-by: jessarcher <jessarcher@users.noreply.github.com>
Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
4 people authored Dec 29, 2023
1 parent 0930ea9 commit d814a27
Show file tree
Hide file tree
Showing 18 changed files with 251 additions and 42 deletions.
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

0 comments on commit d814a27

Please sign in to comment.