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

implement nullable prompt #1084

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ public static T Prompt<T>(this IAnsiConsole console, IPrompt<T> prompt)
return prompt.Show(console);
}

/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt to display.</param>
/// <returns>The prompt input result.</returns>
public static T? PromptNullable<T>(this IAnsiConsole console, INullablePrompt<T> prompt)
{
if (prompt is null)
{
throw new ArgumentNullException(nameof(prompt));
}

return prompt.ShowNullable(console);
}

/// <summary>
/// Displays a prompt to the user.
/// </summary>
Expand All @@ -34,6 +51,20 @@ public static T Ask<T>(this IAnsiConsole console, string prompt)
return new TextPrompt<T>(prompt).Show(console);
}

/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt markup text.</param>
/// <returns>The prompt input result.</returns>
public static T? AskNullable<T>(this IAnsiConsole console, string prompt)
{
return new TextPrompt<T>(prompt)
.AllowEmpty()
.ShowNullable(console);
}

/// <summary>
/// Displays a prompt to the user.
/// </summary>
Expand All @@ -49,6 +80,22 @@ public static T Ask<T>(this IAnsiConsole console, string prompt, CultureInfo? cu
return textPrompt.Show(console);
}

/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt markup text.</param>
/// <param name="culture">Specific CultureInfo to use when converting input.</param>
/// <returns>The prompt input result.</returns>
public static T? AskNullable<T>(this IAnsiConsole console, string prompt, CultureInfo? culture)
{
var textPrompt = new TextPrompt<T>(prompt);
textPrompt.AllowEmpty();
textPrompt.Culture = culture;
return textPrompt.ShowNullable(console);
}

/// <summary>
/// Displays a prompt with two choices, yes or no.
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions src/Spectre.Console/Prompts/INullablePrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Spectre.Console;

/// <summary>
/// Represents a prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
public interface INullablePrompt<T>
{
/// <summary>
/// Shows the prompt.
/// </summary>
/// <param name="console">The console.</param>
/// <returns>The prompt input result.</returns>
T? ShowNullable(IAnsiConsole console);

/// <summary>
/// Shows the prompt asynchronously.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The prompt input result.</returns>
Task<T?> ShowNullableAsync(IAnsiConsole console, CancellationToken cancellationToken);
}
31 changes: 27 additions & 4 deletions src/Spectre.Console/Prompts/TextPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Spectre.Console;
/// Represents a prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
public sealed class TextPrompt<T> : IPrompt<T>, IHasCulture
public sealed class TextPrompt<T> : IPrompt<T>, INullablePrompt<T>, IHasCulture
{
private readonly string _prompt;
private readonly StringComparer? _comparer;
Expand Down Expand Up @@ -72,7 +72,7 @@ public sealed class TextPrompt<T> : IPrompt<T>, IHasCulture
/// <summary>
/// Gets or sets the validator.
/// </summary>
public Func<T, ValidationResult>? Validator { get; set; }
public Func<T?, ValidationResult>? Validator { get; set; }

/// <summary>
/// Gets or sets the style in which the default value is displayed.
Expand Down Expand Up @@ -111,8 +111,31 @@ public T Show(IAnsiConsole console)
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
}

/// <summary>
/// Shows the prompt and requests input from the user.
/// </summary>
/// <param name="console">The console to show the prompt in.</param>
/// <returns>The user input converted to the expected type.</returns>
/// <inheritdoc/>
public T? ShowNullable(IAnsiConsole console)
{
return ShowNullableAsync(console, CancellationToken.None).GetAwaiter().GetResult();
}

/// <inheritdoc/>
public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
{
return (await ShowNullableAsync(console, false, cancellationToken))
?? throw new Exception("This method do not allow for null");
}

/// <inheritdoc/>
public Task<T?> ShowNullableAsync(IAnsiConsole console, CancellationToken cancellationToken)
{
return ShowNullableAsync(console, true, cancellationToken);
}

private async Task<T?> ShowNullableAsync(IAnsiConsole console, bool allowNull, CancellationToken cancellationToken)
{
if (console is null)
{
Expand Down Expand Up @@ -165,7 +188,7 @@ public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellat
continue;
}
}
else if (!TypeConverterHelper.TryConvertFromStringWithCulture<T>(input, Culture, out result) || result == null)
else if (!TypeConverterHelper.TryConvertFromStringWithCulture<T>(input, Culture, out result) || (!allowNull && result == null))
{
console.MarkupLine(ValidationErrorMessage);
WritePrompt(console);
Expand Down Expand Up @@ -232,7 +255,7 @@ private void WritePrompt(IAnsiConsole console)
console.Markup(markup + " ");
}

private bool ValidateResult(T value, [NotNullWhen(false)] out string? message)
private bool ValidateResult(T? value, [NotNullWhen(false)] out string? message)
{
if (Validator != null)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Spectre.Console/Prompts/TextPromptExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public static TextPrompt<T> DefaultValue<T>(this TextPrompt<T> obj, T value)
/// <param name="validator">The validation criteria.</param>
/// <param name="message">The validation error message.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T, bool> validator, string? message = null)
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T?, bool> validator, string? message = null)
{
if (obj is null)
{
Expand All @@ -214,7 +214,7 @@ public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T, bool> va
/// <param name="obj">The prompt.</param>
/// <param name="validator">The validation criteria.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T, ValidationResult> validator)
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T?, ValidationResult> validator)
{
if (obj is null)
{
Expand Down
14 changes: 14 additions & 0 deletions test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Prompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,19 @@ public void Should_Return_Correct_DateTime_When_Asked_US_Culture()
// Then
dateTime.ShouldBe(new DateTime(1998, 2, 1));
}

[Fact]
public void Should_Return_Null_Value_When_Asked_Nullable_US_Culture()
{
// Given
var console = new TestConsole().EmitAnsiSequences();
console.Input.PushKey(ConsoleKey.Enter);

// When
var dateTime = console.AskNullable<decimal?>(string.Empty, CultureInfo.GetCultureInfo("en-US"));

// Then
dateTime.ShouldBe(null);
}
}
}