Skip to content

Commit

Permalink
Adds Flags support vi Enum.IsDefinedWithFlagsSupport
Browse files Browse the repository at this point in the history
Adds support for flags when verifying enums.
  • Loading branch information
nevaldas authored Mar 3, 2020
1 parent f2c31c3 commit d421a93
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 18 deletions.
30 changes: 26 additions & 4 deletions src/projects/EnsureThat/Enforcers/EnumArg.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using EnsureThat.Internals;
using JetBrains.Annotations;

namespace EnsureThat.Enforcers
Expand All @@ -7,22 +8,23 @@ public sealed class EnumArg
{
/// <summary>
/// Confirms that the <paramref name="value"/> is defined in the enum <typeparamref name="T"/>.
/// Note that just like `Enum.IsDefined`, `Flags` based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with `Flags` enums.
/// Note that just like <see cref="Enum.IsDefined(Type, object)"/>,
/// <see cref="FlagsAttribute"/> based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with <see cref="FlagsAttribute"/> enums.
/// </summary>
/// <example>
/// Flags example:
///
/// [Flags]
/// enum Abc{
/// enum Abc {
/// A = 1,
/// B = 2,
/// C = 4,
/// AB = 3
/// }
///
/// Abc.A | Abc.B IsDefined=true (due to Abc.AB)
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to `Flags` attribute, but the composite is not a named enum value
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to <see cref="FlagsAttribute"/> attribute, but the composite is not a named enum value
/// </example>
public T IsDefined<T>(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum
{
Expand All @@ -37,5 +39,25 @@ public T IsDefined<T>(T value, [InvokerParameterName] string paramName = null, O

return value;
}

/// <summary>
/// Confirms that the <paramref name="value"/> is defined in the enum <typeparamref name="T"/>.
/// Supports <see cref="FlagsAttribute"/> attribute.
/// </summary>
public T IsDefinedWithFlagsSupport<T>(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum
{
var isEnumDefined = EnumOf<T>.Contains(value);

if (!isEnumDefined)
{
throw Ensure.ExceptionFactory.ArgumentOutOfRangeException(
string.Format(ExceptionMessages.Enum_IsValidEnum, value, EnumOf<T>.EnumType),
paramName,
value,
optsFn);
}

return value;
}
}
}
2 changes: 1 addition & 1 deletion src/projects/EnsureThat/Ensure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class Ensure
public static BoolArg Bool { get; } = new BoolArg();

/// <summary>
/// Ensures for enumerables.
/// Ensures for enums.
/// </summary>
[NotNull]
public static EnumArg Enum { get; } = new EnumArg();
Expand Down
16 changes: 12 additions & 4 deletions src/projects/EnsureThat/EnsureArg.Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@ public static partial class EnsureArg
{
/// <summary>
/// Confirms that the <paramref name="value"/> is defined in the enum <typeparamref name="T"/>.
/// Note that just like `Enum.IsDefined`, `Flags` based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with `Flags` enums.
/// Note that just like <see cref="Enum.IsDefined(Type, object)"/>,
/// <see cref="FlagsAttribute"/> based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with <see cref="FlagsAttribute"/> enums.
/// </summary>
/// <example>
/// Flags example:
///
/// [Flags]
/// enum Abc{
/// enum Abc {
/// A = 1,
/// B = 2,
/// C = 4,
/// AB = 3
/// }
///
/// Abc.A | Abc.B IsDefined=true (due to Abc.AB)
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to `Flags` attribute, but the composite is not a named enum value
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to <see cref="FlagsAttribute"/> attribute, but the composite is not a named enum value
/// </example>
public static T EnumIsDefined<T>(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum
=> Ensure.Enum.IsDefined(value, paramName, optsFn);

/// <summary>
/// Confirms that the <paramref name="value"/> is defined in the enum <typeparamref name="T"/>.
/// Supports <see cref="FlagsAttribute"/> attribute.
/// </summary>
public static T EnumIsDefinedWithFlagsSupport<T>(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum
=> Ensure.Enum.IsDefinedWithFlagsSupport(value, paramName, optsFn);
}
}
16 changes: 12 additions & 4 deletions src/projects/EnsureThat/EnsureThatEnumExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@ public static class EnsureThatEnumExtensions
{
/// <summary>
/// Confirms that the <paramref name="param.Value"/> is defined in the enum <typeparamref name="T"/>.
/// Note that just like `Enum.IsDefined`, `Flags` based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with `Flags` enums.
/// Note that just like <see cref="System.Enum.IsDefined(System.Type, object)"/>,
/// <see cref="System.FlagsAttribute"/> based enums may be valid combination of defined values, but if the combined value
/// itself is not named an error will be raised. Avoid usage with <see cref="System.FlagsAttribute"/> enums.
/// </summary>
/// <example>
/// Flags example:
///
/// [Flags]
/// enum Abc{
/// enum Abc {
/// A = 1,
/// B = 2,
/// C = 4,
/// AB = 3
/// }
///
/// Abc.A | Abc.B IsDefined=true (due to Abc.AB)
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to `Flags` attribute, but the composite is not a named enum value
/// Abc.A | Abc.C IsDefined=false (A and C are both valid, the composite is valid due to `<see cref="System.FlagsAttribute"/> attribute, but the composite is not a named enum value
/// </example>
public static void IsDefined<T>(this in Param<T> param) where T : struct, System.Enum
=> Ensure.Enum.IsDefined(param.Value, param.Name, param.OptsFn);

/// <summary>
/// Confirms that the <paramref name="param.Value"/> is defined in the enum <typeparamref name="T"/>.
/// Supports <see cref="System.FlagsAttribute"/> attribute.
/// </summary>
public static void IsDefinedWithFlagsSupport<T>(this in Param<T> param) where T : struct, System.Enum
=> Ensure.Enum.IsDefinedWithFlagsSupport(param.Value, param.Name, param.OptsFn);
}
}
46 changes: 46 additions & 0 deletions src/projects/EnsureThat/Internals/EnumOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace EnsureThat.Internals
{
internal static class EnumOf<T> where T : struct, Enum
{
internal static readonly Type EnumType;
private static readonly bool _hasFlags;
private static readonly List<ulong> _values;

static EnumOf()
{
EnumType = typeof(T);

_hasFlags = EnumType.GetTypeInfo().GetCustomAttributes<FlagsAttribute>(false).Any();

var enumValues = Enum.GetValues(EnumType);
_values = new List<ulong>(enumValues.Length);
foreach (var v in enumValues)
_values.Add(Convert.ToUInt64(v));
}

internal static bool Contains(T value)
{
if (!_hasFlags)
return Enum.IsDefined(EnumType, value);

var raw = Convert.ToUInt64(value);
if (raw == 0)
return Enum.IsDefined(EnumType, value);

ulong sum = 0;

foreach (var val in _values)
{
if ((raw & val) == val)
sum |= val;
}

return sum == raw;
}
}
}
131 changes: 126 additions & 5 deletions src/tests/UnitTests/EnsureEnumParamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,53 @@ public class EnsureEnumParamTests : UnitTestBase
[Fact]
public void IsDefined_ShouldNotThrow()
{
var item = Only1IsValid.Valid;
var item = Only1IsValidEnum.Valid;

ShouldNotThrow(
() => Ensure.Enum.IsDefined(item, ParamName),
() => EnsureArg.EnumIsDefined(item, ParamName),
() => Ensure.That(item, ParamName).IsDefined());
}

[Theory]
[InlineData((Only1IsValidEnum)2)]
[InlineData((Only1IsValidEnum)0)]
public void NotDefined_ShouldThrow(Only1IsValidEnum item)
{
ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(Only1IsValidEnum)),
() => Ensure.Enum.IsDefined(item, ParamName),
() => EnsureArg.EnumIsDefined(item, ParamName),
() => Ensure.That(item, ParamName).IsDefined());
}

[Fact]
public void IsDefinedWithFlagsSupport_ShouldNotThrow()
{
var item = Only1IsValidEnum.Valid;

ShouldNotThrow(
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

[Theory]
[InlineData((Only1IsValidEnum)2)]
[InlineData((Only1IsValidEnum)0)]
public void NotDefined_Extended_ShouldThrow(Only1IsValidEnum item)
{
ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(Only1IsValidEnum)),
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

[Fact]
public void FlagIsDefined_ShouldNotThrow_IfNotCombined()
{
var item = TestFlagsEnum.Bar;

ShouldNotThrow(
() => Ensure.Enum.IsDefined(item, ParamName),
Expand All @@ -18,20 +64,95 @@ public void IsDefined_ShouldNotThrow()
}

[Fact]
public void IsNotDefined_ShouldThrow()
public void FlagIsDefined_ShouldThrow_IfCombined()
{
var item = (Only1IsValid)2;
var item = TestFlagsEnum.Bar | TestFlagsEnum.Baz;

ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(Only1IsValid)),
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(TestFlagsEnum)),
() => Ensure.Enum.IsDefined(item, ParamName),
() => EnsureArg.EnumIsDefined(item, ParamName),
() => Ensure.That(item, ParamName).IsDefined());
}

private enum Only1IsValid
[Fact]
public void FlagNotDefined_ShouldThrow()
{
var item = (TestFlagsEnum)3;
ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(TestFlagsEnum)),
() => Ensure.Enum.IsDefined(item, ParamName),
() => EnsureArg.EnumIsDefined(item, ParamName),
() => Ensure.That(item, ParamName).IsDefined());
}

[Theory]
[InlineData(TestFlagsEnum.Bar)]
[InlineData(TestFlagsEnum.Bar | TestFlagsEnum.Baz)]
public void FlagIsDefined_Extended_ShouldNotThrow(TestFlagsEnum item)
{
ShouldNotThrow(
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

[Theory]
[InlineData((TestFlagsEnum)4)]
[InlineData((TestFlagsEnum)0)]
public void FlagNotDefined_Extended_ShouldThrow(TestFlagsEnum item)
{
ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(TestFlagsEnum)),
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

[Theory]
[InlineData(TestFlagsOfWhateverPower.A)]
[InlineData(TestFlagsOfWhateverPower.B)]
[InlineData(TestFlagsOfWhateverPower.C)]
[InlineData(TestFlagsOfWhateverPower.A | TestFlagsOfWhateverPower.C)]
[InlineData(TestFlagsOfWhateverPower.B | TestFlagsOfWhateverPower.C)]
public void FlagOfWhateverPowerIsDefined_ShouldNotThrow(TestFlagsOfWhateverPower item)
{
ShouldNotThrow(
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

[Fact]
public void FlagOfWhateverPowerIsNotDefined_ShouldThrow()
{
var item = (TestFlagsOfWhateverPower)9;

ShouldThrow<ArgumentOutOfRangeException>(
string.Format(ExceptionMessages.Enum_IsValidEnum, item, typeof(TestFlagsOfWhateverPower)),
() => Ensure.Enum.IsDefinedWithFlagsSupport(item, ParamName),
() => EnsureArg.EnumIsDefinedWithFlagsSupport(item, ParamName),
() => Ensure.That(item, ParamName).IsDefinedWithFlagsSupport());
}

public enum Only1IsValidEnum : byte
{
Valid = 1
}

[Flags]
public enum TestFlagsEnum : byte
{
Bar = 1,
Baz = 1 << 1
}

[Flags]
public enum TestFlagsOfWhateverPower : byte
{
A = 4,
B = 5,
C = 8
}
}
}

0 comments on commit d421a93

Please sign in to comment.