Skip to content

Commit

Permalink
Add some logical chaining methods for fallible types. (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
tvandinther authored Mar 23, 2022
1 parent d6b0454 commit c3638d6
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 36 deletions.
142 changes: 142 additions & 0 deletions Fallible.Tests/FallibleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,146 @@ public void ToContravariant_ResolvesContravariantAssignment()
}

#endregion

#region Logical Chaining

private Fallible<int> WillFail() => new Error("Failed");
private Fallible<int> WillSucceed() => 42;

[Fact]
public void Or_ChainsFailedFallibles()
{
const int expectedValue = 2;

var (value, error) = Fallible.If(WillFail).Or(() => FallibleOperation(expectedValue, false));

Assert.Equal(expectedValue, value);
}

[Fact]
public void Or_ReturnsFirstSuccessfulFallible()
{
const int expectedValue = 2;

var (value, error) = Fallible.If(() => FallibleOperation(expectedValue, false)).Or(() => FallibleOperation(2, true));

Assert.Equal(expectedValue, value);
}

[Fact]
public void And_ReturnsFirstFailedFallible()
{
var callCount = 0;

var (_, error) = Fallible.If(() => FallibleOperation(callCount++, true)).And(() => FallibleOperation(callCount++, false));

Assert.True(error);
Assert.Equal(1, callCount);
}

[Fact]
public void And_ExecutesBothFallibleOperations()
{
var callCount = 0;

Fallible.If(() => FallibleOperation(callCount++, false)).And(() => FallibleOperation(callCount++, false));

Assert.Equal(2, callCount);
}

[Fact]
public void And_CanChainDifferentFallibleTypes()
{
Fallible.If(() => FallibleOperation(2, false)).And(() => FallibleOperation("3", false));
}

[Fact]
public void If_BooleanOverload_ReturnsNoError_WhenTrue()
{
var (value, error) = Fallible.If(true).Then(() => 42);

Assert.Equal(42, value);
Assert.False(error);
}

[Fact]
public void If_BooleanOverload_ReturnsError_WhenFalse()
{
var (_, error) = Fallible.If(false).Then(() => 42);

Assert.True(error);
}

[Fact]
public void AndIf_ReturnsNoError_WhenTrue()
{
const int expectedValue = 42;

var (value, error) = Fallible.
If(() => FallibleOperation(expectedValue, false)).
AndIf(x => x == expectedValue);

Assert.Equal(expectedValue, value);
Assert.False(error);
}

[Fact]
public void AndIf_ReturnsError_WhenFalse()
{
var (_, error) = Fallible.
If(WillSucceed).
AndIf(x => x == x + 2);

Assert.True(error);
}

[Fact]
public void AndIf_ReturnsError_WhenChainedOnError()
{
var (_, error) = Fallible.
If(WillFail).
AndIf(x => x == x + 2);

Assert.True(error);
}

[Fact]
public void OrIf_ReturnsNoError_WhenTrue()
{
var callCount = 0;

var (_, error) = Fallible.
If(WillFail).
OrIf(true)
.Then(_ => callCount++);

Assert.Equal(1, callCount);
Assert.False(error);
}

[Fact]
public void OrIf_ReturnsError_WhenFalse()
{
var callCount = 0;

var (_, error) = Fallible.
If(WillFail).
OrIf(false)
.Then(_ => callCount++);

Assert.Equal(0, callCount);
Assert.True(error);
}

[Fact]
public void OrIf_ReturnsError_WhenChainedOnError()
{
var (_, error) = Fallible.
If(WillFail).
OrIf(false);

Assert.True(error);
}

#endregion
}
2 changes: 1 addition & 1 deletion Fallible/Fallible.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageVersion>0.2.3</PackageVersion>
<PackageVersion>0.2.4</PackageVersion>
<Title>Fallible</Title>
<Authors>Tom van Dinther</Authors>
<Description>An idiomatic way to explicitly define, propagate and handle error states in C#. This library is inspired by Go's errors.</Description>
Expand Down
137 changes: 117 additions & 20 deletions Fallible/FallibleStatic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class Fallible
/// <summary>
/// Will execute an operation and try to catch any exceptions and returning an error if caught.
/// </summary>
/// <param name="try">The operation to execute.</param>
/// <param name="try">An operation to execute.</param>
/// <typeparam name="TResult">The type of the fallible being returned.</typeparam>
/// <returns>A fallible result.</returns>
public static Fallible<TResult> Try<TResult>(Func<TResult> @try, [CallerArgumentExpression("try")] string expression = "")
Expand All @@ -36,7 +36,7 @@ public static Fallible<TResult> Try<TResult>(Func<TResult> @try, [CallerArgument
/// Will execute a fallible operation and return the result. If the operation fails, the errorMessage will be
/// prepended.
/// </summary>
/// <param name="try">The expression to execute.</param>
/// <param name="try">An expression to execute.</param>
/// <param name="errorMessage">An optional message to prepend to the error on failure.</param>
/// <typeparam name="TResult">The type of the fallible being returned.</typeparam>
/// <returns>A fallible result.</returns>
Expand All @@ -48,87 +48,184 @@ public static Fallible<TResult> Try<TResult>(Func<Fallible<TResult>> @try, strin

return value;
}

/// <summary>
/// Allows for chaining of non-fallible operations.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="then">The expression to be chained if there is no error.</param>
/// <param name="errorMessage">An optional message to prepend to the error on failure.</param>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
/// <param name="then">An expression to be chained if there is no error.</param>
/// <typeparam name="TResult">The type of the fallible being returned.</typeparam>
/// <returns>A fallible result.</returns>
/// <remarks>If any of the operations fail, the chain stays in an error state.</remarks>
public static Fallible<TResult> Then<TResult>(this Fallible<Void> fallible, Func<TResult> then)
{
var (_, error) = fallible;
if (error) return error;

return then();
}

/// <inheritdoc cref="Then{TResult}"/>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
public static Fallible<TResult> Then<TIn, TResult>(this Fallible<TIn> fallible, Func<TIn, TResult> then)
{
var (value, error) = fallible;
if (error) return error;

return then(value);
}

/// <summary>
/// Allows for chaining of fallible operations.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="then">The expression to be chained if there is no error.</param>
/// <param name="errorMessage">An optional message to prepend to the error on failure.</param>
/// <param name="then">An expression to be chained if there is no error.</param>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
/// <typeparam name="TResult">The type of the fallible being returned.</typeparam>
/// <returns>A fallible result.</returns>
/// <remarks>If any of the operations fail, the chain stays in an error state.</remarks>
public static Fallible<TResult> Then<TIn, TResult>(this Fallible<TIn> fallible, Func<TIn, Fallible<TResult>> then)
{
var (value, error) = fallible;
if (error) return error;

return then(value);
return fallible.Error ? fallible.Error : then(fallible.Value);
}

/// <summary>
/// Allows for chaining of expressions that are executed if the chain is in a failure state. Chains of OnFail will
/// continue to execute until exhausted.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="onFail">The expression to be chained if there is an error.</param>
/// <param name="onFail">An expression to be chained if there is an error.</param>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
/// <returns>The original fallible result.</returns>
/// <remarks>This will propagate a modified error object. Useful for modifying error messages.</remarks>
public static Fallible<TIn> OnFail<TIn>(this Fallible<TIn> fallible, Func<Error, Fallible<TIn>> onFail)
{
var (_, error) = fallible;
return error ? onFail(error) : fallible;
return fallible.Error ? onFail(fallible.Error) : fallible;
}

/// <summary>
/// Allows for chaining of expressions that are executed if the chain is in a failure state. Chains of OnFail will
/// continue to execute until exhausted.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="onFail">The expression to be chained if there is an error.</param>
/// <param name="onFail">An expression to be chained if there is an error.</param>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
/// <returns>The original fallible result.</returns>
public static Fallible<TIn> OnFail<TIn>(this Fallible<TIn> fallible, Action<Error> onFail)
{
var (_, error) = fallible;
if (error) onFail(error);
if (fallible.Error) onFail(fallible.Error);

return fallible;
}

/// <summary>
/// Same as <see cref="Try{TResult}(System.Func{TResult},string)"/> without a parameter for prepending the error
/// message. Used to start a logical chain of fallible operations.
/// </summary>
/// <param name="func">A fallible operation.</param>
/// <typeparam name="T">The type of the fallible being returned.</typeparam>
/// <returns>A fallible result.</returns>
public static Fallible<T> If<T>(Func<Fallible<T>> func) => func();

/// <summary>
/// Creates a fallible result from an expression.
/// </summary>
/// <param name="expression">An expression to evaluate.</param>
/// <returns>A fallible in error state if the expression is false.</returns>
public static Fallible<Void> If(bool expression, [CallerArgumentExpression("expression")] string callerExpression = "")
{
return expression ? Return : new Error($"{callerExpression} was false");
}

/// <summary>
/// Will evaluate the expression if the chain is in a failed state.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="expression">An expression to evaluate.</param>
/// <typeparam name="T">The type of the fallible being returned.</typeparam>
/// <returns>A fallible in error state if the expression is false.</returns>
public static Fallible<T> OrIf<T>(this Fallible<T> fallible, bool expression, [CallerArgumentExpression("expression")] string callerExpression = "")
{
var (value, error) = fallible;

if (error) return expression ? value : new Error($"{callerExpression} was false");

return fallible;
}

/// <summary>
/// Will evaluate the expression if the chain is in a succeeded state.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="expression">An expression to evaluate.</param>
/// <typeparam name="T">The type of the fallible being returned.</typeparam>
/// <returns>A fallible in error state if the expression is false.</returns>
public static Fallible<T> AndIf<T>(this Fallible<T> fallible, bool expression, [CallerArgumentExpression("expression")] string callerExpression = "")
{
var (value, error) = fallible;
if (error) return error;

return expression ? fallible : new Error($"{callerExpression} was false");
}

/// <summary>
/// Will evaluate the expression if the chain is in a succeeded state.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="expressionFunc">The expression function to evaluate the value against.</param>
/// <typeparam name="T">The type of the fallible being returned.</typeparam>
/// <returns>A fallible in error state if the expression is false.</returns>
/// <remarks>Receives the value from the chain.</remarks>
public static Fallible<T> AndIf<T>(this Fallible<T> fallible, Func<T, bool> expressionFunc, [CallerArgumentExpression("expressionFunc")] string callerExpression = "")
{
var (value, error) = fallible;
if (error) return error;

return expressionFunc(value) ? fallible : new Error($"{callerExpression} was false");
}

/// <summary>
/// Chains fallible operations, short-circuiting the evaluation of the second operation if the first operation succeeds.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="func">A fallible operation.</param>
/// <typeparam name="T">The type of the fallible being returned.</typeparam>
/// <returns>The fallible returned from the operation.</returns>
public static Fallible<T> Or<T>(this Fallible<T> fallible, Func<Fallible<T>> func)
{
return fallible.Error ? func() : fallible;
}

/// <summary>
/// Chains fallible operations, short-circuiting the evaluation of the second operation if the first operation fails.
/// </summary>
/// <param name="fallible">The fallible being chained.</param>
/// <param name="func">A fallible operation.</param>
/// <typeparam name="TIn">The type of the fallible being chained.</typeparam>
/// <typeparam name="TOut">The type of the fallible being returned.</typeparam>
/// <returns>The fallible returned from the operation.</returns>
public static Fallible<TOut> And<TIn, TOut>(this Fallible<TIn> fallible, Func<Fallible<TOut>> func)
{
return fallible.Error ? fallible.Error : func();
}

#region Covariance and Contravariance

public static Fallible<TCovariant> ToCovariant<T, TCovariant>(this Fallible<T> fallible)
where T : TCovariant
{
var (value, error) = fallible;

return error ? error : value;
}

public static Fallible<TContravariant> ToContravariant<T, TContravariant>(this Fallible<T> fallible)
where TContravariant : T
{
var (value, error) = fallible;

return error ? error : (TContravariant) value!;
}

#endregion
}
Loading

0 comments on commit c3638d6

Please sign in to comment.