From 99f4d5abffd3f4ffbdb0e7c021c0095fdee2d85c Mon Sep 17 00:00:00 2001 From: Guillaume Caya-Letourneau Date: Tue, 31 Oct 2023 09:27:00 -0400 Subject: [PATCH 1/3] Added guid enumerable attribute --- .../ContainsValidGuidAttributeTests.cs | 81 +++++++++++++++++++ .../ContainsValidGuidAttribute.cs | 35 ++++++++ .../PublicAPI.Shipped.txt | 3 + 3 files changed, 119 insertions(+) create mode 100644 src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs create mode 100644 src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs diff --git a/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs new file mode 100644 index 0000000..e0b79d8 --- /dev/null +++ b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Workleap.ComponentModel.DataAnnotations.Tests; + +public class ContainsValidGuidAttributeTests +{ + [Theory] + [ClassData(typeof(ValidData))] + public void Given_ValidGuids_When_Validate_Then_Valid(object? inputs) + { + var attr = new ContainsValidGuidAttribute(); + Assert.True(attr.IsValid(inputs)); + } + + [Theory] + [ClassData(typeof(InvalidData))] + public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs) + { + var attr = new ContainsValidGuidAttribute(); + 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, ContainsValidGuidAttribute.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[] { 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" } }; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } + + private class SomeClass + { + [ContainsValidGuid] + public Guid?[] Values { get; set; } = Array.Empty(); + } +} + diff --git a/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs b/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs new file mode 100644 index 0000000..9f06201 --- /dev/null +++ b/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.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 class ContainsValidGuidAttribute : ValidationAttribute +{ + internal const string ErrorMessageFormat = "The field {0} must be an enumerable that contains at least one valid GUID"; + + public ContainsValidGuidAttribute() : base(ErrorMessageFormat) + { + } + + public override bool IsValid(object? value) => value switch + { + null => false, + 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..f3b5afc 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.ContainsValidGuidAttribute +Workleap.ComponentModel.DataAnnotations.ContainsValidGuidAttribute.ContainsValidGuidAttribute() -> 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.ContainsValidGuidAttribute.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 From 205691ca470d086bb606bf310df8dcd39af2d486 Mon Sep 17 00:00:00 2001 From: Guillaume Caya-Letourneau Date: Tue, 31 Oct 2023 09:47:17 -0400 Subject: [PATCH 2/3] Fix ci --- .../ContainsValidGuidAttributeTests.cs | 3 +-- .../ContainsValidGuidAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs index e0b79d8..0dd34cc 100644 --- a/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs +++ b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs @@ -77,5 +77,4 @@ private class SomeClass [ContainsValidGuid] public Guid?[] Values { get; set; } = Array.Empty(); } -} - +} \ No newline at end of file diff --git a/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs b/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs index 9f06201..e8b2549 100644 --- a/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs +++ b/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs @@ -6,7 +6,7 @@ namespace Workleap.ComponentModel.DataAnnotations; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] -public class ContainsValidGuidAttribute : ValidationAttribute +public sealed class ContainsValidGuidAttribute : ValidationAttribute { internal const string ErrorMessageFormat = "The field {0} must be an enumerable that contains at least one valid GUID"; From 7b916722321cde4a76ea62a3d53cc58ce7306741 Mon Sep 17 00:00:00 2001 From: Guillaume Caya-Letourneau Date: Tue, 31 Oct 2023 10:16:37 -0400 Subject: [PATCH 3/3] Review fixes --- README.md | 3 ++- ... => ContainsNonEmptyGuidAttributeTests.cs} | 20 ++++++++++--------- ...te.cs => ContainsNonEmptyGuidAttribute.cs} | 8 ++++---- .../PublicAPI.Shipped.txt | 6 +++--- 4 files changed, 20 insertions(+), 17 deletions(-) rename src/Workleap.ComponentModel.DataAnnotations.Tests/{ContainsValidGuidAttributeTests.cs => ContainsNonEmptyGuidAttributeTests.cs} (82%) rename src/Workleap.ComponentModel.DataAnnotations/{ContainsValidGuidAttribute.cs => ContainsNonEmptyGuidAttribute.cs} (80%) 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/ContainsValidGuidAttributeTests.cs b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs similarity index 82% rename from src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs rename to src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs index 0dd34cc..c05b1b1 100644 --- a/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsValidGuidAttributeTests.cs +++ b/src/Workleap.ComponentModel.DataAnnotations.Tests/ContainsNonEmptyGuidAttributeTests.cs @@ -6,13 +6,13 @@ namespace Workleap.ComponentModel.DataAnnotations.Tests; -public class ContainsValidGuidAttributeTests +public sealed class ContainsNonEmptyGuidAttributeTests { [Theory] [ClassData(typeof(ValidData))] public void Given_ValidGuids_When_Validate_Then_Valid(object? inputs) { - var attr = new ContainsValidGuidAttribute(); + var attr = new ContainsNonEmptyGuidAttribute(); Assert.True(attr.IsValid(inputs)); } @@ -20,7 +20,7 @@ public void Given_ValidGuids_When_Validate_Then_Valid(object? inputs) [ClassData(typeof(InvalidData))] public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs) { - var attr = new ContainsValidGuidAttribute(); + var attr = new ContainsNonEmptyGuidAttribute(); Assert.False(attr.IsValid(inputs)); } @@ -28,7 +28,7 @@ public void Given_InvalidGuids_When_Validate_Then_Invalid(object? inputs) public void Validator_TryValidateObject_Returns_The_Expected_Error_Message_When_Validation_Fails() { var something = new SomeClass(); - var expectedErrorMessage = string.Format(CultureInfo.InvariantCulture, ContainsValidGuidAttribute.ErrorMessageFormat, nameof(SomeClass.Values)); + var expectedErrorMessage = string.Format(CultureInfo.InvariantCulture, ContainsNonEmptyGuidAttribute.ErrorMessageFormat, nameof(SomeClass.Values)); var results = new List(); var context = new ValidationContext(something, serviceProvider: null, items: null); @@ -40,10 +40,11 @@ public void Validator_TryValidateObject_Returns_The_Expected_Error_Message_When_ Assert.Equal(expectedErrorMessage, result.ErrorMessage); } - private class ValidData : IEnumerable + private class ValidData : IEnumerable { - public IEnumerator GetEnumerator() + 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") } }; @@ -57,9 +58,9 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } - private class InvalidData : IEnumerable + private class InvalidData : IEnumerable { - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { yield return new object[] { new Guid[] { default } }; yield return new object[] { new Guid?[] { default } }; @@ -67,6 +68,7 @@ public IEnumerator GetEnumerator() 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(); @@ -74,7 +76,7 @@ public IEnumerator GetEnumerator() private class SomeClass { - [ContainsValidGuid] + [ContainsNonEmptyGuid] public Guid?[] Values { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs b/src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs similarity index 80% rename from src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs rename to src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs index e8b2549..9b82591 100644 --- a/src/Workleap.ComponentModel.DataAnnotations/ContainsValidGuidAttribute.cs +++ b/src/Workleap.ComponentModel.DataAnnotations/ContainsNonEmptyGuidAttribute.cs @@ -6,17 +6,17 @@ namespace Workleap.ComponentModel.DataAnnotations; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] -public sealed class ContainsValidGuidAttribute : ValidationAttribute +public sealed class ContainsNonEmptyGuidAttribute : ValidationAttribute { - internal const string ErrorMessageFormat = "The field {0} must be an enumerable that contains at least one valid GUID"; + internal const string ErrorMessageFormat = "The field {0} must be an collection that contains at least one non-empty GUID"; - public ContainsValidGuidAttribute() : base(ErrorMessageFormat) + public ContainsNonEmptyGuidAttribute() : base(ErrorMessageFormat) { } public override bool IsValid(object? value) => value switch { - null => false, + 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), diff --git a/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt b/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt index f3b5afc..4f4fb8a 100644 --- a/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt +++ b/src/Workleap.ComponentModel.DataAnnotations/PublicAPI.Shipped.txt @@ -2,8 +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.ContainsValidGuidAttribute -Workleap.ComponentModel.DataAnnotations.ContainsValidGuidAttribute.ContainsValidGuidAttribute() -> 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 @@ -45,7 +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.ContainsValidGuidAttribute.IsValid(object? value) -> bool +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