diff --git a/README.md b/README.md index fc5c21d..15839a1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ The most useful validation attribute here is probably `ValidatePropertiesAttribu | Attribute | Description | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `GuidAttribute` | Validates that a string property is a well-formatted GUID with an optional format | -| `NotEmptyAttribute` | Validate that an enumerable property is not empty | +| `ContainsNonEmptyGuidAttribute` | Validates that a Guid enumerable property contains at least one non-empty Guid | +| `NotEmptyAttribute` | Validates that an enumerable property is not empty | | `ValidatePropertiesAttribute` | Validates **all properties of a complex type property** (nested object validation) | | `TimeSpanAttribute` | Validates that a string property is a well-formatted TimeSpan with an optional format | | `UrlOfKindAttribute` | Validates that a string or `Uri` property is a well-formatted url of the specified `UriKind` | diff --git a/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs new file mode 100644 index 0000000..c05b1b1 --- /dev/null +++ b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Workleap.ComponentModel.DataAnnotations.Tests; + +public sealed class ContainsNonEmptyGuidAttributeTests +{ + [Theory] + [ClassData(typeof(ValidData))] + public void Given_ValidGuids_When_Validate_Then_Valid(object? inputs) + { + var attr = new ContainsNonEmptyGuidAttribute(); + Assert.True(attr.IsValid(inputs)); + } + + [Theory] + [ClassData(typeof(InvalidData))] + public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs) + { + var attr = new ContainsNonEmptyGuidAttribute(); + Assert.False(attr.IsValid(inputs)); + } + + [Fact] + public void Validator_TryValidateObject_Returns_The_Expected_Error_Message_When_Validation_Fails() + { + var something = new SomeClass(); + var expectedErrorMessage = string.Format(CultureInfo.InvariantCulture, ContainsNonEmptyGuidAttribute.ErrorMessageFormat, nameof(SomeClass.Values)); + + var results = new List(); + var context = new ValidationContext(something, serviceProvider: null, items: null); + var isValid = Validator.TryValidateObject(something, context, results, validateAllProperties: true); + + Assert.False(isValid); + var result = Assert.Single(results); + Assert.NotNull(result.ErrorMessage); + Assert.Equal(expectedErrorMessage, result.ErrorMessage); + } + + private class ValidData : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield return new object?[] { null }; + yield return new object[] { new Guid[] { new("f8daff85-4393-42ae-9ab5-8620ab20c8da") } }; + yield return new object[] { new string[] { "d78b48f9-37b8-47dd-8e47-0325dd3e7899" } }; + yield return new object[] { new Guid[] { default, new("846398d2-416d-4f05-86b0-431e11117abc") } }; + yield return new object[] { new Guid[] { Guid.Empty, new("08bfec4a-da43-40de-a15a-0d10f227e6b7") } }; + yield return new object[] { new Guid?[] { default, new Guid("2dd81281-7498-411c-aea1-5cec96f2dd8d") } }; + yield return new object[] { new string[] { "00000000-0000-0000-0000-000000000000", "7d8c61f7-a334-4599-b801-45f0934099a4" } }; + yield return new object[] { new string[] { string.Empty, "7c3cdfba-4466-44ab-bb95-da02eac67380" } }; + yield return new object[] { new string?[] { default, "f8daff85-4393-42ae-9ab5-8620ab20c8da" } }; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + + private class InvalidData : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield return new object[] { new Guid[] { default } }; + yield return new object[] { new Guid?[] { default } }; + yield return new object[] { new string[] { string.Empty } }; + yield return new object[] { new string?[] { default } }; + yield return new object[] { new Guid[] { Guid.Empty } }; + yield return new object[] { new string[] { "00000000-0000-0000-0000-000000000000" } }; + yield return new object[] { new int[] { 0 } }; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + + private class SomeClass + { + [ContainsNonEmptyGuid] + public Guid?[] Values { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs b/src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs new file mode 100644 index 0000000..9b82591 --- /dev/null +++ b/src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace Workleap.ComponentModel.DataAnnotations; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] +public sealed class ContainsNonEmptyGuidAttribute : ValidationAttribute +{ + internal const string ErrorMessageFormat = "The field {0} must be an collection that contains at least one non-empty GUID"; + + public ContainsNonEmptyGuidAttribute() : base(ErrorMessageFormat) + { + } + + public override bool IsValid(object? value) => value switch + { + null => true, + IEnumerable enumerable => enumerable.Any(this.IsValidGuid), + IEnumerable enumerable => enumerable.Any(x => x != null && this.IsValidGuid(x.Value)), + IEnumerable enumerable => enumerable.Any(this.IsValidGuid), + _ => false, + }; + + private bool IsValidGuid(string valueAsString) + { + return Guid.TryParse(valueAsString, out var guid) && this.IsValidGuid(guid); + } + + private bool IsValidGuid(Guid guid) + { + return guid != Guid.Empty; + } +} \ No newline at end of file diff --git a/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt b/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt index 2031a6a..4f4fb8a 100644 --- a/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt +++ b/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt @@ -2,6 +2,8 @@ abstract Workleap.ComponentModel.DataAnnotations.TextBasedValidationAttribute.IsValid(string! value) -> bool Workleap.ComponentModel.DataAnnotations.ContainsAttribute Workleap.ComponentModel.DataAnnotations.ContainsAttribute.ContainsAttribute(string! text) -> void +Workleap.ComponentModel.DataAnnotations.ContainsNonEmptyGuidAttribute +Workleap.ComponentModel.DataAnnotations.ContainsNonEmptyGuidAttribute.ContainsNonEmptyGuidAttribute() -> void Workleap.ComponentModel.DataAnnotations.EndsWithAttribute Workleap.ComponentModel.DataAnnotations.EndsWithAttribute.EndsWithAttribute(string! text) -> void Workleap.ComponentModel.DataAnnotations.GuidAttribute @@ -43,6 +45,7 @@ Workleap.ComponentModel.DataAnnotations.UrlOfKindAttribute.Kind.get -> System.Ur Workleap.ComponentModel.DataAnnotations.UrlOfKindAttribute.UrlOfKindAttribute(System.UriKind kind) -> void Workleap.ComponentModel.DataAnnotations.ValidatePropertiesAttribute Workleap.ComponentModel.DataAnnotations.ValidatePropertiesAttribute.ValidatePropertiesAttribute() -> void +override Workleap.ComponentModel.DataAnnotations.ContainsNonEmptyGuidAttribute.IsValid(object? value) -> bool override Workleap.ComponentModel.DataAnnotations.GuidAttribute.FormatErrorMessage(string! name) -> string! override Workleap.ComponentModel.DataAnnotations.GuidAttribute.IsValid(object? value) -> bool override Workleap.ComponentModel.DataAnnotations.NotEmptyAttribute.IsValid(object? value) -> bool