Skip to content

Instantly share code, notes, and snippets.

@wgross
Created February 13, 2023 14:38
Show Gist options
  • Save wgross/1dd8148fb84e671f11b85bea506d2cac to your computer and use it in GitHub Desktop.
Save wgross/1dd8148fb84e671f11b85bea506d2cac to your computer and use it in GitHub Desktop.
POC for a rust like return
[Fact]
void Return_result()
{
Result Function() => Result.Ok();
Assert.True(Function() is Ok);
}
[Fact]
void Return_result_value()
{
Result<int> Function() => Result<int>.Ok(10);
Assert.Equal(10,Function().Value);
Assert.True(Function() is Result<int> { Value: 10 });
Assert.True(Function() is Ok<int> { Value: 10 });
}
[Fact]
void Return_result_has_value_on_success()
{
Result<int> Function() => Result<int>.Ok(10);
Assert.True(Function().HasValue);
}
[Fact]
void Return_result_on_error()
{
Result Function() => Result.Error("fail");
Assert.True(Function() is Error { Reason.Message: "fail" });
}
[Fact]
void Return_result_hasnt_value_on_error()
{
Result<int> Function() => Result<int>.Error("fail");
Assert.False(Function().HasValue);
Assert.True(Function() is Error<int> {Reason.Message:"fail"});
}
[Fact]
void Throw_on_error_value_from_string()
{
Result<int> Function() => Result<int>.Error("fail");
var result = Assert.Throws<InvalidOperationException>(() => Function().Value);
Assert.Equal("There is no value available: see inner exeception", result.Message);
Assert.Equal("fail", result.InnerException.Message);
Assert.NotNull(result.InnerException.Data["CallerMemberNameAttribute"]);
Assert.NotNull(result.InnerException.Data["CallerLineNumberAttribute"]);
Assert.NotNull(result.InnerException.Data["StackTrace"]);
Assert.True(Function() is Error<int> { Reason: Exception {Message:"fail"} });
}
[Fact]
void Throw_on_error_value_from_exception()
{
Result<int> Function() => Result<int>.Error(new InvalidOperationException("fail"));
var result = Assert.Throws<InvalidOperationException>(() => Function().Value);
Assert.Equal("There is no value available: see inner exeception", result.Message);
Assert.Equal("fail", result.InnerException.Message);
Assert.NotNull(result.InnerException.Data["CallerMemberNameAttribute"]);
Assert.NotNull(result.InnerException.Data["CallerLineNumberAttribute"]);
Assert.NotNull(result.InnerException.Data["StackTrace"]);
Assert.True(Function() is Error<int> { Reason: InvalidOperationException {Message:"fail"} });
}
[Fact]
void Check_for_error_from_string_1()
{
Result Function() => Result.Error(new InvalidOperationException("fail"));
Assert.True(Function() is Error { Reason: InvalidOperationException {Message:"fail"} });
}
[Fact]
void Check_for_error_from_string_2()
{
Result<int> Function() => Result<int>.Error("fail");
var result = Assert.Throws<InvalidOperationException>(() => Function().Value);
Assert.Equal("There is no value available: see inner exeception", result.Message);
Assert.Equal("fail", result.InnerException.Message);
Assert.NotNull(result.InnerException.Data["CallerMemberNameAttribute"]);
Assert.NotNull(result.InnerException.Data["CallerLineNumberAttribute"]);
Assert.NotNull(result.InnerException.Data["StackTrace"]);
}
[Fact]
void Check_for_error_from_exception()
{
Result<int> Function() => Result<int>.Error(new InvalidOperationException("fail"));
var result = Assert.Throws<InvalidOperationException>(() => Function().Value);
Assert.Equal("There is no value available: see inner exeception", result.Message);
Assert.Equal("fail",result.InnerException.Message);
Assert.NotNull(result.InnerException.Data["CallerMemberNameAttribute"]);
Assert.NotNull(result.InnerException.Data["CallerLineNumberAttribute"]);
Assert.NotNull(result.InnerException.Data["StackTrace"]);
}
[Fact]
void Check_for_success()
{
Result<int> Function() => Result<int>.Ok(10);
Assert.True(Function() is Ok<int>);
Assert.Equal(10, Function() is Ok<int> ok ? ok.Value : 0);
}
[Fact]
void Switch_on_success()
{
Result<int> Function() => Result<int>.Ok(10);
switch (Function())
{
case Ok<int> ok:
Assert.True(true);
break;
default:
Assert.True(false);
break;
}
}
[Fact]
void Switch_expr_success()
{
Result<int> Function() => Result<int>.Ok(10);
Assert.True(Function() switch
{
Ok<int> ok => true,
_ => false
});
}
[Fact]
void Match_success()
{
Result<int> Function() => Result<int>.Ok(10);
Assert.True(Function().Match(onOk:_ => true, onError:(e => false)));
}
[Fact]
void Match_error()
{
Result<int> Function() => Result<int>.Error("fail");
Assert.False(Function().Match(onOk: _ => true, onError: e =>
{
Assert.Equal("fail", e.Message);
Assert.NotNull(e.Data["CallerMemberNameAttribute"]);
Assert.NotNull(e.Data["CallerLineNumberAttribute"]);
Assert.NotNull(e.Data["StackTrace"]);
return false;
}));
}
public static class ExceptionExtensions
{
public static Exception WithData(this Exception exception, string key, object value)
{
exception.Data[key??"<null>"] = value;
return exception;
}
}
public interface Result
{
public static Ok Ok() => new Ok();
public static Error Error(
string reason,
[CallerMemberName] string callerMemberName = "",
[CallerLineNumber] int callerLineNumber = 0)
{
return Error(new Exception(reason), callerMemberName, callerLineNumber);
}
public static Error Error(
Exception reason,
[CallerMemberName] string callerMemberName = "",
[CallerLineNumber] int callerLineNumber = 0)
{
return new Error
{
Reason = reason
.WithData(nameof(CallerMemberNameAttribute), callerMemberName)
.WithData(nameof(CallerLineNumberAttribute), callerLineNumber)
.WithData(nameof(Environment.StackTrace), EnhancedStackTrace.Current().ToString())
};
}
}
/// <summary>
/// Defines the interface of an result. Every result has a property <see cref="Value"/> which either returns
/// the result value or throws an error if invoked unchecked.
/// </summary>
public interface Result<T> : Result
{
public static Ok<T> Ok(T value) => new Ok<T> { Value = value };
public static Error<T> Error(
string reason,
[CallerMemberName] string callerMemberName = "",
[CallerLineNumber] int callerLineNumber = 0)
{
return Error(new Exception(reason), callerMemberName, callerLineNumber);
}
public static Error<T> Error(
Exception reason,
[CallerMemberName] string callerMemberName = "",
[CallerLineNumber] int callerLineNumber = 0)
{
return new Error<T>
{
Reason = reason
.WithData(nameof(CallerMemberNameAttribute), callerMemberName)
.WithData(nameof(CallerLineNumberAttribute), callerLineNumber)
.WithData(nameof(Environment.StackTrace), EnhancedStackTrace.Current().ToString())
};
}
public T Value { get; }
/// <summary>
/// Use to inslect OK or error with conversion
/// </summary>
public bool HasValue => this is Ok<T>;
/// <summary>
/// Chain a processing action for success or failure
/// </summary>
public R Match<R>(Func<T,R> onOk,Func<Exception,R> onError) => this is Ok<T> ok
? onOk(ok.Value)
: onError(((Error<T>)this).Reason);
}
public static class ResultExtension
{
public static R Match<R, T>(this Result<T> result, Func<T, R> onOk = null, Func<T, R> onError = null ) => result switch
{
Ok<T> ok => onOk.Invoke(ok.Value),
_ => default
};
}
public readonly struct Ok : Result
{
}
public readonly struct Ok<T> : Result<T>
{
public T Value { get; init; }
}
public readonly struct Error : Result
{
public Exception Reason { get; init; }
}
public readonly struct Error<T> : Result<T>
{
public Exception Reason { get; init; }
public T Value
{
// the the reason as an inner exceptions. B yths you have the stack trace of the error an an inner exception
// and the stack trace where the error was 'discovered'
get => throw new InvalidOperationException("There is no value available: see inner exeception", this.Reason);
init => throw new NotImplementedException();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment