Skip to content

Commit f67beda

Browse files
vbreussvbtig
andauthored
feat: add "Implements" filter and requirement (#44)
Add separate requirement and filter for types that "Implement" an interface. --------- Co-authored-by: Valentin Breuß <[email protected]>
1 parent e36eb2d commit f67beda

File tree

8 files changed

+509
-30
lines changed

8 files changed

+509
-30
lines changed

Source/Testably.Architecture.Rules/Extensions/TypeExtensions.cs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,45 @@ public static bool HasMethodWithAttribute<TAttribute>(
6868
a => predicate(a, method), inherit));
6969
}
7070

71+
/// <summary>
72+
/// Determines whether the current <see cref="Type" /> implements the <paramref name="interfaceType" />.
73+
/// </summary>
74+
/// <param name="type">The <see cref="Type" />.</param>
75+
/// <param name="interfaceType">The interface <see cref="Type" />.</param>
76+
/// <param name="forceDirect">
77+
/// If set to <see langword="false" /> (default value), the <paramref name="interfaceType" />
78+
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
79+
/// <paramref name="interfaceType" /> to be directly implemented in the <paramref name="type"/>.
80+
/// </param>
81+
public static bool Implements(
82+
this Type type,
83+
Type interfaceType,
84+
bool forceDirect = false)
85+
{
86+
if (!interfaceType.IsInterface)
87+
{
88+
return false;
89+
}
90+
91+
Type[] interfaces = type.GetInterfaces();
92+
if (forceDirect && type.BaseType != null)
93+
{
94+
interfaces = interfaces
95+
.Except(type.BaseType.GetInterfaces())
96+
.ToArray();
97+
}
98+
99+
return interfaces
100+
.Any(childInterface =>
101+
{
102+
Type currentInterface = childInterface.IsGenericType
103+
? childInterface.GetGenericTypeDefinition()
104+
: childInterface;
105+
106+
return currentInterface == interfaceType;
107+
});
108+
}
109+
71110
/// <summary>
72111
/// Determines whether the current <see cref="Type" /> inherits from the <paramref name="parentType" />.
73112
/// </summary>
@@ -78,8 +117,7 @@ public static bool HasMethodWithAttribute<TAttribute>(
78117
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
79118
/// <paramref name="parentType" /> to be the direct parent.
80119
/// </param>
81-
/// <returns></returns>
82-
public static bool Inherits(
120+
public static bool InheritsFrom(
83121
this Type type,
84122
Type parentType,
85123
bool forceDirect = false)
@@ -111,7 +149,7 @@ public static bool Inherits(
111149
return false;
112150
}
113151

114-
if (currentType.ImplementsInterface(parentType, forceDirect))
152+
if (currentType.Implements(parentType, forceDirect))
115153
{
116154
return true;
117155
}
@@ -137,25 +175,5 @@ public static bool Inherits(
137175
public static bool IsStatic(this Type type)
138176
=> type.IsAbstract && type.IsSealed && !type.IsInterface &&
139177
!type.GetConstructors().Any(m => m.IsPublic);
140-
141-
internal static bool ImplementsInterface(this Type type, Type interfaceType, bool forceDirect)
142-
{
143-
Type[] interfaces = type.GetInterfaces();
144-
if (forceDirect && type.BaseType != null)
145-
{
146-
interfaces = interfaces
147-
.Except(type.BaseType.GetInterfaces())
148-
.ToArray();
149-
}
150-
151-
return interfaces
152-
.Any(childInterface =>
153-
{
154-
Type currentInterface = childInterface.IsGenericType
155-
? childInterface.GetGenericTypeDefinition()
156-
: childInterface;
157-
158-
return currentInterface == interfaceType;
159-
});
160-
}
178+
161179
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
3+
namespace Testably.Architecture.Rules;
4+
5+
public static partial class FilterOnTypeExtensions
6+
{
7+
/// <summary>
8+
/// Filter for types that implement the interface <typeparamref name="TInterface" />.
9+
/// </summary>
10+
public static ITypeFilterResult WhichImplement<TInterface>(this ITypeFilter @this,
11+
bool forceDirect = false)
12+
{
13+
return @this.WhichImplement(typeof(TInterface), forceDirect);
14+
}
15+
16+
/// <summary>
17+
/// Filter for types that implement the interface <paramref name="interfaceType" />.
18+
/// </summary>
19+
public static ITypeFilterResult WhichImplement(this ITypeFilter @this,
20+
Type interfaceType,
21+
bool forceDirect = false)
22+
{
23+
return @this.Which(type => type.Implements(interfaceType, forceDirect));
24+
}
25+
26+
/// <summary>
27+
/// Filter for types that don't implement the interface <typeparamref name="TInterface" />.
28+
/// </summary>
29+
public static ITypeFilterResult WhichDoNotImplement<TInterface>(this ITypeFilter @this,
30+
bool forceDirect = false)
31+
{
32+
return @this.WhichDoNotImplement(typeof(TInterface), forceDirect);
33+
}
34+
35+
/// <summary>
36+
/// Filter for types that don't implement the interface <paramref name="interfaceType" />.
37+
/// </summary>
38+
public static ITypeFilterResult WhichDoNotImplement(this ITypeFilter @this,
39+
Type interfaceType,
40+
bool forceDirect = false)
41+
{
42+
return @this.Which(type => !type.Implements(interfaceType, forceDirect));
43+
}
44+
}

Source/Testably.Architecture.Rules/Filters/FilterOnTypeExtensions.InheritFrom.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static ITypeFilterResult WhichInheritFrom<TBase>(this ITypeFilter @this,
1919
public static ITypeFilterResult WhichInheritFrom(this ITypeFilter @this, Type baseType,
2020
bool forceDirect = false)
2121
{
22-
return @this.Which(type => type.Inherits(baseType, forceDirect));
22+
return @this.Which(type => type.InheritsFrom(baseType, forceDirect));
2323
}
2424

2525
/// <summary>
@@ -37,6 +37,6 @@ public static ITypeFilterResult WhichDoNotInheritFrom<TBase>(this ITypeFilter @t
3737
public static ITypeFilterResult WhichDoNotInheritFrom(this ITypeFilter @this, Type baseType,
3838
bool forceDirect = false)
3939
{
40-
return @this.Which(type => !type.Inherits(baseType, forceDirect));
40+
return @this.Which(type => !type.InheritsFrom(baseType, forceDirect));
4141
}
4242
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
3+
namespace Testably.Architecture.Rules;
4+
5+
public static partial class RequirementOnTypeExtensions
6+
{
7+
/// <summary>
8+
/// Expect the types to implement the <typeparamref name="TInterface" />.
9+
/// </summary>
10+
/// <typeparamref name="TInterface">The type of the interface which should be implemented.</typeparamref>
11+
/// <param name="this">The <see cref="IRequirement{Type}" />.</param>
12+
/// <param name="forceDirect">
13+
/// If set to <see langword="false" /> (default value), the <typeparamref name="TInterface" />
14+
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
15+
/// <typeparamref name="TInterface" /> to be directly implemented.
16+
/// </param>
17+
public static IRequirementResult<Type> ShouldImplement<TInterface>(
18+
this IRequirement<Type> @this,
19+
bool forceDirect = false)
20+
=> @this.ShouldImplement(typeof(TInterface), forceDirect);
21+
22+
/// <summary>
23+
/// Expect the types to implement the <paramref name="interfaceType" />.
24+
/// </summary>
25+
/// <param name="this">The <see cref="IRequirement{Type}" />.</param>
26+
/// <param name="interfaceType">The interface which should be implemented.</param>
27+
/// <param name="forceDirect">
28+
/// If set to <see langword="false" /> (default value), the <paramref name="interfaceType" />
29+
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
30+
/// <paramref name="interfaceType" /> to be directly implemented.
31+
/// </param>
32+
public static IRequirementResult<Type> ShouldImplement(
33+
this IRequirement<Type> @this,
34+
Type interfaceType,
35+
bool forceDirect = false)
36+
=> @this.ShouldSatisfy(Requirement.ForType(
37+
type => type.Implements(interfaceType, forceDirect),
38+
type => new TypeTestError(type,
39+
$"Type '{type.Name}' should{(forceDirect ? " directly" : "")} implement '{interfaceType.Name}'.")));
40+
41+
/// <summary>
42+
/// Expect the types to not implement the <typeparamref name="TInterface" />.
43+
/// </summary>
44+
/// <typeparamref name="TInterface">The type of the interface which should be implemented.</typeparamref>
45+
/// <param name="this">The <see cref="IRequirement{Type}" />.</param>
46+
/// <param name="forceDirect">
47+
/// If set to <see langword="false" /> (default value), the <typeparamref name="TInterface" />
48+
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
49+
/// <typeparamref name="TInterface" /> to be directly implemented.
50+
/// </param>
51+
public static IRequirementResult<Type> ShouldNotImplement<TInterface>(
52+
this IRequirement<Type> @this,
53+
bool forceDirect = false)
54+
=> @this.ShouldNotImplement(typeof(TInterface), forceDirect);
55+
56+
/// <summary>
57+
/// Expect the types to not implement the <paramref name="interfaceType" />.
58+
/// </summary>
59+
/// <param name="this">The <see cref="IRequirement{Type}" />.</param>
60+
/// <param name="interfaceType">The interface which should be implemented.</param>
61+
/// <param name="forceDirect">
62+
/// If set to <see langword="false" /> (default value), the <paramref name="interfaceType" />
63+
/// can be anywhere in the inheritance tree, otherwise if set to <see langword="true" /> requires the
64+
/// <paramref name="interfaceType" /> to be directly implemented.
65+
/// </param>
66+
public static IRequirementResult<Type> ShouldNotImplement(
67+
this IRequirement<Type> @this,
68+
Type interfaceType,
69+
bool forceDirect = false)
70+
=> @this.ShouldSatisfy(Requirement.ForType(
71+
type => !type.Implements(interfaceType, forceDirect),
72+
type => new TypeTestError(type,
73+
$"Type '{type.Name}' should not{(forceDirect ? " directly" : "")} implement '{interfaceType.Name}'.")));
74+
}

Source/Testably.Architecture.Rules/Requirements/RequirementOnTypeExtensions.InheritFrom.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static IRequirementResult<Type> ShouldInheritFrom(
3333
Type baseType,
3434
bool forceDirect = false)
3535
=> @this.ShouldSatisfy(Requirement.ForType(
36-
type => type.Inherits(baseType, forceDirect),
36+
type => type.InheritsFrom(baseType, forceDirect),
3737
type => new TypeTestError(type,
3838
$"Type '{type.Name}' should{(forceDirect ? " directly" : "")} inherit from '{baseType.Name}'.")));
3939

@@ -66,7 +66,7 @@ public static IRequirementResult<Type> ShouldNotInheritFrom(
6666
Type baseType,
6767
bool forceDirect = false)
6868
=> @this.ShouldSatisfy(Requirement.ForType(
69-
type => !type.Inherits(baseType, forceDirect),
69+
type => !type.InheritsFrom(baseType, forceDirect),
7070
type => new TypeTestError(type,
7171
$"Type '{type.Name}' should not{(forceDirect ? " directly" : "")} inherit from '{baseType.Name}'.")));
7272
}

Tests/Testably.Architecture.Rules.Tests/Extensions/TypeExtensionsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ public void HasMethodWithAttribute_WithPredicate_ShouldReturnPredicateResult()
8585
[Theory]
8686
[InlineData(false)]
8787
[InlineData(true)]
88-
public void ImplementsInterface_Object_ShouldReturnFalse(bool forceDirect)
88+
public void Implements_Object_ShouldReturnFalse(bool forceDirect)
8989
{
9090
Type sut = typeof(object);
9191

92-
bool result = sut.ImplementsInterface(typeof(IFooInterface), forceDirect);
92+
bool result = sut.Implements(typeof(IFooInterface), forceDirect);
9393

9494
result.Should().BeFalse();
9595
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using FluentAssertions;
2+
using Testably.Architecture.Rules.Tests.TestHelpers;
3+
using Xunit;
4+
5+
namespace Testably.Architecture.Rules.Tests.Filters;
6+
7+
public sealed partial class FilterOnTypeExtensionsTests
8+
{
9+
public sealed class ImplementTests
10+
{
11+
[Theory]
12+
[InlineData(false)]
13+
[InlineData(true)]
14+
public void WhichImplement_WithClass_ShouldReturnFalse(bool forceDirect)
15+
{
16+
ITestResult result = Expect.That.Types
17+
.WhichImplement<FooImplementor1>(forceDirect)
18+
.ShouldAlwaysFail()
19+
.AllowEmpty()
20+
.Check.InAllLoadedAssemblies();
21+
22+
result.ShouldNotBeViolated();
23+
}
24+
25+
[Fact]
26+
public void
27+
WhichImplement_WithInterface_WithForceDirect_ShouldReturnTypesDirectlyImplementingTheInterface()
28+
{
29+
ITestResult result = Expect.That.Types
30+
.WhichImplement<IFooInterface>(true)
31+
.ShouldAlwaysFail()
32+
.Check.InAllLoadedAssemblies();
33+
34+
result.Errors.Length.Should().Be(2);
35+
result.Errors.Should()
36+
.Contain(e => e.ToString().Contains(nameof(FooImplementor1)));
37+
result.Errors.Should()
38+
.NotContain(e => e.ToString().Contains(nameof(FooImplementor2)));
39+
result.Errors.Should()
40+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name));
41+
result.Errors.Should()
42+
.NotContain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name));
43+
}
44+
45+
[Fact]
46+
public void
47+
WhichImplement_WithInterface_WithoutForceDirect_ShouldReturnAllTypesImplementingInterface()
48+
{
49+
ITestResult result = Expect.That.Types
50+
.WhichImplement<IFooInterface>()
51+
.ShouldAlwaysFail()
52+
.Check.InAllLoadedAssemblies();
53+
54+
result.Errors.Length.Should().Be(4);
55+
result.Errors.Should()
56+
.Contain(e => e.ToString().Contains(nameof(FooImplementor1)));
57+
result.Errors.Should()
58+
.Contain(e => e.ToString().Contains(nameof(FooImplementor2)));
59+
result.Errors.Should()
60+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name));
61+
result.Errors.Should()
62+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name));
63+
}
64+
65+
[Fact]
66+
public void
67+
WhichImplement_WithGenericInterface_WithForceDirect_ShouldReturnTypesDirectlyImplementingTheInterface()
68+
{
69+
ITestResult result = Expect.That.Types
70+
.WhichImplement(typeof(IGenericFooInterface<>), true)
71+
.ShouldAlwaysFail()
72+
.Check.InAllLoadedAssemblies();
73+
74+
result.Errors.Length.Should().Be(1);
75+
result.Errors.Should()
76+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name));
77+
result.Errors.Should()
78+
.NotContain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name));
79+
}
80+
81+
[Fact]
82+
public void
83+
WhichImplement_WithGenericInterface_WithoutForceDirect_ShouldReturnAllTypesImplementingInterface()
84+
{
85+
ITestResult result = Expect.That.Types
86+
.WhichImplement(typeof(IGenericFooInterface<>))
87+
.ShouldAlwaysFail()
88+
.Check.InAllLoadedAssemblies();
89+
90+
result.Errors.Length.Should().Be(2);
91+
result.Errors.Should()
92+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor1<>).Name));
93+
result.Errors.Should()
94+
.Contain(e => e.ToString().Contains(typeof(FooGenericImplementor2<>).Name));
95+
}
96+
97+
private class FooGenericImplementor1<T> : IGenericFooInterface<T>,
98+
IFooInterface
99+
{
100+
}
101+
102+
private class FooGenericImplementor2<T> : FooGenericImplementor1<T>
103+
{
104+
}
105+
106+
private class FooImplementor1 : IFooInterface
107+
{
108+
}
109+
110+
private class FooImplementor2 : FooImplementor1
111+
{
112+
}
113+
114+
private interface IFooInterface
115+
{
116+
}
117+
118+
// ReSharper disable once UnusedTypeParameter
119+
private interface IGenericFooInterface<T>
120+
{
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)