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