diff --git a/src/projects/EnsureThat/Enforcers/EnumArg.cs b/src/projects/EnsureThat/Enforcers/EnumArg.cs index 2df4f1e..8c693f3 100644 --- a/src/projects/EnsureThat/Enforcers/EnumArg.cs +++ b/src/projects/EnsureThat/Enforcers/EnumArg.cs @@ -1,4 +1,5 @@ using System; +using EnsureThat.Internals; using JetBrains.Annotations; namespace EnsureThat.Enforcers @@ -7,14 +8,15 @@ public sealed class EnumArg { /// /// Confirms that the is defined in the enum . - /// 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 , + /// 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 enums. /// /// /// Flags example: /// /// [Flags] - /// enum Abc{ + /// enum Abc { /// A = 1, /// B = 2, /// C = 4, @@ -22,7 +24,7 @@ public sealed class EnumArg /// } /// /// 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 attribute, but the composite is not a named enum value /// public T IsDefined(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum { @@ -37,5 +39,25 @@ public T IsDefined(T value, [InvokerParameterName] string paramName = null, O return value; } + + /// + /// Confirms that the is defined in the enum . + /// Supports attribute. + /// + public T IsDefinedWithFlagsSupport(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum + { + var isEnumDefined = EnumOf.Contains(value); + + if (!isEnumDefined) + { + throw Ensure.ExceptionFactory.ArgumentOutOfRangeException( + string.Format(ExceptionMessages.Enum_IsValidEnum, value, EnumOf.EnumType), + paramName, + value, + optsFn); + } + + return value; + } } } \ No newline at end of file diff --git a/src/projects/EnsureThat/Ensure.cs b/src/projects/EnsureThat/Ensure.cs index 596c107..bc7a45a 100644 --- a/src/projects/EnsureThat/Ensure.cs +++ b/src/projects/EnsureThat/Ensure.cs @@ -22,7 +22,7 @@ public static class Ensure public static BoolArg Bool { get; } = new BoolArg(); /// - /// Ensures for enumerables. + /// Ensures for enums. /// [NotNull] public static EnumArg Enum { get; } = new EnumArg(); diff --git a/src/projects/EnsureThat/EnsureArg.Enums.cs b/src/projects/EnsureThat/EnsureArg.Enums.cs index 6a011f2..4337c7b 100644 --- a/src/projects/EnsureThat/EnsureArg.Enums.cs +++ b/src/projects/EnsureThat/EnsureArg.Enums.cs @@ -8,14 +8,15 @@ public static partial class EnsureArg { /// /// Confirms that the is defined in the enum . - /// 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 , + /// 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 enums. /// /// /// Flags example: /// /// [Flags] - /// enum Abc{ + /// enum Abc { /// A = 1, /// B = 2, /// C = 4, @@ -23,9 +24,16 @@ public static partial class EnsureArg /// } /// /// 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 attribute, but the composite is not a named enum value /// public static T EnumIsDefined(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum => Ensure.Enum.IsDefined(value, paramName, optsFn); + + /// + /// Confirms that the is defined in the enum . + /// Supports attribute. + /// + public static T EnumIsDefinedWithFlagsSupport(T value, [InvokerParameterName] string paramName = null, OptsFn optsFn = null) where T : struct, Enum + => Ensure.Enum.IsDefinedWithFlagsSupport(value, paramName, optsFn); } } \ No newline at end of file diff --git a/src/projects/EnsureThat/EnsureThatEnumExtensions.cs b/src/projects/EnsureThat/EnsureThatEnumExtensions.cs index 47dd2c5..a56df1c 100644 --- a/src/projects/EnsureThat/EnsureThatEnumExtensions.cs +++ b/src/projects/EnsureThat/EnsureThatEnumExtensions.cs @@ -4,14 +4,15 @@ public static class EnsureThatEnumExtensions { /// /// Confirms that the is defined in the enum . - /// 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 , + /// 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 enums. /// /// /// Flags example: /// /// [Flags] - /// enum Abc{ + /// enum Abc { /// A = 1, /// B = 2, /// C = 4, @@ -19,9 +20,16 @@ public static class EnsureThatEnumExtensions /// } /// /// 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 ` attribute, but the composite is not a named enum value /// public static void IsDefined(this in Param param) where T : struct, System.Enum => Ensure.Enum.IsDefined(param.Value, param.Name, param.OptsFn); + + /// + /// Confirms that the is defined in the enum . + /// Supports attribute. + /// + public static void IsDefinedWithFlagsSupport(this in Param param) where T : struct, System.Enum + => Ensure.Enum.IsDefinedWithFlagsSupport(param.Value, param.Name, param.OptsFn); } } \ No newline at end of file diff --git a/src/projects/EnsureThat/Internals/EnumOf.cs b/src/projects/EnsureThat/Internals/EnumOf.cs new file mode 100644 index 0000000..8fba3d6 --- /dev/null +++ b/src/projects/EnsureThat/Internals/EnumOf.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace EnsureThat.Internals +{ + internal static class EnumOf where T : struct, Enum + { + internal static readonly Type EnumType; + private static readonly bool _hasFlags; + private static readonly List _values; + + static EnumOf() + { + EnumType = typeof(T); + + _hasFlags = EnumType.GetTypeInfo().GetCustomAttributes(false).Any(); + + var enumValues = Enum.GetValues(EnumType); + _values = new List(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; + } + } +} \ No newline at end of file diff --git a/src/tests/UnitTests/EnsureEnumParamTests.cs b/src/tests/UnitTests/EnsureEnumParamTests.cs index 5fcc00a..909c65d 100644 --- a/src/tests/UnitTests/EnsureEnumParamTests.cs +++ b/src/tests/UnitTests/EnsureEnumParamTests.cs @@ -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( + 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( + 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), @@ -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( - 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( + 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( + 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( + 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 + } } } \ No newline at end of file