From c98ea50e77441d58e4e539bfabe13aff9746418d Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 5 Mar 2024 16:36:37 +0100 Subject: [PATCH 01/10] Add ValidatingNewType --- Dbosoft.Functional.sln | 10 +- .../Validations/ValidatingNewType.cs | 65 ++++++++++ .../Dbosoft.Functional.Tests.csproj | 31 +++++ test/Dbosoft.Functional.Tests/Usings.cs | 3 + .../Validations/ValidatingNewTypeTests.cs | 115 ++++++++++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/Dbosoft.Functional/Validations/ValidatingNewType.cs create mode 100644 test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj create mode 100644 test/Dbosoft.Functional.Tests/Usings.cs create mode 100644 test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs diff --git a/Dbosoft.Functional.sln b/Dbosoft.Functional.sln index 909cbbf..eb57d06 100644 --- a/Dbosoft.Functional.sln +++ b/Dbosoft.Functional.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30907.101 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dbosoft.Functional", "src\Dbosoft.Functional\Dbosoft.Functional.csproj", "{D38F1C28-A069-4E32-81FA-1F3F1C7F3619}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dbosoft.Functional.Tests", "test\Dbosoft.Functional.Tests\Dbosoft.Functional.Tests.csproj", "{ADA73324-0DE5-479D-A63B-888EB922984E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {D38F1C28-A069-4E32-81FA-1F3F1C7F3619}.Debug|Any CPU.Build.0 = Debug|Any CPU {D38F1C28-A069-4E32-81FA-1F3F1C7F3619}.Release|Any CPU.ActiveCfg = Release|Any CPU {D38F1C28-A069-4E32-81FA-1F3F1C7F3619}.Release|Any CPU.Build.0 = Release|Any CPU + {ADA73324-0DE5-479D-A63B-888EB922984E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADA73324-0DE5-479D-A63B-888EB922984E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADA73324-0DE5-479D-A63B-888EB922984E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADA73324-0DE5-479D-A63B-888EB922984E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Dbosoft.Functional/Validations/ValidatingNewType.cs b/src/Dbosoft.Functional/Validations/ValidatingNewType.cs new file mode 100644 index 0000000..8ffc7d1 --- /dev/null +++ b/src/Dbosoft.Functional/Validations/ValidatingNewType.cs @@ -0,0 +1,65 @@ +using LanguageExt.ClassInstances.Pred; +using LanguageExt.Common; +using LanguageExt.TypeClasses; +using LanguageExt; +using System; +using System.Collections.Generic; +using System.Text; +using static LanguageExt.Prelude; + +namespace Dbosoft.Functional.Validations +{ + /// + /// This class extends the LanguageExt + /// for support for validation using . + /// + public abstract class ValidatingNewType : NewType, ORD> + where NEWTYPE : ValidatingNewType + where ORD : struct, Ord + { + protected ValidatingNewType(A value) : base(value) + { + } + + public static Validation NewValidation(A value) => + NewTry(value).Match( + Succ: Success, + Fail: ex => ex is ValidationException vex + ? Fail(vex.Errors) + : Fail(Error.New(ex))); + + public static Either NewEither(A value) => + NewValidation(value).ToEither().MapLeft( + errors => Error.New($"The value is not a valid {typeof(NEWTYPE).Name}.", Error.Many(errors))); + + /// + /// Subclasses should use this method in their constructors to validate the value. + /// + /// + /// Thrown when the validation has failed. + /// + protected static T ValidOrThrow(Validation validation) => + validation.Match( + Succ: identity, + Fail: errors => throw new ValidationException(errors)); + + /// + /// This exception is thrown by + /// when the validation has failed. + /// + /// + public sealed class ValidationException : Exception + { + internal ValidationException(Seq errors) + : base($"The value is not a valid {typeof(T).Name}: {errors.ToFullArrayString()}") + { + Errors = errors; + } + + /// + /// The actual validation errors. + /// + public Seq Errors { get; } + } + } +} diff --git a/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj b/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj new file mode 100644 index 0000000..ee95efe --- /dev/null +++ b/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/Dbosoft.Functional.Tests/Usings.cs b/test/Dbosoft.Functional.Tests/Usings.cs new file mode 100644 index 0000000..59ec1a3 --- /dev/null +++ b/test/Dbosoft.Functional.Tests/Usings.cs @@ -0,0 +1,3 @@ +global using FluentAssertions; +global using FluentAssertions.LanguageExt; +global using Xunit; diff --git a/test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs b/test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs new file mode 100644 index 0000000..94aebb1 --- /dev/null +++ b/test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs @@ -0,0 +1,115 @@ +using Dbosoft.Functional.Validations; +using LanguageExt.ClassInstances; +using LanguageExt.Common; +using LanguageExt; +using static FluentAssertions.FluentActions; +using static LanguageExt.Prelude; + +public class ValidatingNewTypeTests +{ + public class TestTypeWithArgumentException + : ValidatingNewType + { + public TestTypeWithArgumentException(string value) : base(value) + { + throw new ArgumentException("The value is invalid", nameof(value)); + } + } + + public class TestTypeWithValidationException + : ValidatingNewType + { + public TestTypeWithValidationException(string value) : base(value) + { + ValidOrThrow(Fail(Error.New("First validation failed")) + | Fail(Error.New("Second validation failed"))); + } + } + + [Fact] + public void New_WithArgumentException_ThrowsArgumentException() + { + Invoking(() => TestTypeWithArgumentException.New("test")) + .Should().Throw() + .WithMessage("The value is invalid (Parameter 'value')"); + } + + [Fact] + public void New_WithValidationErrors_ThrowsValidationException() + { + var exception = Invoking(() => TestTypeWithValidationException.New("test")) + .Should().Throw>() + .WithMessage("The value is not a valid TestTypeWithValidationException: " + + "[First validation failed, Second validation failed]") + .Subject.Should().ContainSingle().Subject; + + ShouldContainValidationErrors(exception.Errors); + } + + [Fact] + public void NewEither_WithArgumentException_ReturnsErrorWithInnerError() + { + var result = TestTypeWithArgumentException.NewEither("test"); + + var error = result.Should().BeLeft().Subject; + var innerError = error.Inner.Should().BeSome() + .Which.Should().BeOfType().Subject; + innerError.Message.Should().Be("The value is invalid (Parameter 'value')"); + innerError.IsExceptional.Should().BeTrue(); + innerError.Exception.Should().BeSome().Which + .Should().BeOfType(); + } + + [Fact] + public void NewEither_WithValidationErrors_ReturnsErrorWithInnerError() + { + var result = TestTypeWithValidationException.NewEither("test"); + + var error = result.Should().BeLeft().Subject; + error.Message.Should().Be("The value is not a valid TestTypeWithValidationException."); + var innerErrors = error.Inner.Should().BeSome().Which + .Should().BeOfType().Subject.Errors; + ShouldContainValidationErrors(innerErrors); + } + + [Fact] + public void NewValidation_ArgumentException_ReturnsErrorWithException() + { + var result = TestTypeWithArgumentException.NewValidation("test"); + + result.Should().BeFail().Which.Should().SatisfyRespectively( + error => + { + error.Message.Should().Be("The value is invalid (Parameter 'value')"); + error.IsExceptional.Should().BeTrue(); + error.Exception.Should().BeSome().Which + .Should().BeOfType(); + }); + } + + [Fact] + public void NewValidation_WithValidationErrors_ReturnsBothErrors() + { + var result = TestTypeWithValidationException.NewValidation("test"); + + var errors = result.Should().BeFail().Subject; + ShouldContainValidationErrors(errors); + } + + private static void ShouldContainValidationErrors(Seq errors) + { + errors.Should().SatisfyRespectively( + error => + { + error.Message.Should().Be("First validation failed"); + error.IsExceptional.Should().BeFalse(); + error.Exception.Should().BeNone(); + }, + error => + { + error.Message.Should().Be("Second validation failed"); + error.IsExceptional.Should().BeFalse(); + error.Exception.Should().BeNone(); + }); + } +} From 9a6edeeb3561e9ac9bf8a3efb7886e86320a3126 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 5 Mar 2024 19:44:52 +0100 Subject: [PATCH 02/10] Continue implementation --- .../DataTypes/ValidatingNewType.cs | 64 ++++++ .../Dbosoft.Functional.csproj | 3 +- .../Validations/ComplexValidations.cs | 93 +++++++++ .../Validations/ValidatingNewType.cs | 65 ------ .../Validations/ValidationIssue.cs | 46 +++++ .../ValidatingNewTypeTests.cs | 4 +- .../Validations/ComplexValidationsTests.cs | 188 ++++++++++++++++++ 7 files changed, 394 insertions(+), 69 deletions(-) create mode 100644 src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs create mode 100644 src/Dbosoft.Functional/Validations/ComplexValidations.cs delete mode 100644 src/Dbosoft.Functional/Validations/ValidatingNewType.cs create mode 100644 src/Dbosoft.Functional/Validations/ValidationIssue.cs rename test/Dbosoft.Functional.Tests/{Validations => DataTypes}/ValidatingNewTypeTests.cs (98%) create mode 100644 test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs diff --git a/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs new file mode 100644 index 0000000..968b48b --- /dev/null +++ b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs @@ -0,0 +1,64 @@ +using LanguageExt.ClassInstances.Pred; +using LanguageExt.Common; +using LanguageExt.TypeClasses; +using LanguageExt; +using System; +using System.Collections.Generic; +using System.Text; +using static LanguageExt.Prelude; + +namespace Dbosoft.Functional.DataTypes; + +/// +/// This class extends the LanguageExt +/// for support for validation using . +/// +public abstract class ValidatingNewType : NewType, ORD> + where NEWTYPE : ValidatingNewType + where ORD : struct, Ord +{ + protected ValidatingNewType(A value) : base(value) + { + } + + public static Validation NewValidation(A value) => + NewTry(value).Match( + Succ: Success, + Fail: ex => ex is ValidationException vex + ? Fail(vex.Errors) + : Fail(Error.New(ex))); + + public static Either NewEither(A value) => + NewValidation(value).ToEither().MapLeft( + errors => Error.New($"The value is not a valid {typeof(NEWTYPE).Name}.", Error.Many(errors))); + + /// + /// Subclasses should use this method in their constructors to validate the value. + /// + /// + /// Thrown when the validation has failed. + /// + protected static T ValidOrThrow(Validation validation) => + validation.Match( + Succ: identity, + Fail: errors => throw new ValidationException(errors)); + + /// + /// This exception is thrown by + /// when the validation has failed. + /// + /// + public sealed class ValidationException : Exception + { + internal ValidationException(Seq errors) + : base($"The value is not a valid {typeof(T).Name}: {errors.ToFullArrayString()}") + { + Errors = errors; + } + + /// + /// The actual validation errors. + /// + public Seq Errors { get; } + } +} diff --git a/src/Dbosoft.Functional/Dbosoft.Functional.csproj b/src/Dbosoft.Functional/Dbosoft.Functional.csproj index 854eee4..463b528 100644 --- a/src/Dbosoft.Functional/Dbosoft.Functional.csproj +++ b/src/Dbosoft.Functional/Dbosoft.Functional.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 10 true false MIT @@ -30,8 +31,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Dbosoft.Functional/Validations/ComplexValidations.cs b/src/Dbosoft.Functional/Validations/ComplexValidations.cs new file mode 100644 index 0000000..272b875 --- /dev/null +++ b/src/Dbosoft.Functional/Validations/ComplexValidations.cs @@ -0,0 +1,93 @@ +using LanguageExt.Common; +using LanguageExt; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Text; +using static LanguageExt.Prelude; + +namespace Dbosoft.Functional.Validations; + +#nullable enable + +/// +/// These functions can be used to validate complex objects. They +/// automate the process of the descending through an object tree +/// and collecting the validation issues. +/// +public static class ComplexValidations +{ + public static Validation ValidateProperty( + T toValidate, + Expression> getProperty, + Func> validate, + string path = "", + bool required = false) => + Optional(getProperty.Compile().Invoke(toValidate)) + .Filter(notEmpty) + .Match( + Some: v => validate(v).Map(_ => unit) + .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), + None: Success(unit)); + + public static Validation ValidateProperty( + T toValidate, + Expression> getProperty, + Func> validate, + string path = "", + bool required = false) => + Optional(getProperty.Compile().Invoke(toValidate)) + .Match( + Some: v => validate(v).Map(_ => unit) + .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), + None: Success(unit)); + + public static Validation ValidateProperty( + T toValidate, + Expression> getProperty, + Func> validate, + string path = "", + bool required = false) => + Optional(getProperty.Compile().Invoke(toValidate)) + .Match( + Some: v => validate(v, JoinPath(path, getProperty)), + None: Success(unit)); + + private static Validation ValidateProperty( + Option value, + Func> validate, + string propertyName, + string path, + bool required) => + value.Match( + Some: validate, + None: () => required + ? Fail(new ValidationIssue(path, $"The property {propertyName} is required.")) + : Success(unit)); + + public static Validation ValidateList( + T toValidate, + Expression?>> getList, + Func> validate, + string path = "", + Option minCount = default, + Option maxCount = default) => + getList.Compile().Invoke(toValidate).ToSeq() + .Map((index, listItem) => + from li in Optional(listItem).ToValidation( + new ValidationIssue($"{JoinPath(path, getList)}[{index}]", "The entry must not be null.")) + from _ in validate(listItem, $"{JoinPath(path, getList)}[{index}]") + select unit) + .Fold(Success(unit), (acc, listItem) => acc | listItem); + + private static string JoinPath(string path, Expression> getProperty) + { + if (!(getProperty.Body is MemberExpression memberExpression)) + throw new ArgumentException("The expression must access and return a class member"); + + return notEmpty(path) ? $"{path}.{memberExpression.Member.Name}" : memberExpression.Member.Name; + } +} + +#nullable restore diff --git a/src/Dbosoft.Functional/Validations/ValidatingNewType.cs b/src/Dbosoft.Functional/Validations/ValidatingNewType.cs deleted file mode 100644 index 8ffc7d1..0000000 --- a/src/Dbosoft.Functional/Validations/ValidatingNewType.cs +++ /dev/null @@ -1,65 +0,0 @@ -using LanguageExt.ClassInstances.Pred; -using LanguageExt.Common; -using LanguageExt.TypeClasses; -using LanguageExt; -using System; -using System.Collections.Generic; -using System.Text; -using static LanguageExt.Prelude; - -namespace Dbosoft.Functional.Validations -{ - /// - /// This class extends the LanguageExt - /// for support for validation using . - /// - public abstract class ValidatingNewType : NewType, ORD> - where NEWTYPE : ValidatingNewType - where ORD : struct, Ord - { - protected ValidatingNewType(A value) : base(value) - { - } - - public static Validation NewValidation(A value) => - NewTry(value).Match( - Succ: Success, - Fail: ex => ex is ValidationException vex - ? Fail(vex.Errors) - : Fail(Error.New(ex))); - - public static Either NewEither(A value) => - NewValidation(value).ToEither().MapLeft( - errors => Error.New($"The value is not a valid {typeof(NEWTYPE).Name}.", Error.Many(errors))); - - /// - /// Subclasses should use this method in their constructors to validate the value. - /// - /// - /// Thrown when the validation has failed. - /// - protected static T ValidOrThrow(Validation validation) => - validation.Match( - Succ: identity, - Fail: errors => throw new ValidationException(errors)); - - /// - /// This exception is thrown by - /// when the validation has failed. - /// - /// - public sealed class ValidationException : Exception - { - internal ValidationException(Seq errors) - : base($"The value is not a valid {typeof(T).Name}: {errors.ToFullArrayString()}") - { - Errors = errors; - } - - /// - /// The actual validation errors. - /// - public Seq Errors { get; } - } - } -} diff --git a/src/Dbosoft.Functional/Validations/ValidationIssue.cs b/src/Dbosoft.Functional/Validations/ValidationIssue.cs new file mode 100644 index 0000000..2ab46e1 --- /dev/null +++ b/src/Dbosoft.Functional/Validations/ValidationIssue.cs @@ -0,0 +1,46 @@ +using LanguageExt.Common; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Dbosoft.Functional.Validations +{ + /// + /// Represents an issue which was detected during a validation. + /// + public readonly struct ValidationIssue + { + private readonly string _member; + private readonly string _message; + + /// + /// Creates a new validation issue. + /// + public ValidationIssue(string member, string message) + { + _member = member; + _message = message; + } + + /// + /// The path to the member in the object tree which caused the issue, + /// e.g. Participants[2].Name. + /// + public string Member => _member; + + /// + /// The description of the issue. + /// + public string Message => _message; + + /// + /// Converts the issue to an . + /// + public Error ToError() => Error.New(ToString()); + + /// + /// Returns a string representation of the issue. + /// + public override string ToString() => $"{_member}: {_message}"; + } +} diff --git a/test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs similarity index 98% rename from test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs rename to test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs index 94aebb1..ea1609a 100644 --- a/test/Dbosoft.Functional.Tests/Validations/ValidatingNewTypeTests.cs +++ b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs @@ -1,9 +1,9 @@ -using Dbosoft.Functional.Validations; -using LanguageExt.ClassInstances; +using LanguageExt.ClassInstances; using LanguageExt.Common; using LanguageExt; using static FluentAssertions.FluentActions; using static LanguageExt.Prelude; +using Dbosoft.Functional.DataTypes; public class ValidatingNewTypeTests { diff --git a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs new file mode 100644 index 0000000..258fb35 --- /dev/null +++ b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dbosoft.Functional.Validations; +using LanguageExt; +using LanguageExt.Common; +using static Dbosoft.Functional.Validations.ComplexValidations; +using static LanguageExt.Prelude; + +namespace Dbosoft.Functional.Tests.Validations; + +public class ComplexValidationsTests +{ + public class TravelGroupExample + { + [Fact] + public void ValidateTravelGroup_ValidTravelGroupWithOptionalData_ReturnsSuccess() + { + var travelGroup = new TravelGroup + { + Name = "My Travel Group", + Description = "This is a description", + Contact = new Contact + { + Name = "John Doe", + Phone = "+1234567890" + }, + Participants = new[] + { + new Participant { Name = "Alice", Age = 25 }, + new Participant { Name = "Bob", Age = 30 }, + } + }; + + var result = TravelGroupValidations.ValidateTravelGroup(travelGroup); + + result.Should().BeSuccess(); + } + + [Fact] + public void ValidateTravelGroup_ValidTravelGroupWithoutOptionalData_ReturnsSuccess() + { + var travelGroup = new TravelGroup + { + Name = "My Travel Group", + Contact = new Contact + { + Name = "John Doe", + }, + Participants = new[] + { + new Participant { Name = "Alice", Age = 25 }, + new Participant { Name = "Bob", Age = 30 }, + } + }; + + var result = TravelGroupValidations.ValidateTravelGroup(travelGroup); + + result.Should().BeSuccess(); + } + + [Fact] + public void ValidateTravelGroup_InvalidTravelGroup_ReturnsFail() + { + var travelGroup = new TravelGroup + { + Name = "My\nTravel\nGroup", + Contact = new Contact + { + Name = "John\nDoe", + Phone = "abc" + }, + Participants = new[] + { + new Participant { Name = "Alice\nAdams", Age = -1 }, + } + }; + + var result = TravelGroupValidations.ValidateTravelGroup(travelGroup); + + + var errors = result.Should().BeFail().Subject.ToList(); + + result.Should().BeFail().Which.Should().SatisfyRespectively( + issue => + { + issue.Member.Should().Be("Name"); + issue.Message.Should().Be("The description can only contain letters, digits and spaces."); + }, + issue => + { + issue.Member.Should().Be("Contact.Name"); + issue.Message.Should().Be("The description can only contain letters, digits and white space."); + }, + issue => + { + issue.Member.Should().Be("Contact.Phone"); + issue.Message.Should().Be("The phone number can only contain digits or +."); + }, + issue => + { + issue.Member.Should().Be("Participants"); + issue.Message.Should().Be("The list must contain at least 2 entries."); + }, + issue => + { + issue.Member.Should().Be("Participants[0].Name"); + issue.Message.Should().Be("The description can only contain letters, digits and spaces."); + }, + issue => + { + issue.Member.Should().Be("Participants[0].Age"); + issue.Message.Should().Be("The age must be between 0 and 150."); + }); + } + } +} + +public static class TravelGroupValidations +{ + public static Validation ValidateTravelGroup( + TravelGroup travelGroup, string path = "") => + ValidateProperty(travelGroup, x => x.Name, ValidateName, path, required: true) + | ValidateProperty(travelGroup, x => x.Description, ValidateDescription, path, required: false) + | ValidateProperty(travelGroup, x => x.Contact, ValidateContact, path, required: true) + | ValidateList(travelGroup, x => x.Participants, ValidateParticipant, path, minCount: 2, maxCount: 20); + + public static Validation ValidateContact( + Contact contact, string path = "") => + ValidateProperty(contact, x => x.Name, ValidateName, path, required: true) + | ValidateProperty(contact, x => x.Phone, ValidatePhone, path, required: false); + + public static Validation ValidateParticipant( + Participant participant, string path = "") => + ValidateProperty(participant, x => x.Name, ValidateName, path, required: true) + | ValidateProperty(participant, x => x.Age, ValidateAge, path, required: true); + + public static Validation ValidateName(string name) => + from _ in guard(name.ToSeq().All(c => char.IsLetterOrDigit(c) || c == ' '), + Error.New("The description can only contain letters, digits and spaces.")) + .ToValidation() + select name; + + public static Validation ValidateDescription(string description) => + from _ in guard(description.ToSeq().All(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c)), + Error.New("The description can only contain letters, digits and white space.")) + .ToValidation() + select description; + + public static Validation ValidateAge(int age) => + from _ in guard(age is >= 0 and <= 150, + Error.New("The age must be between 0 and 150.")) + .ToValidation() + select age; + + public static Validation ValidatePhone(string phone) => + from _ in guard(phone.ToSeq().All(c => char.IsDigit(c) || c == '+'), + Error.New("The phone number can only contain digits or +.")) + .ToValidation() + select phone; +} + +public class TravelGroup +{ + public string? Name { get; set; } + + public string? Description { get; set; } + + public Contact? Contact { get; set; } + + public Participant[]? Participants { get; set; } +} + +public class Participant +{ + public string? Name { get; set; } + + public int Age { get; set; } +} + +public class Contact +{ + public string? Name { get; set; } + + public string? Phone { get; set; } +} From 39a6113d6292ae4283a33b6594388cc6cab45152 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Tue, 5 Mar 2024 20:31:40 +0100 Subject: [PATCH 03/10] Add additional tests and validate element count of lists --- .../Validations/ComplexValidations.cs | 84 +++++++++++++------ .../Validations/ComplexValidationsTests.cs | 54 ++++++++++-- 2 files changed, 103 insertions(+), 35 deletions(-) diff --git a/src/Dbosoft.Functional/Validations/ComplexValidations.cs b/src/Dbosoft.Functional/Validations/ComplexValidations.cs index 272b875..cb736d4 100644 --- a/src/Dbosoft.Functional/Validations/ComplexValidations.cs +++ b/src/Dbosoft.Functional/Validations/ComplexValidations.cs @@ -24,12 +24,13 @@ public static Validation ValidateProperty( Func> validate, string path = "", bool required = false) => - Optional(getProperty.Compile().Invoke(toValidate)) - .Filter(notEmpty) - .Match( - Some: v => validate(v).Map(_ => unit) - .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), - None: Success(unit)); + ValidateProperty( + Optional(getProperty.Compile().Invoke(toValidate)).Filter(notEmpty), + getProperty, + v => validate(v).Map(_ => unit) + .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), + path, + required); public static Validation ValidateProperty( T toValidate, @@ -37,11 +38,13 @@ public static Validation ValidateProperty> validate, string path = "", bool required = false) => - Optional(getProperty.Compile().Invoke(toValidate)) - .Match( - Some: v => validate(v).Map(_ => unit) - .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), - None: Success(unit)); + ValidateProperty( + Optional(getProperty.Compile().Invoke(toValidate)), + getProperty, + v => validate(v).Map(_ => unit) + .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), + path, + required); public static Validation ValidateProperty( T toValidate, @@ -49,21 +52,25 @@ public static Validation ValidateProperty( Func> validate, string path = "", bool required = false) => - Optional(getProperty.Compile().Invoke(toValidate)) - .Match( - Some: v => validate(v, JoinPath(path, getProperty)), - None: Success(unit)); + ValidateProperty( + Optional(getProperty.Compile().Invoke(toValidate)), + getProperty, + v => validate(v, JoinPath(path, getProperty)), + path, + required); private static Validation ValidateProperty( Option value, + Expression> getProperty, Func> validate, - string propertyName, string path, bool required) => value.Match( Some: validate, None: () => required - ? Fail(new ValidationIssue(path, $"The property {propertyName} is required.")) + ? Fail( + new ValidationIssue(JoinPath(path, getProperty), + $"The {GetPropertyName(getProperty)} is required.")) : Success(unit)); public static Validation ValidateList( @@ -73,21 +80,46 @@ public static Validation ValidateList( string path = "", Option minCount = default, Option maxCount = default) => - getList.Compile().Invoke(toValidate).ToSeq() - .Map((index, listItem) => + ValidateList( + getList.Compile().Invoke(toValidate).ToSeq(), + getList, + validate, + path, + minCount, + maxCount); + + private static Validation ValidateList( + Seq list, + Expression?>> getList, + Func> validate, + string path = "", + Option minCount = default, + Option maxCount = default) => + match(minCount.Filter(c => c > list.Count), + Some: c => Fail( + new ValidationIssue(JoinPath(path, getList), $"The list must have {c} or more entries.")), + None: () => Success(unit)) + | match(maxCount.Filter(c => c < list.Count), + Some: c => Fail( + new ValidationIssue(JoinPath(path, getList), $"The list must have {c} or fewer entries.")), + None: () => Success(unit)) + | list.Map((index, listItem) => from li in Optional(listItem).ToValidation( new ValidationIssue($"{JoinPath(path, getList)}[{index}]", "The entry must not be null.")) from _ in validate(listItem, $"{JoinPath(path, getList)}[{index}]") - select unit) + select unit) .Fold(Success(unit), (acc, listItem) => acc | listItem); - private static string JoinPath(string path, Expression> getProperty) - { - if (!(getProperty.Body is MemberExpression memberExpression)) - throw new ArgumentException("The expression must access and return a class member"); + private static string JoinPath(string path, Expression> getProperty) => + notEmpty(path) ? $"{path}.{GetPropertyName(getProperty)}" : GetPropertyName(getProperty); - return notEmpty(path) ? $"{path}.{memberExpression.Member.Name}" : memberExpression.Member.Name; - } + private static string GetPropertyName(Expression> getProperty) => + getProperty.Body switch + { + MemberExpression memberExpression => memberExpression.Member.Name, + _ => throw new ArgumentException("The expression must access and return a class member.", + nameof(getProperty)) + }; } #nullable restore diff --git a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs index 258fb35..0f4ef66 100644 --- a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs +++ b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Text; using System.Threading.Tasks; using Dbosoft.Functional.Validations; @@ -13,6 +14,27 @@ namespace Dbosoft.Functional.Tests.Validations; public class ComplexValidationsTests { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ValidateProperty_RequiredAndStringIsEmpty_ReturnsFail(string? value) + { + var result = ValidateProperty( + new TestType() { StringValue = value }, + t => t.StringValue, + _ => Success("test"), + "Some.Path", + required: true); + + result.Should().BeFail().Which.Should().SatisfyRespectively( + issue => + { + issue.Member.Should().Be("Some.Path.StringValue"); + issue.Message.Should().Be("The StringValue is required."); + }); + } + public class TravelGroupExample { [Fact] @@ -21,7 +43,7 @@ public void ValidateTravelGroup_ValidTravelGroupWithOptionalData_ReturnsSuccess( var travelGroup = new TravelGroup { Name = "My Travel Group", - Description = "This is a description", + Description = "The most important\ntravel group", Contact = new Contact { Name = "John Doe", @@ -67,6 +89,7 @@ public void ValidateTravelGroup_InvalidTravelGroup_ReturnsFail() var travelGroup = new TravelGroup { Name = "My\nTravel\nGroup", + Description = "The most important | travel group", Contact = new Contact { Name = "John\nDoe", @@ -80,21 +103,23 @@ public void ValidateTravelGroup_InvalidTravelGroup_ReturnsFail() var result = TravelGroupValidations.ValidateTravelGroup(travelGroup); - - var errors = result.Should().BeFail().Subject.ToList(); - result.Should().BeFail().Which.Should().SatisfyRespectively( issue => { issue.Member.Should().Be("Name"); - issue.Message.Should().Be("The description can only contain letters, digits and spaces."); + issue.Message.Should().Be("The name can only contain letters, digits and spaces."); }, issue => { - issue.Member.Should().Be("Contact.Name"); + issue.Member.Should().Be("Description"); issue.Message.Should().Be("The description can only contain letters, digits and white space."); }, issue => + { + issue.Member.Should().Be("Contact.Name"); + issue.Message.Should().Be("The name can only contain letters, digits and spaces."); + }, + issue => { issue.Member.Should().Be("Contact.Phone"); issue.Message.Should().Be("The phone number can only contain digits or +."); @@ -102,12 +127,12 @@ public void ValidateTravelGroup_InvalidTravelGroup_ReturnsFail() issue => { issue.Member.Should().Be("Participants"); - issue.Message.Should().Be("The list must contain at least 2 entries."); + issue.Message.Should().Be("The list must have 2 or more entries."); }, issue => { issue.Member.Should().Be("Participants[0].Name"); - issue.Message.Should().Be("The description can only contain letters, digits and spaces."); + issue.Message.Should().Be("The name can only contain letters, digits and spaces."); }, issue => { @@ -139,7 +164,7 @@ public static Validation ValidateParticipant( public static Validation ValidateName(string name) => from _ in guard(name.ToSeq().All(c => char.IsLetterOrDigit(c) || c == ' '), - Error.New("The description can only contain letters, digits and spaces.")) + Error.New("The name can only contain letters, digits and spaces.")) .ToValidation() select name; @@ -162,6 +187,17 @@ from _ in guard(phone.ToSeq().All(c => char.IsDigit(c) || c == '+'), select phone; } +public class TestType +{ + public string? StringValue { get; set; } + + public int IntValue { get; set; } + + public int? NullableIntValue { get; set; } + + public string[]? StringArray { get; set; } +} + public class TravelGroup { public string? Name { get; set; } From 924f0ba6fdc445baa2dfe6996f6abb9d761fc266 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 10:59:10 +0100 Subject: [PATCH 04/10] * Handle structs and nullable values * Improve tests --- .../Validations/ComplexValidations.cs | 34 +++++++++--- .../Dbosoft.Functional.Tests.csproj | 1 + .../Validations/ComplexValidationsTests.cs | 52 +++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/Dbosoft.Functional/Validations/ComplexValidations.cs b/src/Dbosoft.Functional/Validations/ComplexValidations.cs index cb736d4..9e014fe 100644 --- a/src/Dbosoft.Functional/Validations/ComplexValidations.cs +++ b/src/Dbosoft.Functional/Validations/ComplexValidations.cs @@ -26,12 +26,29 @@ public static Validation ValidateProperty( bool required = false) => ValidateProperty( Optional(getProperty.Compile().Invoke(toValidate)).Filter(notEmpty), - getProperty, v => validate(v).Map(_ => unit) .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), path, + GetPropertyName(getProperty), required); + public static Validation ValidateProperty( + T toValidate, + Expression> getProperty, + Func> validate, + string path = "", + bool required = false) where TProperty :struct + { + var value = getProperty.Compile().Invoke(toValidate); + return ValidateProperty( + value.HasValue ? Some(value.Value) : None, + v => validate(v).Map(_ => unit) + .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), + path, + GetPropertyName(getProperty), + required); + } + public static Validation ValidateProperty( T toValidate, Expression> getProperty, @@ -40,10 +57,10 @@ public static Validation ValidateProperty ValidateProperty( Optional(getProperty.Compile().Invoke(toValidate)), - getProperty, v => validate(v).Map(_ => unit) .MapFail(e => new ValidationIssue(JoinPath(path, getProperty), e.Message)), path, + GetPropertyName(getProperty), required); public static Validation ValidateProperty( @@ -54,23 +71,23 @@ public static Validation ValidateProperty( bool required = false) => ValidateProperty( Optional(getProperty.Compile().Invoke(toValidate)), - getProperty, v => validate(v, JoinPath(path, getProperty)), path, + GetPropertyName(getProperty), required); - private static Validation ValidateProperty( + private static Validation ValidateProperty( Option value, - Expression> getProperty, Func> validate, string path, + string propertyName, bool required) => value.Match( Some: validate, None: () => required ? Fail( - new ValidationIssue(JoinPath(path, getProperty), - $"The {GetPropertyName(getProperty)} is required.")) + new ValidationIssue(JoinPath(path, propertyName), + $"The {propertyName} is required.")) : Success(unit)); public static Validation ValidateList( @@ -113,6 +130,9 @@ from _ in validate(listItem, $"{JoinPath(path, getList)}[{index}]") private static string JoinPath(string path, Expression> getProperty) => notEmpty(path) ? $"{path}.{GetPropertyName(getProperty)}" : GetPropertyName(getProperty); + private static string JoinPath(string path, string propertyName) => + notEmpty(path) ? $"{path}.{propertyName}" : propertyName; + private static string GetPropertyName(Expression> getProperty) => getProperty.Body switch { diff --git a/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj b/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj index ee95efe..a248b19 100644 --- a/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj +++ b/test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj @@ -17,6 +17,7 @@ + all diff --git a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs index 0f4ef66..25bea7f 100644 --- a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs +++ b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs @@ -7,6 +7,7 @@ using Dbosoft.Functional.Validations; using LanguageExt; using LanguageExt.Common; +using Moq; using static Dbosoft.Functional.Validations.ComplexValidations; using static LanguageExt.Prelude; @@ -14,6 +15,57 @@ namespace Dbosoft.Functional.Tests.Validations; public class ComplexValidationsTests { + [Fact] + public void ValidateProperty_NotRequiredAndNullableIntIsNull_ReturnsSuccess() + { + var validateMock = new Mock>>(); + var result = ValidateProperty( + new TestType() { NullableIntValue = null }, + t => t.NullableIntValue, + validateMock.Object, + "Some.Path", + required: false); + + result.Should().BeSuccess(); + validateMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ValidateProperty_NotRequiredAndStringIsEmpty_ReturnsSuccess(string? value) + { + var validateMock = new Mock>>(); + var result = ValidateProperty( + new TestType() { StringValue = value }, + t => t.StringValue, + validateMock.Object, + "Some.Path", + required: false); + + result.Should().BeSuccess(); + validateMock.VerifyNoOtherCalls(); + } + + [Fact] + public void ValidateProperty_RequiredAndNullableIntIsNull_ReturnsFail() + { + var result = ValidateProperty( + new TestType() { NullableIntValue = null }, + t => t.NullableIntValue, + (int _) => Success(1), + "Some.Path", + required: true); + + result.Should().BeFail().Which.Should().SatisfyRespectively( + issue => + { + issue.Member.Should().Be("Some.Path.NullableIntValue"); + issue.Message.Should().Be("The NullableIntValue is required."); + }); + } + [Theory] [InlineData(null)] [InlineData("")] From 88e99a8c8111e8a2e84b2a7a14c6df8cda12e619 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 11:08:40 +0100 Subject: [PATCH 05/10] Update build pipeline --- azure-pipelines.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bdac110..2be1fde 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,13 +10,24 @@ pool: name: 'default' steps: -- script: dotnet restore +- task: DotNetCoreCLI@2 + inputs: + command: restore + - task: DotNetCoreCLI@2 displayName: Build inputs: command: build projects: '**/*.csproj' arguments: '--configuration Release --output $(build.artifactstagingdirectory)' + +- task: DotNetCoreCLI@2 + displayName: dotnet test + inputs: + command: test + projects: '**/*Tests/*.csproj' + arguments: '--configuration Release --collect "Code coverage" --no-build' + - task: PublishBuildArtifacts@1 inputs: ArtifactName: drop From 1bc6acaafe2dc4fd62e34fd8c6bb19fef423f55a Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 11:28:01 +0100 Subject: [PATCH 06/10] Fixes --- azure-pipelines.yml | 13 +++++++++++-- .../Validations/ComplexValidations.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2be1fde..8a49db8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,6 +9,9 @@ trigger: pool: name: 'default' +variables: + buildConfiguration: 'Release' + steps: - task: DotNetCoreCLI@2 inputs: @@ -19,14 +22,20 @@ steps: inputs: command: build projects: '**/*.csproj' - arguments: '--configuration Release --output $(build.artifactstagingdirectory)' + arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: dotnet test inputs: command: test projects: '**/*Tests/*.csproj' - arguments: '--configuration Release --collect "Code coverage" --no-build' + arguments: '--configuration $(buildConfiguration) --collect "Code coverage" --no-build' + +- task: DotNetCoreCLI@2 + displayName: dotnet pack + inputs: + command: pack + arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory) --no-build' - task: PublishBuildArtifacts@1 inputs: diff --git a/src/Dbosoft.Functional/Validations/ComplexValidations.cs b/src/Dbosoft.Functional/Validations/ComplexValidations.cs index 9e014fe..a39924b 100644 --- a/src/Dbosoft.Functional/Validations/ComplexValidations.cs +++ b/src/Dbosoft.Functional/Validations/ComplexValidations.cs @@ -123,7 +123,7 @@ private static Validation ValidateList( | list.Map((index, listItem) => from li in Optional(listItem).ToValidation( new ValidationIssue($"{JoinPath(path, getList)}[{index}]", "The entry must not be null.")) - from _ in validate(listItem, $"{JoinPath(path, getList)}[{index}]") + from _ in validate(li, $"{JoinPath(path, getList)}[{index}]") select unit) .Fold(Success(unit), (acc, listItem) => acc | listItem); From baa8c2c9fcbd30bb0a87b8f291e7a1f741aeb979 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 14:00:42 +0100 Subject: [PATCH 07/10] Improve handling of null values --- .../DataTypes/ValidatingNewType.cs | 9 +++-- .../DataTypes/ValidatingNewTypeTests.cs | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs index 968b48b..4cc2bcf 100644 --- a/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs +++ b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs @@ -24,9 +24,12 @@ protected ValidatingNewType(A value) : base(value) public static Validation NewValidation(A value) => NewTry(value).Match( Succ: Success, - Fail: ex => ex is ValidationException vex - ? Fail(vex.Errors) - : Fail(Error.New(ex))); + Fail: ex => ex switch + { + ValidationException vex => Fail(vex.Errors), + ArgumentNullException _ => Fail(Error.New("The value cannot be null.")), + _ => Fail(Error.New(ex)) + }); public static Either NewEither(A value) => NewValidation(value).ToEither().MapLeft( diff --git a/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs index ea1609a..72503b5 100644 --- a/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs +++ b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs @@ -26,6 +26,15 @@ public TestTypeWithValidationException(string value) : base(value) } } + public class TestTypeWithValidationSuccess + : ValidatingNewType + { + public TestTypeWithValidationSuccess(string value) : base(value) + { + ValidOrThrow(Success(unit)); + } + } + [Fact] public void New_WithArgumentException_ThrowsArgumentException() { @@ -72,6 +81,20 @@ public void NewEither_WithValidationErrors_ReturnsErrorWithInnerError() ShouldContainValidationErrors(innerErrors); } + [Fact] + public void NewEither_WithNull_ReturnsError() + { + var result = TestTypeWithValidationSuccess.NewEither(null!); + + var error = result.Should().BeLeft().Subject; + error.Message.Should().Be("The value is not a valid TestTypeWithValidationSuccess."); + + var innerError = error.Inner.Should().BeSome().Subject; + innerError.Message.Should().Be("The value cannot be null."); + innerError.IsExceptional.Should().BeFalse(); + innerError.Exception.Should().BeNone(); + } + [Fact] public void NewValidation_ArgumentException_ReturnsErrorWithException() { @@ -96,6 +119,20 @@ public void NewValidation_WithValidationErrors_ReturnsBothErrors() ShouldContainValidationErrors(errors); } + [Fact] + public void NewValidation_WithNull_ReturnsError() + { + var result = TestTypeWithValidationSuccess.NewValidation(null!); + + result.Should().BeFail().Which.Should().SatisfyRespectively( + error => + { + error.Message.Should().Be("The value cannot be null."); + error.IsExceptional.Should().BeFalse(); + error.Exception.Should().BeNone(); + }); + } + private static void ShouldContainValidationErrors(Seq errors) { errors.Should().SatisfyRespectively( From 5e6b7866d8b38e15fc9c1cac3a5cfe9349404e9f Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 15:24:21 +0100 Subject: [PATCH 08/10] Last cleanup --- src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs | 4 +--- .../Validations/ComplexValidationsTests.cs | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs index 4cc2bcf..fb024a6 100644 --- a/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs +++ b/src/Dbosoft.Functional/DataTypes/ValidatingNewType.cs @@ -17,9 +17,7 @@ public abstract class ValidatingNewType : NewType where ORD : struct, Ord { - protected ValidatingNewType(A value) : base(value) - { - } + protected ValidatingNewType(A value) : base(value) { } public static Validation NewValidation(A value) => NewTry(value).Match( diff --git a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs index 25bea7f..0bcdf83 100644 --- a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs +++ b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs @@ -243,11 +243,7 @@ public class TestType { public string? StringValue { get; set; } - public int IntValue { get; set; } - public int? NullableIntValue { get; set; } - - public string[]? StringArray { get; set; } } public class TravelGroup From 1694c26f538e637adb0c49b125a769d9336179cb Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Wed, 6 Mar 2024 17:51:57 +0100 Subject: [PATCH 09/10] Reference a proper range of versions of LanguageExt --- src/Dbosoft.Functional/Dbosoft.Functional.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dbosoft.Functional/Dbosoft.Functional.csproj b/src/Dbosoft.Functional/Dbosoft.Functional.csproj index 463b528..122ddec 100644 --- a/src/Dbosoft.Functional/Dbosoft.Functional.csproj +++ b/src/Dbosoft.Functional/Dbosoft.Functional.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From a0df4238f1dc70a67e055badd80c77ae3c16f105 Mon Sep 17 00:00:00 2001 From: Christopher Mann Date: Thu, 7 Mar 2024 18:38:51 +0100 Subject: [PATCH 10/10] Fix incorrect formatting of ValidationIssue --- .../Validations/ValidationIssue.cs | 64 +++++++++---------- .../DataTypes/ValidatingNewTypeTests.cs | 2 + .../Validations/ComplexValidationsTests.cs | 8 +-- .../Validations/ValidationIssueTests.cs | 51 +++++++++++++++ 4 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 test/Dbosoft.Functional.Tests/Validations/ValidationIssueTests.cs diff --git a/src/Dbosoft.Functional/Validations/ValidationIssue.cs b/src/Dbosoft.Functional/Validations/ValidationIssue.cs index 2ab46e1..2f26272 100644 --- a/src/Dbosoft.Functional/Validations/ValidationIssue.cs +++ b/src/Dbosoft.Functional/Validations/ValidationIssue.cs @@ -3,44 +3,44 @@ using System.Collections.Generic; using System.Text; -namespace Dbosoft.Functional.Validations +namespace Dbosoft.Functional.Validations; + +/// +/// Represents an issue which was detected during a validation. +/// +public readonly struct ValidationIssue { + private readonly string _member; + private readonly string _message; + /// - /// Represents an issue which was detected during a validation. + /// Creates a new validation issue. /// - public readonly struct ValidationIssue + public ValidationIssue(string member, string message) { - private readonly string _member; - private readonly string _message; - - /// - /// Creates a new validation issue. - /// - public ValidationIssue(string member, string message) - { - _member = member; - _message = message; - } + _member = member; + _message = message; + } - /// - /// The path to the member in the object tree which caused the issue, - /// e.g. Participants[2].Name. - /// - public string Member => _member; + /// + /// The path to the member in the object tree which caused the issue, + /// e.g. Participants[2].Name. + /// + public string Member => _member; - /// - /// The description of the issue. - /// - public string Message => _message; + /// + /// The description of the issue. + /// + public string Message => _message; - /// - /// Converts the issue to an . - /// - public Error ToError() => Error.New(ToString()); + /// + /// Converts the issue to an . + /// + public Error ToError() => Error.New(ToString()); - /// - /// Returns a string representation of the issue. - /// - public override string ToString() => $"{_member}: {_message}"; - } + /// + /// Returns a string representation of the issue. + /// + public override string ToString() => + string.IsNullOrWhiteSpace(_member) ? _message : $"{_member}: {_message}"; } diff --git a/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs index 72503b5..a7dda4f 100644 --- a/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs +++ b/test/Dbosoft.Functional.Tests/DataTypes/ValidatingNewTypeTests.cs @@ -5,6 +5,8 @@ using static LanguageExt.Prelude; using Dbosoft.Functional.DataTypes; +namespace Dbosoft.Functional.Tests.DataTypes; + public class ValidatingNewTypeTests { public class TestTypeWithArgumentException diff --git a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs index 0bcdf83..e69bb1a 100644 --- a/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs +++ b/test/Dbosoft.Functional.Tests/Validations/ComplexValidationsTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; -using Dbosoft.Functional.Validations; +using Dbosoft.Functional.Validations; using LanguageExt; using LanguageExt.Common; using Moq; diff --git a/test/Dbosoft.Functional.Tests/Validations/ValidationIssueTests.cs b/test/Dbosoft.Functional.Tests/Validations/ValidationIssueTests.cs new file mode 100644 index 0000000..89beda5 --- /dev/null +++ b/test/Dbosoft.Functional.Tests/Validations/ValidationIssueTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dbosoft.Functional.Validations; + +namespace Dbosoft.Functional.Tests.Validations; + +public class ValidationIssueTests +{ + [Fact] + public void ToError_WitMember_ReturnsCorrectError() + { + var issue = new ValidationIssue("MyMember", "Some message"); + var error = issue.ToError(); + + error.Message.Should().Be("MyMember: Some message"); + error.Exception.Should().BeNone(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ToError_WithoutMember_ReturnsCorrectError(string? member) + { + var issue = new ValidationIssue(member, "Some message"); + var error = issue.ToError(); + + error.Message.Should().Be("Some message"); + error.Exception.Should().BeNone(); + } + + [Fact] + public void ToString_WitMember_ReturnsCorrectError() + { + var issue = new ValidationIssue("MyMember", "Some message"); + issue.ToString().Should().Be("MyMember: Some message"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ToString_WithoutMember_ReturnsCorrectError(string? member) + { + var issue = new ValidationIssue(member, "Some message"); + issue.ToString().Should().Be("Some message"); + } +}