diff --git a/DotNetThoughts.Results.Validation.Tests/DotNetThoughts.Results.Validation.Tests.csproj b/DotNetThoughts.Results.Validation.Tests/DotNetThoughts.Results.Validation.Tests.csproj new file mode 100644 index 0000000..81d62c4 --- /dev/null +++ b/DotNetThoughts.Results.Validation.Tests/DotNetThoughts.Results.Validation.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/DotNetThoughts.Results.Validation.Tests/EnumLoaderTests.cs b/DotNetThoughts.Results.Validation.Tests/EnumLoaderTests.cs new file mode 100644 index 0000000..addd525 --- /dev/null +++ b/DotNetThoughts.Results.Validation.Tests/EnumLoaderTests.cs @@ -0,0 +1,214 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Validation.Tests; + +public class EnumLoaderTests +{ + enum Planet + { + Mercury, + Venus, + Earth, + Mars, + Jupiter, + Saturn, + Uranus, + Neptune, + Pluto + } + + [Fact] + public void Parse_ExistsAsGivenEnum_Success() + { + // Arrange + var validPlanet = "Mercury"; + + // Act + var result = EnumLoader.Parse(validPlanet); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Mercury); + } + + [Fact] + public void Parse_DoesNotExistAsGivenEnum_Error() + { + // Arrange + var invalidPlanet = "Xena"; + + // Act + var result = EnumLoader.Parse(invalidPlanet); + + // Assert + result.Success.Should().BeFalse(); + result.HasError>().Should().BeTrue(); + } + + [Fact] + public void Parse_Null_Error() + { + // Arrange + // Act + var result = EnumLoader.Parse(null); + + // Assert + result.Success.Should().BeFalse(); + result.HasError>().Should().BeTrue(); + } + + [Fact] + public void Parse_ImplicitlyNumeric_Success() + { + // Arrange + var enumFormat = Planet.Venus; + var implicitlyNumericFormat = ((int)enumFormat).ToString(); + + // Act + var result = EnumLoader.Parse(implicitlyNumericFormat); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(enumFormat); + } + + [Fact] + public void ParseAllowNull_Null_Success() + { + // Arrange + // Act + var result = EnumLoader.ParseAllowNull(null); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeNull(); + } + + [Fact] + public void ParseAllowNull_NotNull_Success() + { + // Arrange + var validPlanet = "Neptune"; + + // Act + var result = EnumLoader.ParseAllowNull(validPlanet); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Neptune); + } + + [Fact] + public void ParseAllowNull_DoesNotExistAsGivenEnum_Error() + { + // Arrange + var invalidPlanet = "Murrcurry"; + + // Act + var result = EnumLoader.ParseAllowNull(invalidPlanet); + + // Assert + result.Success.Should().BeFalse(); + result.HasError>().Should().BeTrue(); + } + + [Fact] + public void Parse_UpperCased_Error() + { + // Arrange + var invalidPlanet = "MERCURY"; + + // Act + var result = EnumLoader.Parse(invalidPlanet); + + // Assert + result.Success.Should().BeFalse(); + } + + [Fact] + public void ParseCaseInsensitive_UpperCased_Success() + { + // Arrange + var validPlanet = "MERCURY"; + + // Act + var result = EnumLoader.Parse(validPlanet, true); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Mercury); + } + + + [Fact] + public void ParseCaseInsensitive_RandomCased_Success() + { + // Arrange + var validPlanet = "MeRCuRY"; + + // Act + var result = EnumLoader.Parse(validPlanet, true); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Mercury); + } + + [Fact] + public void ParseAllowNull_UpperCased_Error() + { + // Arrange + var invalidPlanet = "MERCURY"; + + // Act + var result = EnumLoader.ParseAllowNull(invalidPlanet); + + // Assert + result.Success.Should().BeFalse(); + } + + [Fact] + public void ParseAllowNullCaseInsensitive_UpperCased_Success() + { + // Arrange + var validPlanet = "MERCURY"; + + // Act + var result = EnumLoader.ParseAllowNull(validPlanet, true); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Mercury); + } + + + [Fact] + public void ParseAllowNullCaseInsensitive_RandomCased_Success() + { + // Arrange + var validPlanet = "MeRCuRY"; + + // Act + var result = EnumLoader.ParseAllowNull(validPlanet, true); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(Planet.Mercury); + } + + [Fact] + public void ParseAllowNullCaseInsensitive_null_Success() + { + // Act + var result = EnumLoader.ParseAllowNull(null, true); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().Be(null); + } + + + +} diff --git a/DotNetThoughts.Results.Validation.Tests/GeneralValidationTests.cs b/DotNetThoughts.Results.Validation.Tests/GeneralValidationTests.cs new file mode 100644 index 0000000..f8760a9 --- /dev/null +++ b/DotNetThoughts.Results.Validation.Tests/GeneralValidationTests.cs @@ -0,0 +1,115 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Validation.Tests; + +public class GeneralValidationTests +{ + [Fact] + public void Parse_Parseable_Success() + { + // Arrange + var parseable = "4000"; + // Act + var parseResult = GeneralValidation.Parse(parseable, StringToLong); + // Assert + parseResult.Success.Should().BeTrue(); + parseResult.Value.Should().Be(long.Parse(parseable)); + } + + [Fact] + public void Parse_NotParseable_Error() + { + // Arrange + var unparseable = "fyratusen"; + // Act + var parseResult = GeneralValidation.Parse(unparseable, StringToLong); + // Assert + parseResult.Success.Should().BeFalse(); + parseResult.HasError().Should().BeTrue(); + } + + [Fact] + public void Parse_Null_Error() + { + // Arrange + // Act + var parseResult = GeneralValidation.Parse((string?)null, StringToLong); + // Assert + parseResult.Success.Should().BeFalse(); + parseResult.HasError().Should().BeTrue(); + } + + [Fact] + public void ParseAllowNull_Null_Success() + { + // Arrange + // Act + var parseResult = GeneralValidation.ParseAllowNull((string?)null, StringToValueObject); + // Assert + parseResult.Success.Should().BeTrue(); + parseResult.Value.Should().Be(null); + } + + [Fact] + public void ParseAllowNull_NotNullParseable_Success() + { + // Arrange + var parseable = "10"; + // Act + var parseResult = GeneralValidation.ParseAllowNull(parseable, StringToValueObject); + // Assert + parseResult.Success.Should().BeTrue(); + parseResult.Value.Should().Be(new SomeValueObject(long.Parse(parseable))); + } + + [Fact] + public void ParseAllowNull_NotNullNotParseable_Error() + { + // Arrange + var parseable = "tio"; + // Act + var parseResult = GeneralValidation.ParseAllowNull(parseable, StringToValueObject); + // Assert + parseResult.Success.Should().BeFalse(); + parseResult.HasError().Should().BeTrue(); + } + + [Fact] + public void ParseAllowNull_NullableStructs() + { + // Arrange + string? parseable = "2022-12-01"; + // Act + var parseResult = GeneralValidation.ParseAllowNullStruct(parseable, v => DateOnly.TryParse(v, out var result) ? result.Return() : Result.Error(new InvalidDateError())); + // Assert + parseResult.Success.Should().BeTrue(); + parseResult.Value.Should().Be(new DateOnly(2022, 12, 1)); + } + + [Fact] + public void ParseAllowNull_NullableStructs2() + { + // Arrange + string? parseable = null; + // Act + var parseResult = GeneralValidation.ParseAllowNullStruct(parseable, v => DateOnly.TryParse(v, out var result) ? result.Return() : Result.Error(new InvalidDateError())); + // Assert + parseResult.Success.Should().BeTrue(); + parseResult.Value.Should().Be(null); + } + public record UnparseableError(string? Candidate) : ErrorBase; + public static Result StringToLong(string? candidate) => + long.TryParse(candidate, out var longified) + ? longified.Return() + : Result.Error(new UnparseableError(candidate)); + public record class SomeValueObject(long Value); + public static Result StringToValueObject(string? candidate) + { + var fitsValueDataType = StringToLong(candidate); + return fitsValueDataType.Success + ? new SomeValueObject(fitsValueDataType.Value).Return() + : Result.Error(new UnparseableError(candidate)); + } +} diff --git a/DotNetThoughts.Results.Validation.Tests/InvalidDateError.cs b/DotNetThoughts.Results.Validation.Tests/InvalidDateError.cs new file mode 100644 index 0000000..54b4df2 --- /dev/null +++ b/DotNetThoughts.Results.Validation.Tests/InvalidDateError.cs @@ -0,0 +1,3 @@ +namespace DotNetThoughts.Results.Validation.Tests; + +internal record InvalidDateError : ErrorBase; \ No newline at end of file diff --git a/DotNetThoughts.Results.Validation/DotNetThoughts.Results.Validation.csproj b/DotNetThoughts.Results.Validation/DotNetThoughts.Results.Validation.csproj new file mode 100644 index 0000000..d170948 --- /dev/null +++ b/DotNetThoughts.Results.Validation/DotNetThoughts.Results.Validation.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + 1.5.7 + True + MIT + + + + + + + diff --git a/DotNetThoughts.Results.Validation/EnumLoader.cs b/DotNetThoughts.Results.Validation/EnumLoader.cs new file mode 100644 index 0000000..2aad2b4 --- /dev/null +++ b/DotNetThoughts.Results.Validation/EnumLoader.cs @@ -0,0 +1,41 @@ +namespace DotNetThoughts.Results.Validation; + +public static class EnumLoader +{ + /// + /// Tries to parse to and returns a with an if is not a valid value of . + /// Otherwise, returns a with the parsed value + /// + /// Case-sensitive + /// + public static Result Parse(string? candidate) where T : struct, Enum => + Parse(candidate, false); + + /// + /// Tries to parse to and returns a with an if is not a valid value of . + /// Otherwise, returns a with the parsed value + /// + public static Result Parse(string? candidate, bool ignoreCase) where T : struct, Enum => + Enum.TryParse(candidate, ignoreCase, out var parsed) + ? Result.Ok(parsed) + : Result.Error(new EnumValueMustExistError(candidate)); + + /// + /// Tries to parse to and returns a with a null value if is not a valid value of . + /// Otherwise, returns a with the parsed value. + /// + /// Case-sensitive + /// + public static Result ParseAllowNull(string? candidate) where T : struct, Enum => + ParseAllowNull(candidate, false); + + /// + /// Tries to parse to and returns a with a null value if is not a valid value of . + /// Otherwise, returns a with the parsed value. + /// + public static Result ParseAllowNull(string? candidate, bool ignoreCase) where T : struct, Enum => + candidate is null + ? Result.Ok(null) + : Parse(candidate, ignoreCase) + .Bind(f => Result.Ok(f)); +} diff --git a/DotNetThoughts.Results.Validation/EnumValueMustExistError.cs b/DotNetThoughts.Results.Validation/EnumValueMustExistError.cs new file mode 100644 index 0000000..3c83474 --- /dev/null +++ b/DotNetThoughts.Results.Validation/EnumValueMustExistError.cs @@ -0,0 +1,16 @@ +namespace DotNetThoughts.Results.Validation; + +/// +/// Represents an error that occurs when the value of a string cannot be parsed to a given enum. +/// +public record EnumValueMustExistError : ErrorBase where T : struct, Enum +{ + public string EnumName => typeof(T).Name; + public string[] ValidValues => Enum.GetValues().Select(x => x.ToString()).ToArray(); + public EnumValueMustExistError(string? candidate) + { + + Message = (candidate?.ToString() ?? "") + $" is not a valid {EnumName}. Valid {EnumName} alternatives: " + + string.Join(", ", ValidValues); + } +} diff --git a/DotNetThoughts.Results.Validation/GeneralValidation.cs b/DotNetThoughts.Results.Validation/GeneralValidation.cs new file mode 100644 index 0000000..efed66c --- /dev/null +++ b/DotNetThoughts.Results.Validation/GeneralValidation.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +namespace DotNetThoughts.Results.Validation; + +public static class GeneralValidation +{ + /// + /// Parses the input using the supplied function, unless it is null, in which case it returns a Result with a MissingArgumentError + /// + public static Result Parse(TInput? input, Func> parser, [CallerArgumentExpression("input")] string? argumentExpression = null) + => MissingArgumentError.IfMissing(input, argumentExpression).Bind(() => parser(input!)); + + public static Result Parse(TInput? input, TInput2? input2, + Func> parser, + [CallerArgumentExpression("input")] string? argumentExpression = null, + [CallerArgumentExpression("input2")] string? argumentExpression2 = null) + where TInput : struct + where TInput2 : struct + => Extensions.OrResult( + MissingArgumentError.IfMissing(input, argumentExpression), + MissingArgumentError.IfMissing(input, argumentExpression2)) + .Bind((_, _) => parser(input!.Value, input2!.Value)); + + /// + /// Parses the input using the supplied function, unless it is null, in which case it just return a null-valued Result + /// + public static Result ParseAllowNull(TInput? input, Func> parser) + where TInput : class + where T : class + => input is null + ? Result.Ok(null) + : parser(input) + .Bind(Result.Ok); + + /// + /// Parses the input using the supplied function, unless it is null, in which case it just return a null-valued Result + /// + public static Result ParseAllowNull(TInput? input, Func> parser) + where TInput : struct + where T : class + => input is null + ? Result.Ok(null) + : parser(input.Value) + .Bind(Result.Ok); + + /// + /// Parses the input using the supplied function, unless it is null, in which case it just return a null-valued Result + /// + public static Result ParseAllowNullStruct(TInput? input, Func> parser) + where TInput : class + where T : struct + => input is null + ? Result.Ok(null) + : parser(input).Bind(x => Result.Ok(x)); + + /// + /// Parses the input using the supplied function, unless it is null, in which case it just return a null-valued Result + /// + public static Result ParseAllowNullStruct(TInput? input, Func> parser) + where TInput : struct + where T : struct + => input is null + ? Result.Ok(null) + : parser(input.Value).Bind(x => Result.Ok(x)); + + /// + /// Parses the input using the supplied function, unless it is null, in which case it just return a Result with the given default value + /// + public static Result ParseOrDefaultOnNull(TInput? input, Func> parser, T defaultIfNull) + where T : class => input is null + ? Result.Ok(defaultIfNull) + : parser(input); + + /// + /// Returns the input if it is not null, wrapped in an ok Result. Otherwise, returns a Result with a MissingArgumentError + /// + public static Result Value(TInput? input, [CallerArgumentExpression("input")] string? argumentExpression = null) where TInput : struct + => Parse(input, x => Result.Ok(x!.Value), argumentExpression); + + /// + /// Returns the input if it is not null, wrapped in an ok Result. Otherwise, returns a Result with a MissingArgumentError + /// + public static Result Value(TInput? input, [CallerArgumentExpression("input")] string? argumentExpression = null) where TInput : class + => Parse(input, x => Result.Ok(x!), argumentExpression); + + /// + /// If the given list or any of its elements is null, returns an error Result with a MissingArgumentError. + /// Otherwise, parses each element of the list using the given function, and returns an OK result with a + /// non-null List of parsed elements. + /// + /// Short-circuits on first missing argument. + /// + public static Result> ParseEach(List? input, Func> parser, [CallerArgumentExpression("input")] string? argumentExpression = null) => + Parse(input, elements => elements.Return>().BindEach(el => Parse(el, parser)), argumentExpression); + + /// + /// If the given list or any of its elements is null, returns an error Result with a MissingArgumentError. + /// Otherwise, parses each element of the list using the given function, and returns an OK result with a + /// non-null List of parsed elements. + /// + /// Does not short-circuit + /// + public static Result> ParseAll(List? input, Func> parser, [CallerArgumentExpression("input")] string? argumentExpression = null) => + Parse(input, elements => elements.Return>().BindAll(el => Parse(el, parser)), argumentExpression); +} diff --git a/DotNetThoughts.Results.Validation/MissingArgumentError.cs b/DotNetThoughts.Results.Validation/MissingArgumentError.cs new file mode 100644 index 0000000..dc59b3e --- /dev/null +++ b/DotNetThoughts.Results.Validation/MissingArgumentError.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace DotNetThoughts.Results.Validation; + +/// +/// Represents an error that occurs when an argument is missing. +/// +public record MissingArgumentError : ErrorBase +{ + public MissingArgumentError(string? argumentExpression = null) + { + if (argumentExpression != null) + Message = $"Argument '{argumentExpression}' is missing"; + } + + /// + /// Returns a with an if is null. + /// + public static Result IfMissing(T? argument, [CallerArgumentExpression("argument")] string? argumentEpression = null) => argument is null ? UnitResult.Error(new MissingArgumentError(argumentEpression)) : UnitResult.Ok; +} diff --git a/DotNetThoughts.sln b/DotNetThoughts.sln index e065e3d..e3ab919 100644 --- a/DotNetThoughts.sln +++ b/DotNetThoughts.sln @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.Results.Anal EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.Results.Analyzer.Package", "DotNetThoughts.Results.Analyzer\DotNetThoughts.Results.Analyzer.Package\DotNetThoughts.Results.Analyzer.Package.csproj", "{792972AF-1E1D-4A92-9E5C-B73C6695AFDB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.Results.Validation", "DotNetThoughts.Results.Validation\DotNetThoughts.Results.Validation.csproj", "{EC515A1B-AA41-447F-988B-180741D08D85}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.Results.Validation.Tests", "DotNetThoughts.Results.Validation.Tests\DotNetThoughts.Results.Validation.Tests.csproj", "{2C726F29-9DFA-4EDF-90C9-85A238BCDFE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {792972AF-1E1D-4A92-9E5C-B73C6695AFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU {792972AF-1E1D-4A92-9E5C-B73C6695AFDB}.Release|Any CPU.ActiveCfg = Release|Any CPU {792972AF-1E1D-4A92-9E5C-B73C6695AFDB}.Release|Any CPU.Build.0 = Release|Any CPU + {EC515A1B-AA41-447F-988B-180741D08D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC515A1B-AA41-447F-988B-180741D08D85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC515A1B-AA41-447F-988B-180741D08D85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC515A1B-AA41-447F-988B-180741D08D85}.Release|Any CPU.Build.0 = Release|Any CPU + {2C726F29-9DFA-4EDF-90C9-85A238BCDFE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C726F29-9DFA-4EDF-90C9-85A238BCDFE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C726F29-9DFA-4EDF-90C9-85A238BCDFE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C726F29-9DFA-4EDF-90C9-85A238BCDFE6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Results.Tests/AndResultTests.cs b/Results.Tests/AndResultTests.cs new file mode 100644 index 0000000..cae1cfd --- /dev/null +++ b/Results.Tests/AndResultTests.cs @@ -0,0 +1,30 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class AndResultTests +{ + + [Fact] + public void HappyDays_And1() + { + var result = Result.Ok(1) + .And(x => Result.Ok(2)); + + result.Success.Should().BeTrue(); + result.Value.Should().Be((1, 2)); + } + + [Fact] + public void HappyDays_And2() + { + var result = Result.Ok(1) + .And(x => Result.Ok(2)) + .And((x, y) => Result.Ok(3)); + + result.Success.Should().BeTrue(); + result.Value.Should().Be((1, 2, 3)); + } +} \ No newline at end of file diff --git a/Results.Tests/BindAllTests.cs b/Results.Tests/BindAllTests.cs new file mode 100644 index 0000000..72f14ff --- /dev/null +++ b/Results.Tests/BindAllTests.cs @@ -0,0 +1,127 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class BindAllTests +{ + + [Fact] + public void Unit_Errors_DoesNotShortCircuit() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, false, true, true, false }); + var result = bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + successfulResults.Should().Be(4); + failedResults.Should().Be(2); + } + + [Fact] + public void Unit_Success() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, true, true, true, true }); + var result = bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + } + + [Fact] + public async Task TaskUnit_Errors_DoesNotShortCircuit() + { + int successfulResults = 0; + int failedResults = 0; + Func>> success = () => { successfulResults++; return UnitResult.Ok; }; + Func>> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, false, true, true, false }); + var result = await bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + successfulResults.Should().Be(4); + failedResults.Should().Be(2); + } + + [Fact] + public async Task TaskUnit_Success() + { + int successfulResults = 0; + int failedResults = 0; + Func>> success = () => { successfulResults++; return UnitResult.Ok; }; + Func>> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, true, true, true, true }); + var result = await bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + } + + [Fact] + public void T_Errors_DoesNotShortCircuit() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return Result.Ok(true); }; + Func> failure = () => { failedResults++; return Result.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, false, true, true, false }); + var result = bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + successfulResults.Should().Be(4); + failedResults.Should().Be(2); + } + + [Fact] + public void T_Success() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return Result.Ok(true); }; + Func> failure = () => { failedResults++; return Result.Error(new FakeError()); }; + var bs = Result>.Ok(new List() { true, true, true, true, true, true }); + var result = bs.BindAll(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + result.Value.Count().Should().Be(6); + } + + + [Fact] + public async Task T_Success_WithTaskResultInput_WithTaskResultOutput() + { + int successfulResults = 0; + int failedResults = 0; + Func>> success = () => { successfulResults++; return Task.FromResult(Result.Ok(true)); }; + Func>> failure = () => { failedResults++; return Task.FromResult(Result.Error(new FakeError())); }; + var bs = Task.FromResult(Result>.Ok(new List() { true, true, true, true, true, true })); + var result = await bs.BindAll(async x => + { + if (x) + { + return await success(); + } + else + { + return await failure(); + } + }); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + result.Value.Count().Should().Be(6); + } +} diff --git a/Results.Tests/BindEachTests.cs b/Results.Tests/BindEachTests.cs new file mode 100644 index 0000000..3594df0 --- /dev/null +++ b/Results.Tests/BindEachTests.cs @@ -0,0 +1,99 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class BindEachTests +{ + [Fact] + public void UnitResult_Errors_ShortCircuits() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + List bs = new List() { true, true, false, true, true, false }; + var result = bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(1); + successfulResults.Should().Be(2); + failedResults.Should().Be(1); + } + + [Fact] + public void UnitResult_Success() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + List bs = new List() { true, true, true, true, true, true }; + var result = bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + } + + [Fact] + public void TResult_Errors_ShortCircuits() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return Result.Ok(true); }; + Func> failure = () => { failedResults++; return Result.Error(new FakeError()); }; + List bs = new List() { true, true, false, true, true, false }; + var result = bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(1); + successfulResults.Should().Be(2); + failedResults.Should().Be(1); + } + + + [Fact] + public void TResult_Success() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return Result.Ok(true); }; + Func> failure = () => { failedResults++; return Result.Error(new FakeError()); }; + List bs = new List() { true, true, true, true, true, true }; + var result = bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + } + + [Fact] + public async Task UnitResult_Errors_ShortCircuits_Tasks() + { + int successfulResults = 0; + int failedResults = 0; + Func>> success = () => { successfulResults++; return Task.FromResult(UnitResult.Ok); }; + Func>> failure = () => { failedResults++; return Task.FromResult(UnitResult.Error(new FakeError())); }; + List bs = new List() { true, true, false, true, true, false }; + var result = await bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(1); + successfulResults.Should().Be(2); + failedResults.Should().Be(1); + } + + [Fact] + public async Task UnitResult_Success_Tasks() + { + int successfulResults = 0; + int failedResults = 0; + Func>> success = () => { successfulResults++; return Task.FromResult(UnitResult.Ok); }; + Func>> failure = () => { failedResults++; return Task.FromResult(UnitResult.Error(new FakeError())); }; + List bs = new List() { true, true, true, true, true, true }; + var result = await bs.Return>().BindEach(x => x ? success() : failure()); + result.Success.Should().BeTrue(); + result.Errors.Count().Should().Be(0); + successfulResults.Should().Be(6); + failedResults.Should().Be(0); + } +} \ No newline at end of file diff --git a/Results.Tests/BindTests.cs b/Results.Tests/BindTests.cs new file mode 100644 index 0000000..93676e9 --- /dev/null +++ b/Results.Tests/BindTests.cs @@ -0,0 +1,106 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class BindTests +{ + [Theory] + [InlineData(123)] + [InlineData(null)] + public void ReturnWrapsInSuccessResult(object? value) + { + value.Return().Success.Should().BeTrue(); + value.Return().Value.Should().Be(value); + } + + [Fact] + public void BindTransfersValueToLastInChain() + { + Result.Ok(new object()) + .Bind(x => Result.Ok(1)) + .Bind(x => Result.Ok(2)) + .Value.Should().Be(2); + } + + [Fact] + public void BindTransfersValueToLastInChainWhenBindingDeep() + { + Result.Ok(new object()) + .Bind(x => Result.Ok(1) + .Bind(x => Result.Ok(2))) + .Value.Should().Be(2); + } + + [Fact] + public void BindReturnsErrorIfBeginsWithError() + { + Result.Error(new FakeError()) + .Bind(x => Result.Ok(1)) + .Bind(x => Result.Ok(2)) + .Success.Should().BeFalse(); + } + [Fact] + public void BindReturnsErrorIfEndsWithError() + { + Result.Ok(new object()) + .Bind(x => Result.Ok(1)) + .Bind(x => Result.Error(new FakeError())) + .Success.Should().BeFalse(); + } + + [Fact] + public void BindReturnsErrorIfErrorInMiddle() + { + Result.Ok(new object()) + .Bind(x => Result.Error(new FakeError())) + .Bind(x => Result.Ok(2)) + .Success.Should().BeFalse(); + } + + [Fact] + public void BindPassesValueCorrectly() + { + Result.Ok(1) + .Bind(x => Result.Ok(x + 1)) + .Bind(x => Result.Ok(x + 1)) + .Value.Should().Be(3); + } + + [Fact] + public void BindPassesValueCorrectlyWhenBindingInside() + { + Result.Ok(1) + .Bind(x => Result.Ok(x + 1) + .Bind(x => Result.Ok(x + 1))) + .Value.Should().Be(3); + } + + [Fact] + public void BindWith2Tuple() + { + Result<(int, int)>.Ok((0, 10)) + .Bind((x, y) => Result<(int, int)>.Ok((x + 1, y + 1))) + .Bind((x, y) => Result<(int, int)>.Ok((x + 1, y + 1))) + .Value.Should().Be((2, 12)); + } + + [Fact] + public void BindWith3Tuple() + { + Result<(int, int, decimal)>.Ok((0, 10, 100m)) + .Bind((x, y, z) => Result<(int, int, decimal)>.Ok((x + 1, y + 1, z + 1))) + .Bind((x, y, z) => Result<(int, int, decimal)>.Ok((x + 1, y + 1, z + 1))) + .Value.Should().Be((2, 12, 102m)); + } + + [Fact] + public void BindWith4Tuple() + { + Result<(int, int, decimal, bool)>.Ok((0, 10, 100m, true)) + .Bind((x, y, z, w) => Result<(int, int, decimal, bool)>.Ok((x + 1, y + 1, z + 1, true))) + .Bind((x, y, z, w) => Result<(int, int, decimal, bool)>.Ok((x + 1, y + 1, z + 1, true))) + .Value.Should().Be((2, 12, 102m, true)); + } +} \ No newline at end of file diff --git a/Results.Tests/DotNetThoughts.Results.Tests.csproj b/Results.Tests/DotNetThoughts.Results.Tests.csproj index 37ca95c..4a6b1bf 100644 --- a/Results.Tests/DotNetThoughts.Results.Tests.csproj +++ b/Results.Tests/DotNetThoughts.Results.Tests.csproj @@ -8,9 +8,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Results.Tests/MapTests.cs b/Results.Tests/MapTests.cs new file mode 100644 index 0000000..52a2eb3 --- /dev/null +++ b/Results.Tests/MapTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class MapTests +{ + [Fact] + public void MapTransfersValueToLastInChain() + { + Result.Ok(new object()) + .Map(x => 1) + .Map(x => 2) + .Value.Should().Be(2); + } + + [Fact] + public async Task MapFromTaskTransfersValueToLastInChain() + { + (await Task.FromResult(Result.Ok(new object())) + .Map(x => 1) + .Map(x => 2)) + .Value.Should().Be(2); + } + + [Fact] + public void MapReturnsErrorIfBeginsWithError() + { + Result.Error(new FakeError()) + .Map(x => 1) + .Map(x => 2) + .Success.Should().BeFalse(); + } + [Fact] + public void MapReturnsErrorIfEndsWithError() + { + Result.Ok(new object()) + .Map(x => 1) + .Bind(x => Result.Error(new FakeError())) + .Success.Should().BeFalse(); + } + + [Fact] + public void MapReturnsErrorIfErrorInMiddle() + { + Result.Ok(new object()) + .Bind(x => Result.Error(new FakeError())) + .Map(x => 2) + .Success.Should().BeFalse(); + } + + [Fact] + public void MapPassesValueCorrectly() + { + Result.Ok(1) + .Map(x => x + 1) + .Map(x => x + 1) + .Value.Should().Be(3); + } + + [Fact] + public void MapWith2Tuple() + { + Result<(int, int)>.Ok((0, 10)) + .Map((x, y) => (x + 1, y + 1)) + .Map((x, y) => (x + 1, y + 1)) + .Value.Should().Be((2, 12)); + } + + [Fact] + public async Task MapWith2Tuple_FromTask() + { + (await Task.FromResult(Result<(int, int)>.Ok((0, 10))) + .Map((x, y) => (x + 1, y + 1)) + .Map((x, y) => (x + 1, y + 1))) + .Value.Should().Be((2, 12)); + } + + [Fact] + public void MapWith3Tuple() + { + Result<(int, int, decimal)>.Ok((0, 10, 100m)) + .Map((x, y, z) => (x + 1, y + 1, z + 1)) + .Map((x, y, z) => (x + 1, y + 1, z + 1)) + .Value.Should().Be((2, 12, 102m)); + } +} \ No newline at end of file diff --git a/Results.Tests/OrTests.cs b/Results.Tests/OrTests.cs new file mode 100644 index 0000000..c7f9bbc --- /dev/null +++ b/Results.Tests/OrTests.cs @@ -0,0 +1,132 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class OrTests +{ + private static readonly Result _unitResultError = UnitResult.Error(new FakeError()); + private static readonly Result _intResultError = Result.Error(new FakeError()); + private static readonly Task> _intResultErrorTask = Task.FromResult(Result.Error(new FakeError())); + + [Fact] + public void SuccessfulOrSuccesfulEqualsSuccesful() + { + var result = UnitResult.Ok.Or(UnitResult.Ok); + result.Success.Should().BeTrue(); + } + + [Fact] + public void SuccessfulOrFailureEqualsFailure() + { + var result = UnitResult.Ok.Or(_unitResultError); + result.Success.Should().BeFalse(); + } + + [Fact] + public void ReturnValuesShouldBeCombined() + { + var result = Result.Ok(1).Or(Result.Ok(2)); + result.Success.Should().BeTrue(); + result.Value.Should().Be((1, 2)); + } + + [Fact] + public void ErrorsShouldBeCollectedFromAllResults() + { + var result = Result.Error(new FakeError()).Or(_intResultError); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + } + + [Fact] + public void ErrorsShouldBeCollectedFromAll3Results() + { + var result = _intResultError.Or(_intResultError).Or(Result.Error(new FakeError())); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(3); + } + + [Fact] + public async Task ErrorsShouldBeCollectedFromAll2ResultTasks() + { + var result = await _intResultErrorTask + .Or(_intResultErrorTask); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + } + + [Fact] + public async Task ErrorsShouldBeCollectedFromAll2ResultTasksMixedWithNoTasks() + { + var result = await _intResultErrorTask.Or(_intResultError); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(2); + } + + [Fact] + public async Task ErrorsShouldBeCollectedFromAll3ResultTasksMixedWithNoTasks() + { + var result = await _intResultErrorTask.Or(_intResultError).Or(_intResultErrorTask); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(3); + } + + [Fact] + public void AllSuccessfulEqualsSuccesful() + { + var result = UnitResult.Ok + .Or(UnitResult.Ok) + .Or(UnitResult.Ok) + .Or(UnitResult.Ok) + .Or(UnitResult.Ok) + .Or(UnitResult.Ok) + .Or(UnitResult.Ok) + .Or(UnitResult.Ok); + result.Success.Should().BeTrue(); + } + + [Fact] + public void AllErrorsEqualsErrors() + { + var result = _unitResultError + .Or(_unitResultError) + .Or(_unitResultError) + .Or(_unitResultError) + .Or(_unitResultError) + .Or(_unitResultError) + .Or(_unitResultError) + .Or(_unitResultError); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(8); + } + + [Fact] + public void StaticOr_ErrorsShouldBeCollectedFromAll3Results() + { + var result = Extensions.OrResult( + _intResultError, + _intResultError, + Result.Error(new FakeError())); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(3); + } + + [Fact] + public void StaticOr_AllResultsAreEvaluated() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return _unitResultError; }; + var result = Extensions.OrResult( + success(), failure(), success(), failure(), + success(), failure(), success(), failure() + ); + result.Success.Should().BeFalse(); + result.Errors.Count().Should().Be(4); + successfulResults.Should().Be(4); + failedResults.Should().Be(4); + } +} \ No newline at end of file diff --git a/Results.Tests/SelectManyTests.cs b/Results.Tests/SelectManyTests.cs new file mode 100644 index 0000000..62c8f8f --- /dev/null +++ b/Results.Tests/SelectManyTests.cs @@ -0,0 +1,76 @@ +using FluentAssertions; + +using Xunit; + +namespace DotNetThoughts.Results.Tests; + +public class SelectManyTests +{ + [Fact] + public void SelectMany_ThreeSuccessful() + { + var result = + from a in UnitResult.Ok + from b in Result.Ok(2) + from c in Result.Ok("c") + select (a, b, c); + result.Success.Should().BeTrue(); + result.Value.Should().Be((Unit.Instance, 2, "c")); + } + + [Fact] + public void SelectMany_OneFailure() + { + var result = + from a in UnitResult.Ok + from b in Result.Ok(2) + from c in Result.Error(new FakeError()) + select (a, b, c); + result.Success.Should().BeFalse(); + result.HasError().Should().BeTrue(); + } + + [Fact] + public void SelectMany_FirstFailureShortCircuits() + { + int successfulResults = 0; + int failedResults = 0; + Func> success = () => { successfulResults++; return UnitResult.Ok; }; + Func> failure = () => { failedResults++; return UnitResult.Error(new FakeError()); }; + + var result = + from a in success() + from b in failure() + from c in success() + select (a, b, c); + result.Success.Should().BeFalse(); + result.HasError().Should().BeTrue(); + successfulResults.Should().Be(1); + failedResults.Should().Be(1); + } + + [Fact] + public async Task SelectManyTasks_ThreeSuccessful() + { + var result = await + (from a in Task.FromResult(UnitResult.Ok) + from b in Result.Ok(2) + from c in Task.FromResult(Result.Ok("c")) + select (a, b, c)); + result.Success.Should().BeTrue(); + result.Value.Should().Be((Unit.Instance, 2, "c")); + } + + [Fact] + public async Task SelectManyTasks2_ThreeSuccessful() + { + var result = await + (from a in UnitResult.Ok + from b in Task.FromResult(Result.Ok(2)) + from c in Task.FromResult(Result.Ok("c")) + select (a, b, c)); + result.Success.Should().BeTrue(); + result.Value.Should().Be((Unit.Instance, 2, "c")); + } + +}