From 7615561e4d499d86ca40bb6de6c20acb1cbef8e6 Mon Sep 17 00:00:00 2001 From: antek Date: Mon, 4 May 2026 15:07:36 +0200 Subject: [PATCH 1/2] allow validation chaining --- .../Unit/Prompts/TextPromptTests.cs | 188 ++++++++++++++++++ .../Prompts/TextPromptExtensions.cs | 28 ++- 2 files changed, 215 insertions(+), 1 deletion(-) diff --git a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs index 09d0eb983..d5716c480 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -489,4 +489,192 @@ public async Task Uses_case_insensitive_comparison_when_no_comparer_is_passed(st // Then result.ShouldBe("Yes"); } + + [Fact] + public void Validate_BoolOverload_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a"); + }, "missing a"); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_BoolOverload_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a"); + }, "missing a"); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_BoolOverload_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => s.Contains("a"), "missing a"); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } + + [Fact] + public void Validate_FuncOverload_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_FuncOverload_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_FuncOverload_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a") ); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } + + [Fact] + public void Validate_MixedOverloads_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_MixedOverloads_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_MixedOverloads_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a") ); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/TextPromptExtensions.cs b/src/Spectre.Console/Prompts/TextPromptExtensions.cs index e0e9c4662..bb67f36ce 100644 --- a/src/Spectre.Console/Prompts/TextPromptExtensions.cs +++ b/src/Spectre.Console/Prompts/TextPromptExtensions.cs @@ -178,9 +178,20 @@ public static TextPrompt DefaultValue(this TextPrompt obj, T value) public static TextPrompt Validate(this TextPrompt obj, Func validator, string? message = null) { ArgumentNullException.ThrowIfNull(obj); + ArgumentNullException.ThrowIfNull(validator); + var previous = obj.Validator; obj.Validator = result => { + if (previous is not null) + { + var previousResult = previous(result); + if (!previousResult.Successful) + { + return previousResult; + } + } + if (validator(result)) { return ValidationResult.Success(); @@ -202,8 +213,23 @@ public static TextPrompt Validate(this TextPrompt obj, Func va public static TextPrompt Validate(this TextPrompt obj, Func validator) { ArgumentNullException.ThrowIfNull(obj); + ArgumentNullException.ThrowIfNull(validator); + + var previous = obj.Validator; + + obj.Validator = result => + { + if (previous is not null) + { + var previousResult = previous(result); + if (!previousResult.Successful) + { + return previousResult; + } + } - obj.Validator = validator; + return validator(result); + }; return obj; } From 0cc1b47e5778be23ba3e69cf8387b751b782b185 Mon Sep 17 00:00:00 2001 From: antek Date: Sun, 17 May 2026 13:09:48 +0200 Subject: [PATCH 2/2] add tests for chaining 3 validators --- .../Unit/Prompts/TextPromptTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs index d5716c480..d2646a8db 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -677,4 +677,68 @@ public void Validate_MixedOverloads_Returns_Success_When_All_Validators_Pass() // Then result.ShouldBeEquivalentTo(ValidationResult.Success()); } + + [Fact] + public void Validate_MixedOverloads_WithThreeValidators_Returns_ThirdValidationError() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + var thirdInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") + ? ValidationResult.Success() + : ValidationResult.Error("missing a"); + }) + .Validate(s => + { + thirdInvoked = true; + return s.EndsWith("z"); + }, "must end with z"); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("must end with z")); + secondInvoked.ShouldBeTrue(); + thirdInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_MixedOverloads_WithThreeValidators_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + var thirdInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") + ? ValidationResult.Success() + : ValidationResult.Error("missing a"); + }) + .Validate(s => + { + thirdInvoked = true; + return s.EndsWith("z"); + }, "must end with z"); + + // When + var result = prompt.Validator?.Invoke("abz"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + secondInvoked.ShouldBeTrue(); + thirdInvoked.ShouldBeTrue(); + } } \ No newline at end of file