-
-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add SmartEnumNameAttribute, a DataAnnotations ValidationAttribute (#447)
* Add SmartEnum Validation attribute, with tests. * Rename GetValidSmartEnumValues to GetValidSmartEnumNames * Clarify tests. * Readme notes (to be removed). * Rename to SmartEnumNameAttribute * Additional tests * Flesh out README entry for SmartEnumNameAttribute * Remove ReSharper comments. * Fix variable name. * Update test class to be more clear. * Fix my name :) * Update Contributing documentation to reference SmartEnum instead of GuardClauses * Fix typo * Variable renames --------- Co-authored-by: Steve Smith <steve@kentsmiths.com>
- Loading branch information
Showing
4 changed files
with
292 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace Ardalis.SmartEnum | ||
{ | ||
/// <summary> | ||
/// A <see cref="ValidationAttribute"/> that ensures the provided value matches the | ||
/// <see cref="SmartEnum{TEnum}.Name"/> of a <see cref="SmartEnum{TEnum}"/>/<see cref="SmartEnum{TEnum,TValue}"/>. | ||
/// Nulls and non-<see cref="string"/> values are considered valid | ||
/// (add <see cref="RequiredAttribute"/> if you want the field to be required). | ||
/// </summary> | ||
public class SmartEnumNameAttribute : ValidationAttribute | ||
{ | ||
private readonly bool _allowCaseInsensitiveMatch; | ||
private readonly Type _smartEnumType; | ||
|
||
/// <param name="smartEnumType">The expected SmartEnum type.</param> | ||
/// <param name="propertyName">The name of the property that the attribute is being used on.</param> | ||
/// <param name="allowCaseInsensitiveMatch"> | ||
/// Unless this is true, only exact case matching the | ||
/// <see cref="SmartEnum{TEnum}" /> Name will validate. | ||
/// </param> | ||
/// <param name="errorMessage"> | ||
/// Message template to show when validation fails. {0} is <paramref name="propertyName" /> and | ||
/// {1} is the comma-separated list of SmartEnum names. | ||
/// </param> | ||
/// <exception cref="ArgumentNullException">When any of the constructor parameters are null.</exception> | ||
/// <exception cref="InvalidOperationException"> | ||
/// When <paramref name="smartEnumType" /> is not a | ||
/// <see cref="SmartEnum{TEnum}" /> or <see cref="SmartEnum{TEnum,TValue}" /> | ||
/// </exception> | ||
public SmartEnumNameAttribute( | ||
Type smartEnumType, | ||
[CallerMemberName] string propertyName = null, | ||
bool allowCaseInsensitiveMatch = false, | ||
string errorMessage = "{0} must be one of: {1}" | ||
) | ||
{ | ||
if (smartEnumType is null) throw new ArgumentNullException(nameof(smartEnumType)); | ||
if (propertyName is null) throw new ArgumentNullException(nameof(propertyName)); | ||
if (errorMessage is null) throw new ArgumentNullException(nameof(errorMessage)); | ||
List<string> smartEnumBaseTypes = new() { typeof(SmartEnum<>).Name, typeof(SmartEnum<,>).Name }; | ||
if (smartEnumType.BaseType == null || !smartEnumBaseTypes.Contains(smartEnumType.BaseType.Name)) | ||
throw new InvalidOperationException($"{nameof(smartEnumType)} must be a SmartEnum."); | ||
_smartEnumType = smartEnumType; | ||
_allowCaseInsensitiveMatch = allowCaseInsensitiveMatch; | ||
ErrorMessage = string.Format(errorMessage, propertyName, string.Join(", ", GetValidSmartEnumNames())); | ||
} | ||
|
||
public override bool IsValid(object value) | ||
{ | ||
if (value is not string name) return true; | ||
|
||
return _allowCaseInsensitiveMatch | ||
? GetValidSmartEnumNames().Contains(name, StringComparer.InvariantCultureIgnoreCase) | ||
: GetValidSmartEnumNames().Contains(name); | ||
} | ||
|
||
private List<string> GetValidSmartEnumNames() | ||
{ | ||
List<string> validNames = new(); | ||
var typeWithList = _smartEnumType.BaseType!.Name == typeof(SmartEnum<>).Name | ||
? _smartEnumType.BaseType.BaseType! | ||
: _smartEnumType.BaseType!; | ||
var listProp = typeWithList.GetProperty("List", BindingFlags.Public | BindingFlags.Static); | ||
var rawValue = listProp!.GetValue(null); | ||
foreach (var val in (IEnumerable)rawValue!) | ||
{ | ||
var namePropInfo = val.GetType().GetProperty("Name", BindingFlags.Public | BindingFlags.Instance); | ||
var value = namePropInfo!.GetValue(val); | ||
if (value is string name) validNames.Add(name); | ||
} | ||
return validNames; | ||
} | ||
} | ||
} |
177 changes: 177 additions & 0 deletions
177
test/SmartEnum.UnitTests/SmartEnumNameAttributeTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.Linq; | ||
using FluentAssertions; | ||
using FluentAssertions.Execution; | ||
using Xunit; | ||
|
||
namespace Ardalis.SmartEnum.UnitTests | ||
{ | ||
public class SmartEnumNameAttributeTests | ||
{ | ||
[Fact] | ||
public void ThrowsWhenCtorGetsNullType() | ||
{ | ||
Action ctorCall = () => new SmartEnumNameAttribute(null); | ||
|
||
ctorCall.Should().ThrowExactly<ArgumentNullException>(); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsWhenCtorGetsNullPropertyName() | ||
{ | ||
Action ctorCall = () => new SmartEnumNameAttribute(typeof(TestSmartEnum), propertyName: null, errorMessage: "Some Error Message"); | ||
|
||
ctorCall.Should().ThrowExactly<ArgumentNullException>(); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsWhenCtorGetsNullErrorMessage() | ||
{ | ||
Action ctorCall = () => new SmartEnumNameAttribute(typeof(TestSmartEnum), errorMessage: null); | ||
|
||
ctorCall.Should().ThrowExactly<ArgumentNullException>(); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsWhenCtorGetsNonSmartEnumType() | ||
{ | ||
Action ctorCall = () => new SmartEnumNameAttribute(typeof(SmartEnumNameAttributeTests)); | ||
|
||
ctorCall.Should().ThrowExactly<InvalidOperationException>(); | ||
} | ||
|
||
[Fact] | ||
public void DoesNotThrowWhenCtorForSmartEnumWithDifferentKeyType() | ||
{ | ||
Action ctorCall = () => new SmartEnumNameAttribute(typeof(TestSmartEnumWithStringKeyType)); | ||
|
||
ctorCall.Should().NotThrow(); | ||
} | ||
|
||
[Fact] | ||
public void ReturnsErrorMessageContainingPropertyNameAndAllPossibleSmartEnumValues() | ||
{ | ||
var model = new TestValidationModel { SomeProp = "foo" }; | ||
var validationContext = new ValidationContext(model, null, null); | ||
List<ValidationResult> validationResults = new List<ValidationResult>(); | ||
|
||
Validator.TryValidateObject(model, validationContext, validationResults, true); | ||
|
||
using (new AssertionScope()) | ||
{ | ||
validationResults.Should().HaveCount(1); | ||
string errorMessage = validationResults.Single().ErrorMessage; | ||
errorMessage.Should().Contain(nameof(TestValidationModel.SomeProp)); | ||
errorMessage.Should().Contain(TestSmartEnum.TestFoo.Name); | ||
errorMessage.Should().Contain(TestSmartEnum.TestBar.Name); | ||
errorMessage.Should().Contain(TestSmartEnum.TestFizz.Name); | ||
errorMessage.Should().Contain(TestSmartEnum.TestBuzz.Name); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void IsValidGivenNonString() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
object nonString = new { }; | ||
|
||
bool isValid = attribute.IsValid(nonString); | ||
|
||
isValid.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public void IsValidGivenNullString() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
string nullString = null; | ||
|
||
bool isValid = attribute.IsValid(nullString); | ||
|
||
isValid.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public void IsValidGivenNullNonString() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
object nullObject = null; | ||
|
||
bool isValid = attribute.IsValid(nullObject); | ||
|
||
isValid.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public void IsValidForEachMemberOfAGivenSmartEnum() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
using (new AssertionScope()) | ||
{ | ||
foreach (var name in TestSmartEnum.List.Select(at => at.Name)) | ||
{ | ||
bool isValid = attribute.IsValid(name); | ||
isValid.Should().BeTrue(); | ||
} | ||
} | ||
} | ||
|
||
[Fact] | ||
public void IsValidForCaseInsensitiveStringWhenCaseInsensitiveMatchingEnabled() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum), allowCaseInsensitiveMatch: true); | ||
var caseInsensitiveSource = TestSmartEnum.TestFoo.Name.ToLower(); | ||
|
||
bool isValid = attribute.IsValid(caseInsensitiveSource); | ||
|
||
isValid.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public void IsNotValidForCaseInsensitiveStringWhenCaseInsensitiveMatchingDisabled() | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
var caseInsensitiveSource = TestSmartEnum.TestFoo.Name.ToLower(); | ||
|
||
bool isValid = attribute.IsValid(caseInsensitiveSource); | ||
|
||
isValid.Should().BeFalse(); | ||
} | ||
|
||
[Theory] | ||
[InlineData(" ")] | ||
[InlineData("Some Wrong Value")] | ||
[InlineData("25")] | ||
public void IsNotValidGivenNonSmartEnumNames(string invalidName) | ||
{ | ||
var attribute = new SmartEnumNameAttribute(typeof(TestSmartEnum)); | ||
|
||
bool isValid = attribute.IsValid(invalidName); | ||
|
||
isValid.Should().BeFalse(); | ||
} | ||
|
||
private class TestValidationModel | ||
{ | ||
[SmartEnumName(typeof(TestSmartEnum))] | ||
public string SomeProp { get; set; } | ||
} | ||
|
||
private class TestSmartEnum : SmartEnum<TestSmartEnum> | ||
{ | ||
public static readonly TestSmartEnum TestFoo = new TestSmartEnum(nameof(TestFoo), 1); | ||
public static readonly TestSmartEnum TestBar = new TestSmartEnum(nameof(TestBar), 2); | ||
public static readonly TestSmartEnum TestFizz = new TestSmartEnum(nameof(TestFizz), 3); | ||
public static readonly TestSmartEnum TestBuzz = new TestSmartEnum(nameof(TestBuzz), 4); | ||
|
||
private TestSmartEnum(string name, int value) : base(name, value) { } | ||
} | ||
|
||
private class TestSmartEnumWithStringKeyType : SmartEnum<TestSmartEnumWithStringKeyType, string> | ||
{ | ||
private TestSmartEnumWithStringKeyType(string name, string value) : base(name, value) { } | ||
} | ||
} | ||
} |