Skip to content

Commit d2176b6

Browse files
committed
fully implement option validators
1 parent a6a2c1a commit d2176b6

File tree

2 files changed

+281
-32
lines changed

2 files changed

+281
-32
lines changed
+124-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,46 @@
11
namespace Danom.Validation;
22

3+
using System.Text.RegularExpressions;
34
using FluentValidation;
45
using FluentValidation.Validators;
56

7+
internal static partial class ValidationHelpers
8+
{
9+
internal static bool Validate<T, TValue>(
10+
ValidationContext<T> context,
11+
IValidator<TValue> validator,
12+
TValue instance)
13+
{
14+
var validationResult = validator.Validate(instance);
15+
16+
if (!validationResult.IsValid)
17+
{
18+
var quotedDisplayName = string.Concat("'", context.DisplayName, "'");
19+
validationResult.Errors.ForEach(e =>
20+
context.AddFailure(
21+
propertyName: context.DisplayName,
22+
errorMessage: ValidationHelpers.ReplaceMissingDisplayName(e.ErrorMessage, quotedDisplayName)));
23+
}
24+
25+
return validationResult.IsValid;
26+
}
27+
28+
static string ReplaceMissingDisplayName(string errorMessage, string properDisplayName) =>
29+
QuotedDisplayNameRegex().Replace(errorMessage, properDisplayName);
30+
31+
[GeneratedRegex(@"''")]
32+
private static partial Regex QuotedDisplayNameRegex();
33+
}
34+
635
/// <summary>
7-
/// A validator for <see cref="Option{TValue}"/> that validates the value if it is Some.
36+
/// A validator for <see cref="Option{TValue}"/> that validates the value if it
37+
/// is Some, otherwise the value is considered valid.
838
/// </summary>
939
/// <typeparam name="T"></typeparam>
1040
/// <typeparam name="TValue"></typeparam>
1141
/// <param name="validator"></param>
12-
public class OptionValidator<T, TValue>(IValidator<TValue> validator) : PropertyValidator<T, Option<TValue>>
42+
public sealed class OptionalValidator<T, TValue>(IValidator<TValue> validator)
43+
: PropertyValidator<T, Option<TValue>>
1344
{
1445
/// <summary>
1546
/// Specifies the name of the validator.
@@ -24,15 +55,71 @@ public class OptionValidator<T, TValue>(IValidator<TValue> validator) : Property
2455
/// <returns></returns>
2556
public override bool IsValid(ValidationContext<T> context, Option<TValue> value) =>
2657
value.Match(
27-
some: x => validator.Validate(x).IsValid,
58+
some: x => ValidationHelpers.Validate(context, validator, x),
2859
none: () => true);
60+
61+
/// <summary>
62+
/// Returns the default error message template for the validator.
63+
/// </summary>
64+
/// <param name="errorCode"></param>
65+
/// <returns></returns>
66+
protected override string GetDefaultMessageTemplate(string errorCode) =>
67+
"'{PropertyName}' is optional, but invalid.";
68+
}
69+
70+
/// <summary>
71+
/// A validator for <see cref="Option{TValue}"/> that validates the value if it
72+
/// is Some, otherwise the value is considered invalid.
73+
/// </summary>
74+
/// <typeparam name="T"></typeparam>
75+
/// <typeparam name="TValue"></typeparam>
76+
/// <param name="validator"></param>
77+
public class RequiredValidator<T, TValue>(IValidator<TValue> validator) : PropertyValidator<T, Option<TValue>>
78+
{
79+
/// <summary>
80+
/// Specifies the name of the validator.
81+
/// </summary>
82+
public override string Name => "OptionNotNoneValidator";
83+
84+
/// <summary>
85+
/// Returns whether the Option is valid given the specified context.
86+
/// </summary>
87+
/// <param name="context"></param>
88+
/// <param name="value"></param>
89+
/// <returns></returns>
90+
public override bool IsValid(ValidationContext<T> context, Option<TValue> value) =>
91+
value.Match(
92+
some: x => ValidationHelpers.Validate(context, validator, x),
93+
none: () => false);
94+
95+
/// <summary>
96+
/// Returns the default error message template for the validator.
97+
/// </summary>
98+
/// <param name="errorCode"></param>
99+
/// <returns></returns>
100+
protected override string GetDefaultMessageTemplate(string errorCode) =>
101+
"'{PropertyName}' is required and invalid or missing.";
29102
}
30103

31104
/// <summary>
32105
/// Contains extension methods for <see cref="IRuleBuilder{T, TValue}"/>.
33106
/// </summary>
34107
public static class OptionValidatorExtensions
35108
{
109+
/// <summary>
110+
/// Set the validator for an Option value. The value is considered valid if
111+
/// it is None.
112+
/// </summary>
113+
/// <typeparam name="T"></typeparam>
114+
/// <typeparam name="TValue"></typeparam>
115+
/// <param name="ruleBuilder"></param>
116+
/// <param name="validator"></param>
117+
/// <returns></returns>
118+
public static IRuleBuilder<T, Option<TValue>> Optional<T, TValue>(
119+
this IRuleBuilder<T, Option<TValue>> ruleBuilder,
120+
IValidator<TValue> validator) =>
121+
ruleBuilder.SetValidator(new OptionalValidator<T, TValue>(validator));
122+
36123
/// <summary>
37124
/// Specifies a validator for the value if it is Some, otherwise the value
38125
/// is considered valid.
@@ -42,28 +129,56 @@ public static class OptionValidatorExtensions
42129
/// <param name="ruleBuilder"></param>
43130
/// <param name="action"></param>
44131
/// <returns></returns>
45-
public static IRuleBuilder<T, Option<TValue>> WhenSome<T, TValue>(
132+
public static IRuleBuilder<T, Option<TValue>> Optional<T, TValue>(
46133
this IRuleBuilder<T, Option<TValue>> ruleBuilder,
47134
Action<IRuleBuilder<TValue, TValue>> action)
48135
{
49136
var inlineValidator = new InlineValidator<TValue>();
50137
action(inlineValidator.RuleFor(x => x));
51-
var optionValidator = new OptionValidator<T, TValue>(inlineValidator);
138+
var optionValidator = new OptionalValidator<T, TValue>(inlineValidator);
52139
return ruleBuilder.SetValidator(optionValidator);
53140
}
54141

55142
/// <summary>
56-
/// Set the validator for an Option value.
143+
/// Indicates that an <see cref="Option{T}"/> is required to have a value.
144+
/// </summary>
145+
/// <typeparam name="T"></typeparam>
146+
/// <typeparam name="TValue"></typeparam>
147+
/// <param name="ruleBuilder"></param>
148+
/// <returns></returns>
149+
public static IRuleBuilder<T, Option<TValue>> Required<T, TValue>(
150+
this IRuleBuilder<T, Option<TValue>> ruleBuilder) =>
151+
ruleBuilder.SetValidator(new RequiredValidator<T, TValue>(new InlineValidator<TValue>()));
152+
153+
/// <summary>
154+
/// Indicates that an <see cref="Option{T}"/> is required to have a value.
57155
/// </summary>
58156
/// <typeparam name="T"></typeparam>
59157
/// <typeparam name="TValue"></typeparam>
60158
/// <param name="ruleBuilder"></param>
61159
/// <param name="validator"></param>
62160
/// <returns></returns>
63-
public static IRuleBuilder<T, Option<TValue>> SetValidator<T, TValue>(
161+
public static IRuleBuilder<T, Option<TValue>> Required<T, TValue>(
162+
this IRuleBuilder<T, Option<TValue>> ruleBuilder,
163+
IValidator<TValue> validator) =>
164+
ruleBuilder.SetValidator(new RequiredValidator<T, TValue>(validator));
165+
166+
/// <summary>
167+
/// Indicates that an <see cref="Option{T}"/> is required to have a value.
168+
/// </summary>
169+
/// <typeparam name="T"></typeparam>
170+
/// <typeparam name="TValue"></typeparam>
171+
/// <param name="ruleBuilder"></param>
172+
/// <param name="action"></param>
173+
/// <returns></returns>
174+
public static IRuleBuilder<T, Option<TValue>> Required<T, TValue>(
64175
this IRuleBuilder<T, Option<TValue>> ruleBuilder,
65-
IValidator<TValue> validator)
176+
Action<IRuleBuilder<TValue, TValue>> action)
66177
{
67-
return ruleBuilder.SetValidator(new OptionValidator<T, TValue>(validator));
178+
var inlineValidator = new InlineValidator<TValue>();
179+
action(inlineValidator.RuleFor(x => x));
180+
var optionValidator = new RequiredValidator<T, TValue>(inlineValidator);
181+
return ruleBuilder.SetValidator(optionValidator);
68182
}
183+
69184
}

test/Danom.Validation.Tests/OptionValidatorTests.cs

+157-23
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,174 @@ namespace Danom.Validation.Tests;
55

66
public sealed class OptionValidatorTests
77
{
8-
[Fact]
9-
public void ReturnsOkResult_WhenValidationSucceeds()
8+
public sealed class Optional
109
{
11-
var input = new TestInput { Value = Option<int>.Some(1) };
12-
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
10+
[Fact]
11+
public void ReturnsOkResult_WhenValidationSucceeds()
12+
{
1313

14-
AssertResult.IsOk(input, result);
15-
Assert.False(result.IsError);
16-
}
14+
var input = new TestInput { Value = Option<int>.Some(1) };
15+
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
1716

18-
[Fact]
19-
public void ReturnsErrorResult_WhenValidationFails()
20-
{
21-
var input = new TestInput { Value = Option<int>.Some(0) };
22-
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
17+
AssertResult.IsOk(input, result);
18+
Assert.False(result.IsError);
19+
}
2320

24-
AssertResult.IsError(result);
25-
Assert.False(result.IsOk);
26-
}
21+
[Fact]
22+
public void ReturnsErrorResult_WhenValidationFails()
23+
{
24+
var input = new TestInput { Value = Option<int>.Some(0) };
25+
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
2726

28-
public sealed class TestInput
29-
{
30-
public Option<int> Value { get; set; }
27+
AssertResult.IsError(result);
28+
Assert.False(result.IsOk);
29+
Assert.Equal(
30+
$"Error([{Environment.NewLine}Value - 'Value' must be greater than '0'., 'Value' is optional, but invalid.{Environment.NewLine}])",
31+
result.ToString());
32+
}
3133

32-
public override string ToString() => Value.ToString();
34+
public sealed class TestInput
35+
{
36+
public Option<int> Value { get; set; }
37+
38+
public override string ToString() => Value.ToString();
39+
}
40+
41+
public sealed class TestInputValidator : AbstractValidator<TestInput>
42+
{
43+
public TestInputValidator()
44+
{
45+
RuleFor(x => x.Value).Optional(x => x.GreaterThan(0));
46+
}
47+
}
3348
}
3449

35-
public sealed class TestInputValidator : AbstractValidator<TestInput>
50+
public sealed class Required
3651
{
37-
public TestInputValidator()
52+
public sealed class TestInput
3853
{
39-
RuleFor(x => x.Value).WhenSome(x => x.GreaterThan(0));
54+
public Option<int> Value { get; set; }
55+
56+
public override string ToString() => Value.ToString();
57+
}
58+
59+
public sealed class TestInputValidator : AbstractValidator<TestInput>
60+
{
61+
public TestInputValidator()
62+
{
63+
RuleFor(x => x.Value).Required();
64+
}
65+
}
66+
67+
public sealed class TestInputValidator2 : AbstractValidator<TestInput>
68+
{
69+
public TestInputValidator2()
70+
{
71+
RuleFor(x => x.Value).Required(x => x.GreaterThan(2));
72+
}
73+
}
74+
75+
public sealed class TestId
76+
{
77+
public int Value { get; set; }
78+
}
79+
80+
public sealed class TestIdValidator : AbstractValidator<TestId>
81+
{
82+
public TestIdValidator()
83+
{
84+
RuleFor(x => x.Value).GreaterThan(0).WithMessage("{PropertyName} must be a valid TestId.");
85+
}
86+
}
87+
88+
public sealed class TestIdInput
89+
{
90+
public Option<TestId> TestId { get; set; }
4091
}
41-
}
4292

93+
public sealed class TestIdInputValidator : AbstractValidator<TestIdInput>
94+
{
95+
public TestIdInputValidator()
96+
{
97+
RuleFor(x => x.TestId).Required(new TestIdValidator());
98+
}
99+
}
100+
101+
[Fact]
102+
public void ReturnsErrorResult_WhenValueIsNone()
103+
{
104+
var input = new TestInput { Value = Option<int>.None() };
105+
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
106+
107+
AssertResult.IsError(result);
108+
Assert.False(result.IsOk);
109+
}
110+
111+
[Fact]
112+
public void ReturnsOkResult_WhenValueIsSome()
113+
{
114+
var input = new TestInput { Value = Option<int>.Some(1) };
115+
var result = ValidationResult<TestInput>.From<TestInputValidator>(input);
116+
117+
AssertResult.IsOk(input, result);
118+
Assert.False(result.IsError);
119+
}
120+
121+
[Fact]
122+
public void ReturnsOkResult_WhenValueIsSome2()
123+
{
124+
var input = new TestInput { Value = Option<int>.Some(3) };
125+
var result = ValidationResult<TestInput>.From<TestInputValidator2>(input);
126+
127+
AssertResult.IsOk(input, result);
128+
Assert.False(result.IsError);
129+
}
130+
131+
[Fact]
132+
public void ReturnsErrorResult_WhenValueIsNone2()
133+
{
134+
var input = new TestInput { Value = Option<int>.None() };
135+
var result = ValidationResult<TestInput>.From<TestInputValidator2>(input);
136+
137+
AssertResult.IsError(result);
138+
Assert.False(result.IsOk);
139+
Assert.Equal(
140+
$"Error([{Environment.NewLine}Value - 'Value' is required and invalid or missing.{Environment.NewLine}])",
141+
result.ToString());
142+
143+
input = new TestInput { Value = Option<int>.Some(2) };
144+
result = ValidationResult<TestInput>.From<TestInputValidator2>(input);
145+
146+
AssertResult.IsError(result);
147+
Assert.False(result.IsOk);
148+
149+
Assert.Equal(
150+
$"Error([{Environment.NewLine}Value - 'Value' must be greater than '2'., 'Value' is required and invalid or missing.{Environment.NewLine}])",
151+
result.ToString());
152+
}
153+
154+
[Fact]
155+
public void ReturnsOkResult_WhenTestIdIsSome()
156+
{
157+
var input = new TestIdInput { TestId = Option<TestId>.Some(new TestId() { Value = 1 }) };
158+
var result = ValidationResult<TestIdInput>.From<TestIdInputValidator>(input);
159+
160+
AssertResult.IsOk(input, result);
161+
Assert.False(result.IsError);
162+
}
163+
164+
[Fact]
165+
public void ReturnsErrorResult_WhenTestIdIsNone()
166+
{
167+
var input = new TestIdInput { TestId = Option<TestId>.None() };
168+
var result = ValidationResult<TestIdInput>.From<TestIdInputValidator>(input);
169+
170+
AssertResult.IsError(result);
171+
Assert.False(result.IsOk);
172+
Assert.Equal(
173+
$"Error([{Environment.NewLine}TestId - 'Test Id' is required and invalid or missing.{Environment.NewLine}])",
174+
result.ToString());
175+
}
176+
}
43177

44178
}

0 commit comments

Comments
 (0)