Skip to content

Commit

Permalink
Add guard clauses for SmartEnum (#533)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Asafima and ardalis authored Sep 11, 2024
1 parent c5dcd50 commit 00b8f41
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 4 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/publish-guardclauses.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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>(.*)<\/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
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Ardalis.GuardClauses" Version="4.5.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.10" />
<PackageVersion Include="Constant" Version="2.0.4" />
Expand All @@ -22,14 +23,13 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
<PackageVersion Include="protobuf-net" Version="3.2.26" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="Utf8Json" Version="1.3.7" />
<PackageVersion Include="xunit" Version="2.7.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>

<ItemGroup>
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934" PrivateAssets="All" />
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934" PrivateAssets="All" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions SmartEnum.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
66 changes: 66 additions & 0 deletions src/SmartEnum.GuardClauses/GuardAgainstSmartEnumOutOfRange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Ardalis.GuardClauses;
using System;

namespace Ardalis.SmartEnum.GuardClauses
{
/// <summary>
/// Provides guard clauses to ensure input values are valid instances of a specified SmartEnum.
/// </summary>
public static class GuardAgainstSmartEnumOutOfRange
{
/// <summary>
/// Throws a<see cref="SmartEnumNotFoundException" /> or a custom<see cref="Exception" />
/// if <paramref name="input"/> is not a valid <see cref="SmartEnum{TEnum}"/> value.
/// </summary>
/// <typeparam name="TEnum">The type of the smart enum.</typeparam>
/// <param name="guardClause">The guard clause interface.</param>
/// <param name="input">The value to check against the smart enum values.</param>
/// <param name="message">Optional. Custom error message to pass to <see cref="SmartEnumNotFoundException"/>.</param>
/// <param name="exceptionCreator">Optional. A function that creates a custom exception.</param>
/// <returns>The valid <see cref="SmartEnum{TEnum}"/> value <paramref name="input" />.</returns>
/// <exception cref="SmartEnumNotFoundException">Thrown when <paramref name="input" />
/// is not a valid enum value, and no custom exception is provided.</exception>
/// <exception cref="Exception">Thrown when a custom exception is provided by <paramref name="exceptionCreator" />.</exception>
public static TEnum SmartEnumOutOfRange<TEnum>(
this IGuardClause guardClause,
int input,
string message = null,
Func<Exception> exceptionCreator = null)
where TEnum : SmartEnum<TEnum>
{
return guardClause.SmartEnumOutOfRange<TEnum, int>(input, message, exceptionCreator);
}

/// <summary>
/// Throws a <see cref="SmartEnumNotFoundException"/> or a custom <see cref="Exception"/>
/// if <paramref name="input"/> is not a valid <see cref="SmartEnum{TEnum, TValue}"/> value.
/// </summary>
/// <typeparam name="TEnum">The type of the smart enum.</typeparam>
/// <typeparam name="TValue">The type of the value that the smart enum uses.</typeparam>
/// <param name="guardClause">The guard clause interface.</param>
/// <param name="input">The value to check against the smart enum values.</param>
/// <param name="message">Optional. Custom error message to pass to <see cref="SmartEnumNotFoundException"/>.</param>
/// <param name="exceptionCreator">Optional. A function that creates a custom exception.</param>
/// <returns>The valid enum value <typeparamref name="TEnum"/> corresponding to <paramref name="input"/>.</returns>
/// <exception cref="SmartEnumNotFoundException">Thrown when <paramref name="input"/>
/// is not a valid enum value and no custom exception is provided.</exception>
/// <exception cref="Exception">Thrown when a custom exception
/// is provided by <paramref name="exceptionCreator"/>.</exception>
public static TEnum SmartEnumOutOfRange<TEnum, TValue>(
this IGuardClause guardClause,
TValue input,
string message = null,
Func<Exception> exceptionCreator = null)
where TEnum : SmartEnum<TEnum, TValue>
where TValue : IEquatable<TValue>, IComparable<TValue>
{
if (SmartEnum<TEnum, TValue>.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);
}
}
}
119 changes: 119 additions & 0 deletions src/SmartEnum.GuardClauses/README.md
Original file line number Diff line number Diff line change
@@ -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<Status>
{
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<Status>(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<Status>(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<Color, string>
{
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<Color, string>(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.
32 changes: 32 additions & 0 deletions src/SmartEnum.GuardClauses/SmartEnum.GuardClauses.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>Ardalis.SmartEnum.GuardClauses</PackageId>
<Title>Ardalis.SmartEnum.GuardClauses</Title>
<Description>Guard clauses for Ardalis.SmartEnum.</Description>
<Summary>Guard clauses for Ardalis.SmartEnum.</Summary>
<PackageTags>enum;smartenum;ardalis;guard;guardclauses</PackageTags>
<PackageReleaseNotes>Added support for guard clauses</PackageReleaseNotes>
<PackageIcon>icon.png</PackageIcon>
<AssemblyName>Ardalis.SmartEnum.GuardClauses</AssemblyName>
<RootNamespace>Ardalis.SmartEnum.GuardClauses</RootNamespace>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
</ItemGroup>
<ItemGroup>
<None Include="README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="$(SolutionDir)img\icon.png" Pack="true" Visible="false" PackagePath="" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SmartEnum\SmartEnum.csproj" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/SmartEnum/SmartFlagEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public static IEnumerable<TEnum> FromValue(TValue value)
/// <returns></returns>
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));
Expand Down
Loading

0 comments on commit 00b8f41

Please sign in to comment.