Skip to content

Commit 9667b1e

Browse files
committed
Add conditional rule support: When/Unless for RuleSet
Conditional rule application and documentation updates - Conditional RuleSet API -- Introduce `When` and multiple overloads of `Unless` to enable conditional and alternative rule application in RuleSet. -- Add delegate types `RuleSetBuilder` and `RuleSetFactory` to support new API patterns. -- Ensure property prefix normalization and problem aggregation in conditional scenarios. - Documentation and Examples -- Update README to document new conditional features, including usage examples for `When` and `Unless`. -- Clarify that conditional rules do not alter problem metadata. - Testing -- Add comprehensive unit tests for all `When` and `Unless` overloads, including edge cases and property prefix handling. -- Update existing tests to ensure non-null access to problem metadata. - Versioning -- Bump project preview version to `-preview-4.0` to reflect new features.
1 parent 09174a0 commit 9667b1e

7 files changed

Lines changed: 390 additions & 15 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</PropertyGroup>
1111
<PropertyGroup>
1212
<SVVer>1.0.0</SVVer>
13-
<SVPreview>-preview-3.0</SVPreview>
13+
<SVPreview>-preview-4.0</SVPreview>
1414
</PropertyGroup>
1515
<PropertyGroup>
1616
<SPVer>1.0.0-preview-6.0</SPVer>

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Core concepts
2222
- `RuleSet`: fluent DSL that accumulates `Problems` whenever a rule fails.
2323
- `IValidable` and `ValidateFunc`: plug-in points for nested validations.
2424
- Implicit conversion: a `RuleSet` can be treated as `Problems?` or queried via `HasProblems(out var problems)`.
25+
- Conditional rules: `When` and `Unless` let you apply rule groups conditionally or as alternatives.
2526

2627
Quick start
2728
```csharp
@@ -73,6 +74,32 @@ var set = Rules.Set()
7374
.GreaterThanOrEqual(Quantity, 1);
7475
```
7576

77+
Conditional rules (When/Unless)
78+
```csharp
79+
// Apply rules only when a condition is true
80+
var set = Rules.Set()
81+
.When(isGuestCheckout,
82+
s => s.NotEmpty(Email).Email(Email));
83+
84+
// Skip rules when a condition is true
85+
set = set.Unless(hasAddressOnFile,
86+
s => s.NotEmpty(ShippingAddress.Street)
87+
.NotEmpty(ShippingAddress.City)
88+
.NotEmpty(ShippingAddress.ZipCode));
89+
90+
// Alternative groups: add problems from both if both fail
91+
set = set.Unless(
92+
s => s.NotEmpty(PromoCode), // condition group
93+
s => s.Min(TotalAmount, 100)); // alternative group
94+
95+
// Using factories/builders with prefixes preserved/normalized
96+
set = Rules.Set<object>()
97+
.WithPropertyPrefix("order")
98+
.Unless(
99+
() => Rules.Set().WithPropertyPrefix("order").NotEmpty(order.CustomerId),
100+
s => s.NotEmpty(order.CustomerId)); // Property becomes "CustomerId" (prefix removed)
101+
```
102+
76103
Custom rules with Must
77104
```csharp
78105
var set = Rules.Set()
@@ -174,6 +201,7 @@ SmartProblems integration
174201
- `expected` (`Rules.ExpectedValueProperty`): expected value(s), when applicable.
175202
- `pattern` (`Rules.PatternProperty`): regex used in `Matches/NotMatches`.
176203
- For dual-operand rules (`Both*`, comparisons), properties and values are attached for both operands.
204+
- Conditional rules (`When/Unless`) simply control whether rule groups run; metadata remains consistent for each failing rule.
177205

178206
Best practices
179207
- Centralize validation per request/DTO in a single function that returns `Problems?`.

RoyalCode.SmartValidations.Tests/RuleSetRules/RuleSetTests.LessGreater.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void LessThanOrEqual_Int_Greater_ProducesProblem()
8686
// Assert
8787
Assert.True(set.HasProblems(out var problems));
8888
var p = Assert.Single(problems!);
89-
Assert.Equal(Rules.LessThanOrEqual, p.Extensions[Rules.RuleProperty]);
89+
Assert.Equal(Rules.LessThanOrEqual, p.Extensions![Rules.RuleProperty]);
9090
var props = Assert.IsType<string?[]>(p.Extensions["properties"]);
9191
Assert.Equal(new[] { nameof(value1), nameof(value2) }, props);
9292
var vals = Assert.IsType<int[]>(p.Extensions["values"]);
@@ -121,7 +121,7 @@ public void GreaterThan_Int_NotGreater_ProducesProblem()
121121
// Assert
122122
Assert.True(set.HasProblems(out var problems));
123123
var p = Assert.Single(problems!);
124-
Assert.Equal(Rules.GreaterThan, p.Extensions[Rules.RuleProperty]);
124+
Assert.Equal(Rules.GreaterThan, p.Extensions![Rules.RuleProperty]);
125125
var props = Assert.IsType<string?[]>(p.Extensions["properties"]);
126126
Assert.Equal(new[] { nameof(value1), nameof(value2) }, props);
127127
var vals = Assert.IsType<int[]>(p.Extensions["values"]);
@@ -156,7 +156,7 @@ public void GreaterThanOrEqual_Int_Less_ProducesProblem()
156156
// Assert
157157
Assert.True(set.HasProblems(out var problems));
158158
var p = Assert.Single(problems!);
159-
Assert.Equal(Rules.GreaterThanOrEqual, p.Extensions[Rules.RuleProperty]);
159+
Assert.Equal(Rules.GreaterThanOrEqual, p.Extensions![Rules.RuleProperty]);
160160
var props = Assert.IsType<string?[]>(p.Extensions["properties"]);
161161
Assert.Equal(new[] { nameof(value1), nameof(value2) }, props);
162162
var vals = Assert.IsType<int[]>(p.Extensions["values"]);

RoyalCode.SmartValidations.Tests/RuleSetRules/RuleSetTests.StringAndPattern.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void Matches_PatternMismatch_ProducesProblem()
1818
Assert.True(set.HasProblems(out var problems));
1919
var p = Assert.Single(problems!);
2020
Assert.Equal(nameof(value), p.Property);
21-
Assert.Equal(Rules.MatchPattern, p.Extensions[Rules.RuleProperty]);
21+
Assert.Equal(Rules.MatchPattern, p.Extensions![Rules.RuleProperty]);
2222
Assert.Equal(pattern, p.Extensions[Rules.PatternProperty]);
2323
Assert.Equal("abc", p.Extensions[Rules.CurrentValueProperty]);
2424
}
@@ -51,7 +51,7 @@ public void Matches_RegexOverload_Mismatch_ProducesProblem()
5151
// Assert
5252
Assert.True(set.HasProblems(out var problems));
5353
var p = Assert.Single(problems!);
54-
Assert.Equal(Rules.MatchPattern, p.Extensions[Rules.RuleProperty]);
54+
Assert.Equal(Rules.MatchPattern, p.Extensions![Rules.RuleProperty]);
5555
Assert.Equal(regex.ToString(), p.Extensions[Rules.PatternProperty]);
5656
Assert.Equal("abc", p.Extensions[Rules.CurrentValueProperty]);
5757
}
@@ -70,7 +70,7 @@ public void NotMatches_PatternMatch_ProducesProblem()
7070
Assert.True(set.HasProblems(out var problems));
7171
var p = Assert.Single(problems!);
7272
Assert.Equal(nameof(value), p.Property);
73-
Assert.Equal(Rules.NotMatchPattern, p.Extensions[Rules.RuleProperty]);
73+
Assert.Equal(Rules.NotMatchPattern, p.Extensions![Rules.RuleProperty]);
7474
Assert.Equal(pattern, p.Extensions[Rules.PatternProperty]);
7575
Assert.Equal("abc", p.Extensions[Rules.CurrentValueProperty]);
7676
}
@@ -88,7 +88,7 @@ public void StartsWith_Mismatch_ProducesProblem()
8888
// Assert
8989
Assert.True(set.HasProblems(out var problems));
9090
var p = Assert.Single(problems!);
91-
Assert.Equal(Rules.StartsWith, p.Extensions[Rules.RuleProperty]);
91+
Assert.Equal(Rules.StartsWith, p.Extensions![Rules.RuleProperty]);
9292
Assert.Equal("hello", p.Extensions[Rules.CurrentValueProperty]);
9393
Assert.Equal(expected, p.Extensions[Rules.ExpectedValueProperty]);
9494
}
@@ -106,7 +106,7 @@ public void EndsWith_Mismatch_ProducesProblem()
106106
// Assert
107107
Assert.True(set.HasProblems(out var problems));
108108
var p = Assert.Single(problems!);
109-
Assert.Equal(Rules.EndsWith, p.Extensions[Rules.RuleProperty]);
109+
Assert.Equal(Rules.EndsWith, p.Extensions![Rules.RuleProperty]);
110110
Assert.Equal("file.txt", p.Extensions[Rules.CurrentValueProperty]);
111111
Assert.Equal(expected, p.Extensions[Rules.ExpectedValueProperty]);
112112
}
@@ -124,7 +124,7 @@ public void Contains_MissingSubstring_ProducesProblem()
124124
// Assert
125125
Assert.True(set.HasProblems(out var problems));
126126
var p = Assert.Single(problems!);
127-
Assert.Equal(Rules.Contains, p.Extensions[Rules.RuleProperty]);
127+
Assert.Equal(Rules.Contains, p.Extensions![Rules.RuleProperty]);
128128
Assert.Equal("abcdef", p.Extensions[Rules.CurrentValueProperty]);
129129
Assert.Equal(expected, p.Extensions[Rules.ExpectedValueProperty]);
130130
}
@@ -142,7 +142,7 @@ public void NotContain_ContainsUnexpected_ProducesProblem()
142142
// Assert
143143
Assert.True(set.HasProblems(out var problems));
144144
var p = Assert.Single(problems!);
145-
Assert.Equal(Rules.NotContain, p.Extensions[Rules.RuleProperty]);
145+
Assert.Equal(Rules.NotContain, p.Extensions![Rules.RuleProperty]);
146146
Assert.Equal("abcdef", p.Extensions[Rules.CurrentValueProperty]);
147147
Assert.Equal(unexpected, p.Extensions[Rules.ExpectedValueProperty]);
148148
}
@@ -159,7 +159,7 @@ public void OnlyLetters_WithDigits_ProducesProblem()
159159
// Assert
160160
Assert.True(set.HasProblems(out var problems));
161161
var p = Assert.Single(problems!);
162-
Assert.Equal(Rules.OnlyLetters, p.Extensions[Rules.RuleProperty]);
162+
Assert.Equal(Rules.OnlyLetters, p.Extensions![Rules.RuleProperty]);
163163
Assert.Equal(value, p.Extensions[Rules.CurrentValueProperty]);
164164
}
165165

@@ -175,7 +175,7 @@ public void OnlyDigits_WithLetters_ProducesProblem()
175175
// Assert
176176
Assert.True(set.HasProblems(out var problems));
177177
var p = Assert.Single(problems!);
178-
Assert.Equal(Rules.OnlyDigits, p.Extensions[Rules.RuleProperty]);
178+
Assert.Equal(Rules.OnlyDigits, p.Extensions![Rules.RuleProperty]);
179179
Assert.Equal(value, p.Extensions[Rules.CurrentValueProperty]);
180180
}
181181

@@ -191,7 +191,7 @@ public void OnlyLettersOrDigits_WithSymbol_ProducesProblem()
191191
// Assert
192192
Assert.True(set.HasProblems(out var problems));
193193
var p = Assert.Single(problems!);
194-
Assert.Equal(Rules.OnlyLettersOrDigits, p.Extensions[Rules.RuleProperty]);
194+
Assert.Equal(Rules.OnlyLettersOrDigits, p.Extensions![Rules.RuleProperty]);
195195
Assert.Equal(value, p.Extensions[Rules.CurrentValueProperty]);
196196
}
197197

@@ -207,7 +207,7 @@ public void NoWhiteSpace_WithSpace_ProducesProblem()
207207
// Assert
208208
Assert.True(set.HasProblems(out var problems));
209209
var p = Assert.Single(problems!);
210-
Assert.Equal(Rules.NoWhiteSpace, p.Extensions[Rules.RuleProperty]);
210+
Assert.Equal(Rules.NoWhiteSpace, p.Extensions![Rules.RuleProperty]);
211211
Assert.Equal(value, p.Extensions[Rules.CurrentValueProperty]);
212212
}
213213
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
namespace RoyalCode.SmartValidations.Tests.RuleSetRules;
2+
3+
public partial class RuleSetTests
4+
{
5+
[Fact]
6+
public void When_ConditionTrue_AppliesRules()
7+
{
8+
// Arrange
9+
string name = "";
10+
11+
// Act
12+
var set = Rules.Set().When(true, s => s.NotEmpty(name));
13+
14+
// Assert
15+
Assert.True(set.HasProblems(out var problems));
16+
var p = Assert.Single(problems!);
17+
Assert.Equal(nameof(name), p.Property);
18+
Assert.Equal(Rules.NotNullOrNotEmpty, p.Extensions![Rules.RuleProperty]);
19+
}
20+
21+
[Fact]
22+
public void When_ConditionFalse_DoesNotApplyRules()
23+
{
24+
// Arrange
25+
string name = "";
26+
27+
// Act
28+
var set = Rules.Set().When(false, s => s.NotEmpty(name));
29+
30+
// Assert
31+
Assert.False(set.HasProblems(out var problems));
32+
Assert.Null(problems);
33+
}
34+
35+
[Fact]
36+
public void Unless_BoolTrue_SkipsRules()
37+
{
38+
// Arrange
39+
string code = "";
40+
41+
// Act
42+
var set = Rules.Set().Unless(true, s => s.NotEmpty(code));
43+
44+
// Assert
45+
Assert.False(set.HasProblems(out var problems));
46+
Assert.Null(problems);
47+
}
48+
49+
[Fact]
50+
public void Unless_BoolFalse_AppliesRules()
51+
{
52+
// Arrange
53+
string code = "";
54+
55+
// Act
56+
var set = Rules.Set().Unless(false, s => s.NotEmpty(code));
57+
58+
// Assert
59+
Assert.True(set.HasProblems(out var problems));
60+
var p = Assert.Single(problems!);
61+
Assert.Equal(nameof(code), p.Property);
62+
Assert.Equal(Rules.NotNullOrNotEmpty, p.Extensions![Rules.RuleProperty]);
63+
}
64+
65+
[Fact]
66+
public void Unless_Builder_BothGroupsFail_AddsBothProblems()
67+
{
68+
// Arrange
69+
string a = "";
70+
string b = "";
71+
72+
// Act
73+
var set = Rules.Set()
74+
.Unless(
75+
s => s.NotEmpty(a),
76+
s => s.NotEmpty(b));
77+
78+
// Assert
79+
Assert.True(set.HasProblems(out var problems));
80+
Assert.Equal(2, problems!.Count);
81+
}
82+
83+
[Fact]
84+
public void Unless_Builder_ConditionPasses_NoProblemsAdded()
85+
{
86+
// Arrange
87+
string a = "ok";
88+
string b = "";
89+
90+
// Act
91+
var set = Rules.Set()
92+
.Unless(
93+
s => s.NotEmpty(a),
94+
s => s.NotEmpty(b));
95+
96+
// Assert
97+
Assert.False(set.HasProblems(out var problems));
98+
Assert.Null(problems);
99+
}
100+
101+
[Fact]
102+
public void Unless_Builder_AlternativePasses_NoProblemsAdded()
103+
{
104+
// Arrange
105+
string a = "";
106+
string b = "ok";
107+
108+
// Act
109+
var set = Rules.Set()
110+
.Unless(
111+
s => s.NotEmpty(a),
112+
s => s.NotEmpty(b));
113+
114+
// Assert
115+
Assert.False(set.HasProblems(out var problems));
116+
Assert.Null(problems);
117+
}
118+
119+
[Fact]
120+
public void Unless_FactoryAndBuilder_PrefixIsPreservedAndRemovedFromProperty()
121+
{
122+
// Arrange
123+
var model = new { Name = "" };
124+
var set = Rules.Set().WithPropertyPrefix("model");
125+
126+
// Act
127+
set = set.Unless(
128+
() => Rules.Set().WithPropertyPrefix("model").NotEmpty(model.Name),
129+
s => s.NotEmpty(model.Name));
130+
131+
// Assert
132+
Assert.True(set.HasProblems(out var problems));
133+
Assert.Equal(2, problems.Count);
134+
Assert.All(problems, p => Assert.Equal("Name", p.Property));
135+
136+
// Arrange
137+
set = Rules.Set().WithPropertyPrefix("model");
138+
139+
// Act
140+
set = set.Unless(
141+
s => s.NotEmpty(model.Name),
142+
s => s.NotEmpty(model.Name));
143+
144+
// Assert
145+
Assert.True(set.HasProblems(out problems));
146+
Assert.Equal(2, problems.Count);
147+
Assert.All(problems, p => Assert.Equal("Name", p.Property));
148+
}
149+
150+
[Fact]
151+
public void Unless_PreEvaluated_AddsWhenBothHaveProblems()
152+
{
153+
// Arrange
154+
string v1 = "";
155+
string v2 = "";
156+
var cond = Rules.Set().NotEmpty(v1);
157+
var alt = Rules.Set().NotEmpty(v2);
158+
159+
// Act
160+
var set = Rules.Set().Unless(cond, alt);
161+
162+
// Assert
163+
Assert.True(set.HasProblems(out var problems));
164+
Assert.Equal(2, problems!.Count);
165+
}
166+
}

0 commit comments

Comments
 (0)