From 00b8f415c7befe27d70f159902956bfa0f06a457 Mon Sep 17 00:00:00 2001 From: Asaf Madari <100016552+Asafima@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:27:00 +0300 Subject: [PATCH] Add guard clauses for SmartEnum (#533) * Feat(GuardsClauses): Added SmartEnum.GuardClauses.csproj, linked to sln, referenced GuardClauses in Packages.props * Added guard against SmartEnum out of range - referencing SmartEnum and GuardClauses * Feat(GuardClauses for SmartEnum): Remove unused usings after merge with source repo * Feat(GuardClauses): Test against generic exception type * Feat(Guard clauses): Fix documentation, Added unit tests, improved logic for Guard * Updated System.Text.Json due to vulnerability issue, removed todo comment for compilation (left the comment) * Added documentation for static class and method parameters (CS1591) * Added readme file for SmartEnum.GuardClauses nuget package * Added github-actions publish workflow --------- Co-authored-by: Steve Smith --- .github/workflows/publish-guardclauses.yml | 35 +++++ Directory.Packages.props | 6 +- SmartEnum.sln | 14 ++ .../GuardAgainstSmartEnumOutOfRange.cs | 66 +++++++++ src/SmartEnum.GuardClauses/README.md | 119 ++++++++++++++++ .../SmartEnum.GuardClauses.csproj | 32 +++++ src/SmartEnum/SmartFlagEnum.cs | 2 +- .../GuardAgainstSmartEnumOutOfRange.cs | 132 ++++++++++++++++++ .../SmartEnum.GuardClauses.UnitTests.csproj | 29 ++++ .../TestEnums.cs | 27 ++++ 10 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-guardclauses.yml create mode 100644 src/SmartEnum.GuardClauses/GuardAgainstSmartEnumOutOfRange.cs create mode 100644 src/SmartEnum.GuardClauses/README.md create mode 100644 src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj create mode 100644 test/SmartEnum.GuardClauses.UnitTests/GuardAgainstSmartEnumOutOfRange.cs create mode 100644 test/SmartEnum.GuardClauses.UnitTests/SmartEnum.GuardClauses.UnitTests.csproj create mode 100644 test/SmartEnum.GuardClauses.UnitTests/TestEnums.cs diff --git a/.github/workflows/publish-guardclauses.yml b/.github/workflows/publish-guardclauses.yml new file mode 100644 index 00000000..ba9269af --- /dev/null +++ b/.github/workflows/publish-guardclauses.yml @@ -0,0 +1,35 @@ +name: publish SmartEnum.GuardClauses to nuget +on: + workflow_dispatch: + push: + branches: + - main # Your default release branch + paths: + - 'src/SmartEnum.GuardClauses/**' +jobs: + publish: + name: list SmartEnum.GuardClauses on nuget.org + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + + # Required for a specific dotnet version that doesn't come with ubuntu-latest / windows-latest + # Visit bit.ly/2synnZl to see the list of SDKs that are pre-installed with ubuntu-latest / windows-latest + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + + # Publish + - name: publish on version change + uses: alirezanet/publish-nuget@v3.0.0 + with: + PROJECT_FILE_PATH: src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj # Relative to repository root + VERSION_FILE_PATH: Directory.Build.props # Filepath with version info, relative to repository root. Defaults to project file + VERSION_REGEX: (.*)<\/Version> # Regex pattern to extract version info in a capturing group + TAG_COMMIT: true # Flag to enable / disable git tagging + TAG_FORMAT: SmartEnumGuardClauses-v* # Format of the git tag, [*] gets replaced with version + NUGET_KEY: ${{secrets.NUGET_API_KEY}} # nuget.org API key \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index d8b9644c..ee269c45 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ true + @@ -22,14 +23,13 @@ - + - - + \ No newline at end of file diff --git a/SmartEnum.sln b/SmartEnum.sln index f5da9161..d062938a 100644 --- a/SmartEnum.sln +++ b/SmartEnum.sln @@ -68,6 +68,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.Dapper.UnitTests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.Dapper.IntegrationTests", "test\SmartEnum.Dapper.IntegrationTests\SmartEnum.Dapper.IntegrationTests.csproj", "{ACCA93E9-EE80-490C-81A3-824086E4EA2F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartEnum.GuardClauses", "src\SmartEnum.GuardClauses\SmartEnum.GuardClauses.csproj", "{A720F348-2176-4A47-ADC5-CC2664FDA516}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartEnum.GuardClauses.UnitTests", "test\SmartEnum.GuardClauses.UnitTests\SmartEnum.GuardClauses.UnitTests.csproj", "{B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -162,6 +166,14 @@ Global {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACCA93E9-EE80-490C-81A3-824086E4EA2F}.Release|Any CPU.Build.0 = Release|Any CPU + {A720F348-2176-4A47-ADC5-CC2664FDA516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A720F348-2176-4A47-ADC5-CC2664FDA516}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A720F348-2176-4A47-ADC5-CC2664FDA516}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A720F348-2176-4A47-ADC5-CC2664FDA516}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -190,6 +202,8 @@ Global {7E08FCFA-2318-4D36-BAB5-8AFA41F00CC3} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04} {ADBD5097-87A4-492B-9399-6A4CCC53CD5A} = {79268877-BBEF-4DE2-B8D9-697F21933159} {ACCA93E9-EE80-490C-81A3-824086E4EA2F} = {EF5634F4-4667-4481-934C-D1CFA042AD0B} + {A720F348-2176-4A47-ADC5-CC2664FDA516} = {FA199ECB-5F29-442A-AAC6-91DBCB7A5A04} + {B7B944B3-E9DC-4CFC-BADC-11EC2F226AA2} = {79268877-BBEF-4DE2-B8D9-697F21933159} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46896DE3-41B8-442F-A6FB-6AC9F11CCBCE} diff --git a/src/SmartEnum.GuardClauses/GuardAgainstSmartEnumOutOfRange.cs b/src/SmartEnum.GuardClauses/GuardAgainstSmartEnumOutOfRange.cs new file mode 100644 index 00000000..780f4b5c --- /dev/null +++ b/src/SmartEnum.GuardClauses/GuardAgainstSmartEnumOutOfRange.cs @@ -0,0 +1,66 @@ +using Ardalis.GuardClauses; +using System; + +namespace Ardalis.SmartEnum.GuardClauses +{ + /// + /// Provides guard clauses to ensure input values are valid instances of a specified SmartEnum. + /// + public static class GuardAgainstSmartEnumOutOfRange + { + /// + /// Throws a or a custom + /// if is not a valid value. + /// + /// The type of the smart enum. + /// The guard clause interface. + /// The value to check against the smart enum values. + /// Optional. Custom error message to pass to . + /// Optional. A function that creates a custom exception. + /// The valid value . + /// Thrown when + /// is not a valid enum value, and no custom exception is provided. + /// Thrown when a custom exception is provided by . + public static TEnum SmartEnumOutOfRange( + this IGuardClause guardClause, + int input, + string message = null, + Func exceptionCreator = null) + where TEnum : SmartEnum + { + return guardClause.SmartEnumOutOfRange(input, message, exceptionCreator); + } + + /// + /// Throws a or a custom + /// if is not a valid value. + /// + /// The type of the smart enum. + /// The type of the value that the smart enum uses. + /// The guard clause interface. + /// The value to check against the smart enum values. + /// Optional. Custom error message to pass to . + /// Optional. A function that creates a custom exception. + /// The valid enum value corresponding to . + /// Thrown when + /// is not a valid enum value and no custom exception is provided. + /// Thrown when a custom exception + /// is provided by . + public static TEnum SmartEnumOutOfRange( + this IGuardClause guardClause, + TValue input, + string message = null, + Func exceptionCreator = null) + where TEnum : SmartEnum + where TValue : IEquatable, IComparable + { + if (SmartEnum.TryFromValue(input, out TEnum result)) + { + return result; + } + + var exceptionMessage = message ?? $"The value '{input}' is not a valid {typeof(TEnum).Name}."; + throw exceptionCreator?.Invoke() ?? new SmartEnumNotFoundException(exceptionMessage); + } + } +} diff --git a/src/SmartEnum.GuardClauses/README.md b/src/SmartEnum.GuardClauses/README.md new file mode 100644 index 00000000..fad07b59 --- /dev/null +++ b/src/SmartEnum.GuardClauses/README.md @@ -0,0 +1,119 @@ +# Ardalis.SmartEnum.GuardClauses + +Ardalis.SmartEnum.GuardClauses is a NuGet package that provides guard clauses to ensure input values are valid instances of a specified SmartEnum. It helps you to validate that a given value corresponds to a valid SmartEnum value and throws appropriate exceptions if it is not. + +## Installation + +To install the Ardalis.SmartEnum.GuardClauses package, run the following command in the NuGet Package Manager Console: + +```bash +Install-Package Ardalis.SmartEnum.GuardClauses +``` + +Alternatively, you can install it via the .NET CLI: + +```bash +dotnet add package Ardalis.SmartEnum.GuardClauses +``` + +## Usage + +### SmartEnumOutOfRange Method + +The primary method provided by this package is `SmartEnumOutOfRange`, which can be used to validate if an input value is a valid `SmartEnum`. + +### Example Usage + +Here's an example of how to use the `SmartEnumOutOfRange` method: + +```csharp +using Ardalis.GuardClauses; +using Ardalis.SmartEnum; +using Ardalis.SmartEnum.GuardClauses; +using System; + +public class Status : SmartEnum +{ + public static readonly Status Draft = new Status(nameof(Draft), 1); + public static readonly Status Published = new Status(nameof(Published), 2); + public static readonly Status Archived = new Status(nameof(Archived), 3); + + private Status(string name, int value) : base(name, value) { } +} + +public class Example +{ + public void ValidateStatus(int statusValue) + { + // This will throw a SmartEnumNotFoundException if the statusValue is not a valid Status + Status status = Guard.Against.SmartEnumOutOfRange(statusValue); + Console.WriteLine($"Validated status: {status.Name}"); + } +} +``` + +In this example, the `ValidateStatus` method checks if the `statusValue` is a valid `Status` SmartEnum. If the value is invalid, a `SmartEnumNotFoundException` is thrown. + +### Custom Exception Handling + +You can also pass a custom exception creator function to the `SmartEnumOutOfRange` method: + +```csharp +public void ValidateStatusWithCustomException(int statusValue) +{ + try + { + Status status = Guard.Against.SmartEnumOutOfRange(statusValue, exceptionCreator: () => new ArgumentException("Invalid status value provided.")); + Console.WriteLine($"Validated status: {status.Name}"); + } + catch (Exception ex) + { + Console.WriteLine($"Exception caught: {ex.Message}"); + } +} +``` + +In this example, if the `statusValue` is not valid, a custom `ArgumentException` will be thrown instead of the default `SmartEnumNotFoundException`. + +### Supporting Different Value Types + +The package also supports SmartEnums with different value types, such as `string`, `Guid`, etc.: + +```csharp +public class Color : SmartEnum +{ + public static readonly Color Red = new Color(nameof(Red), "FF0000"); + public static readonly Color Green = new Color(nameof(Green), "00FF00"); + public static readonly Color Blue = new Color(nameof(Blue), "0000FF"); + + private Color(string name, string value) : base(name, value) { } +} + +public void ValidateColor(string colorValue) +{ + Color color = Guard.Against.SmartEnumOutOfRange(colorValue); + Console.WriteLine($"Validated color: {color.Name}"); +} +``` + +In this case, the `SmartEnumOutOfRange` method checks if `colorValue` corresponds to a valid `Color` SmartEnum. + +## Additional Information + +For more details on the SmartEnum package and its usage, check out the official [SmartEnum repository](https://github.com/ardalis/SmartEnum/). + +## License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/ardalis/SmartEnum/blob/main/LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please see the [CONTRIBUTING](https://github.com/ardalis/SmartEnum/blob/main/CONTRIBUTING.md) guide for details. + +## Acknowledgements + +Special thanks to [Ardalis](https://github.com/ardalis) for creating the SmartEnum package and to all contributors for their ongoing efforts. + +--- + +This README provides an overview of how to use the Ardalis.SmartEnum.GuardClauses package. Make sure to check out the [SmartEnum repository](https://github.com/ardalis/SmartEnum/) for further examples and documentation. \ No newline at end of file diff --git a/src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj b/src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj new file mode 100644 index 00000000..746deb84 --- /dev/null +++ b/src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj @@ -0,0 +1,32 @@ + + + + Ardalis.SmartEnum.GuardClauses + Ardalis.SmartEnum.GuardClauses + Guard clauses for Ardalis.SmartEnum. + Guard clauses for Ardalis.SmartEnum. + enum;smartenum;ardalis;guard;guardclauses + Added support for guard clauses + icon.png + Ardalis.SmartEnum.GuardClauses + Ardalis.SmartEnum.GuardClauses + README.md + + + + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + + + True + \ + + + + + + + diff --git a/src/SmartEnum/SmartFlagEnum.cs b/src/SmartEnum/SmartFlagEnum.cs index 5448367a..acc3b2e4 100644 --- a/src/SmartEnum/SmartFlagEnum.cs +++ b/src/SmartEnum/SmartFlagEnum.cs @@ -206,7 +206,7 @@ public static IEnumerable FromValue(TValue value) /// public static TEnum DeserializeValue(TValue value) { - //todo we should not be calling get options for each deserialization. Perhaps move it to a lazy field _enumOptions. + // we should not be calling get options for each deserialization. Perhaps move it to a lazy field _enumOptions. var enumList = GetAllOptions(); var returnValue = enumList.FirstOrDefault(x => x.Value.Equals(value)); diff --git a/test/SmartEnum.GuardClauses.UnitTests/GuardAgainstSmartEnumOutOfRange.cs b/test/SmartEnum.GuardClauses.UnitTests/GuardAgainstSmartEnumOutOfRange.cs new file mode 100644 index 00000000..8a70581d --- /dev/null +++ b/test/SmartEnum.GuardClauses.UnitTests/GuardAgainstSmartEnumOutOfRange.cs @@ -0,0 +1,132 @@ +using Ardalis.GuardClauses; +using Ardalis.SmartEnum; +using Ardalis.SmartEnum.GuardClauses; +using FluentAssertions; +using System; +using Xunit; + +namespace SmartEnum.GuardClauses.UnitTests +{ + public class GuardAgainstSmartEnumOutOfRange + { + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void ValidSmartEnumValues_ReturnsSmartEnum(int input) + { + var result = Guard.Against.SmartEnumOutOfRange(input); + + AssertSmartEnumIsValid(input, result); + } + + [Theory] + [InlineData(1.2)] + [InlineData(2.3)] + [InlineData(3.4)] + public void ValidSmartEnumValues_ReturnsSmartEnumDouble(double input) + { + TestEnumDouble result = Guard.Against.SmartEnumOutOfRange(input); + + AssertSmartEnumIsValid(input, result); + } + + [Theory] + [InlineData(999)] + [InlineData(-1)] + [InlineData(0)] + public void InvalidEnumValue_ThrowsSmartEnumNotFoundException(int invalidValue) + { + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange(invalidValue)); + + AssertException(exception, nameof(TestEnum)); + } + + [Theory] + [InlineData(-2.01)] + [InlineData(1.21)] + [InlineData(0.01)] + public void InvalidEnumValue_ThrowsSmartEnumDoubleNotFoundException(double invalidValue) + { + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange(invalidValue)); + + AssertException(exception, nameof(TestEnumDouble)); + } + + [Fact] + public void InvalidEnumValueWithCustomMessage_ThrowsSmartEnumNotFoundExceptionWithCustomMessage() + { + const int invalidValue = 999; + string customMessage = "Custom error message"; + + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange(invalidValue, customMessage)); + + AssertException(exception, customMessage); + } + + [Fact] + public void InvalidEnumValueWithCustomMessage_ThrowsSmartEnumDoubleNotFoundExceptionWithCustomMessage() + { + const double invalidValue = 32.1; + string customMessage = "Custom error message"; + + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange(invalidValue, customMessage)); + + AssertException(exception, customMessage); + } + + [Fact] + public void InvalidEnumValueWithCustomException_ThrowsCustomException() + { + const int invalidValue = 999; + const string Message = "Custom exception"; + + var customException = new InvalidOperationException(Message); + + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange( + invalidValue, + message: "ignored", + exceptionCreator: () => customException)); + + AssertException(exception, Message); + } + + [Fact] + public void InvalidDoubleEnumValueWithCustomException_ThrowsCustomException() + { + const double invalidValue = 32.33; + const string Message = "Custom exception"; + + var customException = new InvalidOperationException(Message); + + var exception = Assert.Throws(() => + Guard.Against.SmartEnumOutOfRange( + invalidValue, + message: "ignored", + exceptionCreator: () => customException)); + + AssertException(exception, Message); + } + + private static void AssertSmartEnumIsValid(TExpected expected, TEnum result) + where TEnum : SmartEnum + where TExpected : IEquatable, IComparable + { + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.Value.Should().Be(expected); + } + + private static void AssertException(Exception exception, string errorMsg) + where TException : Exception + { + exception.Should().BeOfType(); + exception.Message.Should().Contain(errorMsg); + } + } +} diff --git a/test/SmartEnum.GuardClauses.UnitTests/SmartEnum.GuardClauses.UnitTests.csproj b/test/SmartEnum.GuardClauses.UnitTests/SmartEnum.GuardClauses.UnitTests.csproj new file mode 100644 index 00000000..c86e3348 --- /dev/null +++ b/test/SmartEnum.GuardClauses.UnitTests/SmartEnum.GuardClauses.UnitTests.csproj @@ -0,0 +1,29 @@ + + + false + 7.3 + strict + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers + + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/test/SmartEnum.GuardClauses.UnitTests/TestEnums.cs b/test/SmartEnum.GuardClauses.UnitTests/TestEnums.cs new file mode 100644 index 00000000..2f78d663 --- /dev/null +++ b/test/SmartEnum.GuardClauses.UnitTests/TestEnums.cs @@ -0,0 +1,27 @@ +using Ardalis.SmartEnum; + +namespace SmartEnum.GuardClauses.UnitTests +{ + public sealed class TestEnum : SmartEnum + { + public static readonly TestEnum One = new TestEnum(nameof(One), 1); + public static readonly TestEnum Two = new TestEnum(nameof(Two), 2); + public static readonly TestEnum Three = new TestEnum(nameof(Three), 3); + + private TestEnum(string name, int value) : base(name, value) + { + } + } + + + public sealed class TestEnumDouble : SmartEnum + { + public static readonly TestEnumDouble One = new TestEnumDouble("A string!", 1.2); + public static readonly TestEnumDouble Two = new TestEnumDouble("Another string!", 2.3); + public static readonly TestEnumDouble Three = new TestEnumDouble("Yet another string!", 3.4); + + private TestEnumDouble(string name, double value) : base(name, value) + { + } + } +}