Skip to content

Commit 0539d43

Browse files
Add support for inheritance of validations (#19)
1 parent 78bd07a commit 0539d43

File tree

48 files changed

+535
-43
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+535
-43
lines changed

src/Immediate.Validations.Generators/ITypeSymbolExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,31 @@ typeSymbol is
103103
},
104104
},
105105
};
106+
107+
public static bool IsIValidationTarget(this INamedTypeSymbol? typeSymbol) =>
108+
typeSymbol is
109+
{
110+
MetadataName: "IValidationTarget`1",
111+
ContainingNamespace:
112+
{
113+
Name: "Shared",
114+
ContainingNamespace:
115+
{
116+
Name: "Validations",
117+
ContainingNamespace:
118+
{
119+
Name: "Immediate",
120+
ContainingNamespace.IsGlobalNamespace: true,
121+
},
122+
},
123+
},
124+
};
125+
126+
public static bool IsValidationTarget([NotNullWhen(returnValue: true)] this INamedTypeSymbol? typeSymbol) =>
127+
typeSymbol is not null
128+
&& typeSymbol.Interfaces
129+
.Any(i =>
130+
i.IsIValidationTarget()
131+
&& SymbolEqualityComparer.Default.Equals(typeSymbol, i.TypeArguments[0])
132+
);
106133
}

src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ CancellationToken token
2525
var symbol = (INamedTypeSymbol)context.TargetSymbol;
2626
var @namespace = symbol.ContainingNamespace.ToString().NullIf("<global namespace>");
2727
var outerClasses = GetOuterClasses(symbol);
28+
var baseValidatorTypes = GetBaseValidatorTypes(symbol);
2829
var properties = GetProperties(context.SemanticModel.Compilation, symbol, token);
2930

3031
return new()
@@ -33,6 +34,7 @@ CancellationToken token
3334
OuterClasses = outerClasses,
3435
Class = GetClass(symbol),
3536
IsReferenceType = symbol.IsReferenceType,
37+
BaseValidatorTypes = baseValidatorTypes,
3638
Properties = properties,
3739
};
3840
}
@@ -53,19 +55,41 @@ private static Class GetClass(INamedTypeSymbol symbol) =>
5355

5456
private static EquatableReadOnlyList<Class> GetOuterClasses(INamedTypeSymbol symbol)
5557
{
56-
var outerClasses = new List<Class>();
58+
List<Class>? outerClasses = null;
5759
var outerSymbol = symbol.ContainingType;
5860
while (outerSymbol is not null)
5961
{
60-
outerClasses.Add(GetClass(outerSymbol));
62+
(outerClasses ??= []).Add(GetClass(outerSymbol));
6163
outerSymbol = outerSymbol.ContainingType;
6264
}
6365

66+
if (outerClasses is null)
67+
return default;
68+
6469
outerClasses.Reverse();
6570

6671
return outerClasses.ToEquatableReadOnlyList();
6772
}
6873

74+
private static EquatableReadOnlyList<string> GetBaseValidatorTypes(INamedTypeSymbol symbol)
75+
{
76+
List<string>? baseValidatorTypes = null;
77+
78+
if (symbol.BaseType.IsValidationTarget())
79+
(baseValidatorTypes = []).Add(symbol.BaseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
80+
81+
foreach (var i in symbol.Interfaces)
82+
{
83+
if (i.IsValidationTarget())
84+
(baseValidatorTypes ??= []).Add(i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
85+
}
86+
87+
if (baseValidatorTypes is null)
88+
return default;
89+
90+
return baseValidatorTypes.ToEquatableReadOnlyList();
91+
}
92+
6993
private static EquatableReadOnlyList<ValidationTargetProperty> GetProperties(
7094
Compilation compilation,
7195
INamedTypeSymbol symbol,

src/Immediate.Validations.Generators/Models.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public sealed record ValidationTarget
66
public required EquatableReadOnlyList<Class> OuterClasses { get; init; }
77
public required Class Class { get; init; }
88
public required bool IsReferenceType { get; init; }
9+
public required EquatableReadOnlyList<string> BaseValidatorTypes { get; init; }
910
public required EquatableReadOnlyList<ValidationTargetProperty> Properties { get; init; }
1011
}
1112

src/Immediate.Validations.Generators/Templates/Validations.sbntxt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ partial {{ c.type }} {{ c.name }}
1515

1616
partial {{ class.type }} {{ class.name }}
1717
{
18-
public static List<ValidationError> Validate({{ class.name; if is_reference_type; "?"; end }} target)
18+
static List<ValidationError> IValidationTarget<{{ class.name }}>.Validate({{ class.name; if is_reference_type; "?"; end }} target) =>
19+
Validate(target);
20+
21+
public static {{ if class.type == "interface"; "new"; end }} List<ValidationError> Validate({{ class.name; if is_reference_type; "?"; end }} target)
1922
{
2023
{{~ if is_reference_type ~}}
2124
if (target is not { } t)
@@ -35,6 +38,10 @@ partial {{ class.type }} {{ class.name }}
3538

3639
var errors = new List<ValidationError>();
3740

41+
{{~ for bc in base_validator_types ~}}
42+
errors.AddRange({{ bc }}.Validate(t));
43+
{{~ end ~}}
44+
3845
{{~ for p in properties ~}}
3946
__Validate{{ p.property_name }}(errors, t, t.{{ p.property_name }});
4047
{{~ end ~}}

src/Immediate.Validations.Shared/IValidationTarget.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ public interface IValidationTarget<T>
2929
"CA1002:Do not expose generic lists",
3030
Justification = "List<> is returned for performance; this method is generally only used internally."
3131
)]
32-
static abstract List<ValidationError> Validate(T target);
32+
static abstract List<ValidationError> Validate(T? target);
3333
}

src/Immediate.Validations.Shared/ValidateAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ namespace Immediate.Validations.Shared;
77
/// <summary>
88
/// Applied to a class to indicate that validation methods should be generated.
99
/// </summary>
10-
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
10+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
1111
public sealed class ValidateAttribute : Attribute;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Immediate.Validations.Shared;
2+
using Xunit;
3+
4+
namespace Immediate.Validations.FunctionalTests.IntegrationTests;
5+
6+
public sealed partial class InheritanceTests
7+
{
8+
[Validate]
9+
public partial interface IBaseInterface : IValidationTarget<IBaseInterface>
10+
{
11+
[NotEmptyOrWhiteSpace]
12+
string Description { get; }
13+
}
14+
15+
[Validate]
16+
public partial record BaseClass : IValidationTarget<BaseClass>
17+
{
18+
[MinLength(4)]
19+
public required string Id { get; init; }
20+
}
21+
22+
[Validate]
23+
public partial record Class : BaseClass, IBaseInterface, IValidationTarget<Class>
24+
{
25+
public required string Description { get; init; }
26+
}
27+
28+
[Fact]
29+
public void ValidClassHasNoErrors()
30+
{
31+
var @class = new Class
32+
{
33+
Id = "1234",
34+
Description = "Hello World!",
35+
};
36+
37+
var errors = Class.Validate(@class);
38+
39+
Assert.Empty(errors);
40+
}
41+
42+
[Fact]
43+
public void ShortIdHasErrors()
44+
{
45+
var @class = new Class
46+
{
47+
Id = "123",
48+
Description = "Hello World!",
49+
};
50+
51+
var errors = Class.Validate(@class);
52+
53+
Assert.Equal(
54+
[
55+
new()
56+
{
57+
PropertyName = "Id",
58+
ErrorMessage = "String is of length '3', which is shorter than the minimum allowed length of '4'.",
59+
}
60+
],
61+
errors
62+
);
63+
}
64+
65+
[Fact]
66+
public void EmptyDescriptionHasErrors()
67+
{
68+
var @class = new Class
69+
{
70+
Id = "1234",
71+
Description = "",
72+
};
73+
74+
var errors = Class.Validate(@class);
75+
76+
Assert.Equal(
77+
[
78+
new()
79+
{
80+
PropertyName = "Description",
81+
ErrorMessage = "Property must not be `null` or whitespace.",
82+
}
83+
],
84+
errors
85+
);
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//HintName: IV...BaseInterface.g.cs
2+
using System.Collections.Generic;
3+
using Immediate.Validations.Shared;
4+
5+
#nullable enable
6+
#pragma warning disable CS1591
7+
8+
9+
partial interface BaseInterface
10+
{
11+
static List<ValidationError> IValidationTarget<BaseInterface>.Validate(BaseInterface? target) =>
12+
Validate(target);
13+
14+
public static new List<ValidationError> Validate(BaseInterface? target)
15+
{
16+
if (target is not { } t)
17+
{
18+
return
19+
[
20+
new()
21+
{
22+
PropertyName = ".self",
23+
ErrorMessage = "`target` must not be `null`.",
24+
},
25+
];
26+
}
27+
28+
var errors = new List<ValidationError>();
29+
30+
31+
32+
return errors;
33+
}
34+
35+
36+
37+
}
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//HintName: IV...ValidateClass.g.cs
2+
using System.Collections.Generic;
3+
using Immediate.Validations.Shared;
4+
5+
#nullable enable
6+
#pragma warning disable CS1591
7+
8+
9+
partial class ValidateClass
10+
{
11+
static List<ValidationError> IValidationTarget<ValidateClass>.Validate(ValidateClass? target) =>
12+
Validate(target);
13+
14+
public static List<ValidationError> Validate(ValidateClass? target)
15+
{
16+
if (target is not { } t)
17+
{
18+
return
19+
[
20+
new()
21+
{
22+
PropertyName = ".self",
23+
ErrorMessage = "`target` must not be `null`.",
24+
},
25+
];
26+
}
27+
28+
var errors = new List<ValidationError>();
29+
30+
errors.AddRange(global::BaseInterface.Validate(t));
31+
32+
33+
return errors;
34+
}
35+
36+
37+
38+
}
39+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//HintName: IV...BaseClass.g.cs
2+
using System.Collections.Generic;
3+
using Immediate.Validations.Shared;
4+
5+
#nullable enable
6+
#pragma warning disable CS1591
7+
8+
9+
partial class BaseClass
10+
{
11+
static List<ValidationError> IValidationTarget<BaseClass>.Validate(BaseClass? target) =>
12+
Validate(target);
13+
14+
public static List<ValidationError> Validate(BaseClass? target)
15+
{
16+
if (target is not { } t)
17+
{
18+
return
19+
[
20+
new()
21+
{
22+
PropertyName = ".self",
23+
ErrorMessage = "`target` must not be `null`.",
24+
},
25+
];
26+
}
27+
28+
var errors = new List<ValidationError>();
29+
30+
31+
32+
return errors;
33+
}
34+
35+
36+
37+
}
38+

0 commit comments

Comments
 (0)