RSCG – FunicularSwitch
| name | FunicularSwitch |
| nuget |
https://www.nuget.org/packages/FunicularSwitch.Generators/ https://www.nuget.org/packages/FunicularSwitch |
| link | https://github.com/bluehands/Funicular-Switch |
| author | bluehands |
Generating discriminated unions for C# 9.0 and above.
This is how you can use FunicularSwitch .
The code that you start with is
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FunicularSwitch" Version="5.0.1" />
<PackageReference Include="FunicularSwitch.Generators" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
The code that you will use is
using Union;
Console.WriteLine("Save or not");
var data = SaveToDatabase.Save(0);
Console.WriteLine(data.Match(
ok => true,
error => false));
data = SaveToDatabase.Save(1);
Console.WriteLine(data.Match(ok => true,error => false));
namespace Union;
[FunicularSwitch.Generators.ResultType(ErrorType = typeof(ErrorDetails))]
public abstract partial class ResultSave<T> { };
public class ErrorDetails
{
}
//[FunicularSwitch.Generators.UnionType]
//public abstract partial class ResultSave { };
//public sealed partial record Success(int Value): ResultSave;
//public sealed partial record ValidationError(string Message):ResultSave;
////public sealed partial record Ok(T Value) : ResultSave<T>;
////public sealed partial record Error(Exception Exception) : ResultSave<T>;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Union;
internal class SaveToDatabase
{
public static ResultSave<int> Save(int i)
{
if (i == 0)
{
return new ResultSave<int>.Error_(new ErrorDetails());
}
return new ResultSave<int>.Ok_(i);
}
}
The code that is generated is
using System;
// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
[AttributeUsage(AttributeTargets.Enum)]
sealed class ExtendedEnumAttribute : Attribute
{
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;
}
enum EnumCaseOrder
{
Alphabetic,
AsDeclared
}
/// <summary>
/// Generate match methods for all enums defined in assembly that contains AssemblySpecifier.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly,AllowMultiple = true)]
class ExtendEnumsAttribute : Attribute
{
public Type AssemblySpecifier { get; }
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;
public ExtendEnumsAttribute() => AssemblySpecifier = typeof(ExtendEnumsAttribute);
public ExtendEnumsAttribute(Type assemblySpecifier)
{
AssemblySpecifier = assemblySpecifier;
}
}
/// <summary>
/// Generate match methods for Type. Must be enum.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly,AllowMultiple = true)]
class ExtendEnumAttribute : Attribute
{
public Type Type { get; }
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;
public ExtendEnumAttribute(Type type)
{
Type = type;
}
}
enum ExtensionAccessibility
{
Internal,
Public
}
}
using System;
// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
/// <summary>
/// Mark an abstract partial type with a single generic argument with the ResultType attribute.
/// This type from now on has Ok | Error semantics with map and bind operations.
/// </summary>
[AttributeUsage(AttributeTargets.Class,Inherited = false)]
sealed class ResultTypeAttribute : Attribute
{
public ResultTypeAttribute() => ErrorType = typeof(string);
public ResultTypeAttribute(Type errorType) => ErrorType = errorType;
public Type ErrorType { get; set; }
}
/// <summary>
/// Mark a static method or a member method or you error type with the MergeErrorAttribute attribute.
/// Static signature: TError -> TError -> TError. Member signature: TError -> TError
/// We are now able to collect errors and methods like Validate,Aggregate,FirstOk that are useful to combine results are generated.
/// </summary>
[AttributeUsage(AttributeTargets.Method,Inherited = false)]
sealed class MergeErrorAttribute : Attribute
{
}
/// <summary>
/// Mark a static method with the ExceptionToError attribute.
/// Signature: Exception -> TError
/// This method is always called,when an exception happens in a bind operation.
/// So a call like result.Map(i => i/0) will return an Error produced by the factory method instead of throwing the DivisionByZero exception.
/// </summary>
[AttributeUsage(AttributeTargets.Method,Inherited = false)]
sealed class ExceptionToError : Attribute
{
}
}
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FunicularSwitch;
namespace Union
{
#pragma warning disable 1591
public abstract partial class ResultSave
{
public static ResultSave<T> Error<T>(ErrorDetails details) => new ResultSave<T>.Error_(details);
public static ResultSave<T> Ok<T>(T value) => new ResultSave<T>.Ok_(value);
public bool IsError => GetType().GetGenericTypeDefinition() == typeof(ResultSave<>.Error_);
public bool IsOk => !IsError;
public abstract ErrorDetails? GetErrorOrDefault();
public static ResultSave<T> Try<T>(Func<T> action,Func<Exception,ErrorDetails> formatError)
{
try
{
return action();
}
catch (Exception e)
{
return Error<T>(formatError(e));
}
}
public static async Task<ResultSave<T>> Try<T>(Func<Task<T>> action,Func<Exception,ErrorDetails> formatError)
{
try
{
return await action();
}
catch (Exception e)
{
return Error<T>(formatError(e));
}
}
}
public abstract partial class ResultSave<T> : ResultSave,IEnumerable<T>
{
public static ResultSave<T> Error(ErrorDetails message) => Error<T>(message);
public static ResultSave<T> Ok(T value) => Ok<T>(value);
public static implicit operator ResultSave<T>(T value) => ResultSave.Ok(value);
public static bool operator true(ResultSave<T> result) => result.IsOk;
public static bool operator false(ResultSave<T> result) => result.IsError;
public static bool operator !(ResultSave<T> result) => result.IsError;
//just here to suppress warning,never called because all subtypes (Ok_,Error_) implement Equals and GetHashCode
bool Equals(ResultSave<T> other) => this switch
{
Ok_ ok => ok.Equals((object)other),
Error_ error => error.Equals((object)other),
_ => throw new InvalidOperationException($"Unexpected type derived from {nameof(ResultSave<T>)}")
};
public override int GetHashCode() => this switch
{
Ok_ ok => ok.GetHashCode(),
Error_ error => error.GetHashCode(),
_ => throw new InvalidOperationException($"Unexpected type derived from {nameof(ResultSave<T>)}")
};
public override bool Equals(object? obj)
{
if (ReferenceEquals(null,obj)) return false;
if (ReferenceEquals(this,obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ResultSave<T>)obj);
}
public static bool operator ==(ResultSave<T>? left,ResultSave<T>? right) => Equals(left,right);
public static bool operator !=(ResultSave<T>? left,ResultSave<T>? right) => !Equals(left,right);
public void Match(Action<T> ok,Action<ErrorDetails>? error = null) => Match(
v =>
{
ok.Invoke(v);
return 42;
},
err =>
{
error?.Invoke(err);
return 42;
});
public T1 Match<T1>(Func<T,T1> ok,Func<ErrorDetails,T1> error)
{
return this switch
{
Ok_ okResultSave => ok(okResultSave.Value),
Error_ errorResultSave => error(errorResultSave.Details),
_ => throw new InvalidOperationException($"Unexpected derived result type: {GetType()}")
};
}
public async Task<T1> Match<T1>(Func<T,Task<T1>> ok,Func<ErrorDetails,Task<T1>> error)
{
return this switch
{
Ok_ okResultSave => await ok(okResultSave.Value).ConfigureAwait(false),
Error_ errorResultSave => await error(errorResultSave.Details).ConfigureAwait(false),
_ => throw new InvalidOperationException($"Unexpected derived result type: {GetType()}")
};
}
public Task<T1> Match<T1>(Func<T,Task<T1>> ok,Func<ErrorDetails,T1> error) =>
Match(ok,e => Task.FromResult(error(e)));
public async Task Match(Func<T,Task> ok)
{
if (this is Ok_ okResultSave) await ok(okResultSave.Value).ConfigureAwait(false);
}
public T Match(Func<ErrorDetails,T> error) => Match(v => v,error);
public ResultSave<T1> Bind<T1>(Func<T,ResultSave<T1>> bind)
{
switch (this)
{
case Ok_ ok:
try
{
return bind(ok.Value);
}
// ReSharper disable once RedundantCatchClause
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception e)
#pragma warning restore CS0168 // Variable is declared but never used
{
throw; //createGenericErrorResult
}
case Error_ error:
return error.Convert<T1>();
default:
throw new InvalidOperationException($"Unexpected derived result type: {GetType()}");
}
}
public async Task<ResultSave<T1>> Bind<T1>(Func<T,Task<ResultSave<T1>>> bind)
{
switch (this)
{
case Ok_ ok:
try
{
return await bind(ok.Value).ConfigureAwait(false);
}
// ReSharper disable once RedundantCatchClause
#pragma warning disable CS0168 // Variable is declared but never used
catch (Exception e)
#pragma warning restore CS0168 // Variable is declared but never used
{
throw; //createGenericErrorResult
}
case Error_ error:
return error.Convert<T1>();
default:
throw new InvalidOperationException($"Unexpected derived result type: {GetType()}");
}
}
public ResultSave<T1> Map<T1>(Func<T,T1> map)
=> Bind(value => Ok(map(value)));
public Task<ResultSave<T1>> Map<T1>(Func<T,Task<T1>> map)
=> Bind(async value => Ok(await map(value).ConfigureAwait(false)));
public T? GetValueOrDefault()
=> Match(
v => (T?)v,
_ => default
);
public T GetValueOrDefault(Func<T> defaultValue)
=> Match(
v => v,
_ => defaultValue()
);
public T GetValueOrDefault(T defaultValue)
=> Match(
v => v,
_ => defaultValue
);
public T GetValueOrThrow()
=> Match(
v => v,
details => throw new InvalidOperationException($"Cannot access error result value. Error: {details}"));
public IEnumerator<T> GetEnumerator() => Match(ok => new[] { ok },_ => Enumerable.Empty<T>()).GetEnumerator();
public override string ToString() => Match(ok => $"Ok {ok?.ToString()}",error => $"Error {error}");
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public sealed partial class Ok_ : ResultSave<T>
{
public T Value { get; }
public Ok_(T value) => Value = value;
public override ErrorDetails? GetErrorOrDefault() => null;
public bool Equals(Ok_? other)
{
if (ReferenceEquals(null,other)) return false;
if (ReferenceEquals(this,other)) return true;
return EqualityComparer<T>.Default.Equals(Value,other.Value);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null,obj)) return false;
if (ReferenceEquals(this,obj)) return true;
return obj is Ok_ other && Equals(other);
}
public override int GetHashCode() => Value == null ? 0 : EqualityComparer<T>.Default.GetHashCode(Value);
public static bool operator ==(Ok_ left,Ok_ right) => Equals(left,right);
public static bool operator !=(Ok_ left,Ok_ right) => !Equals(left,right);
}
public sealed partial class Error_ : ResultSave<T>
{
public ErrorDetails Details { get; }
public Error_(ErrorDetails details) => Details = details;
public ResultSave<T1>.Error_ Convert<T1>() => new ResultSave<T1>.Error_(Details);
public override ErrorDetails? GetErrorOrDefault() => Details;
public bool Equals(Error_? other)
{
if (ReferenceEquals(null,other)) return false;
if (ReferenceEquals(this,other)) return true;
return Equals(Details,other.Details);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null,obj)) return false;
if (ReferenceEquals(this,obj)) return true;
return obj is Error_ other && Equals(other);
}
public override int GetHashCode() => Details.GetHashCode();
public static bool operator ==(Error_ left,Error_ right) => Equals(left,right);
public static bool operator !=(Error_ left,Error_ right) => !Equals(left,right);
}
}
public static partial class ResultSaveExtension
{
#region bind
public static async Task<ResultSave<T1>> Bind<T,T1>(
this Task<ResultSave<T>> result,
Func<T,ResultSave<T1>> bind)
=> (await result.ConfigureAwait(false)).Bind(bind);
public static async Task<ResultSave<T1>> Bind<T,T1>(
this Task<ResultSave<T>> result,
Func<T,Task<ResultSave<T1>>> bind)
=> await (await result.ConfigureAwait(false)).Bind(bind).ConfigureAwait(false);
#endregion
#region map
public static async Task<ResultSave<T1>> Map<T,T1>(
this Task<ResultSave<T>> result,
Func<T,T1> map)
=> (await result.ConfigureAwait(false)).Map(map);
public static Task<ResultSave<T1>> Map<T,T1>(
this Task<ResultSave<T>> result,
Func<T,Task<T1>> bind)
=> Bind(result,async v => ResultSave.Ok(await bind(v).ConfigureAwait(false)));
public static ResultSave<T> MapError<T>(this ResultSave<T> result,Func<ErrorDetails,ErrorDetails> mapError) =>
result.Match(ok => ok,error => ResultSave.Error<T>(mapError(error)));
#endregion
#region match
public static async Task<T1> Match<T,T1>(
this Task<ResultSave<T>> result,
Func<T,Task<T1>> ok,
Func<ErrorDetails,Task<T1>> error)
=> await (await result.ConfigureAwait(false)).Match(ok,error).ConfigureAwait(false);
public static async Task<T1> Match<T,T1>(
this Task<ResultSave<T>> result,
Func<T,Task<T1>> ok,
Func<ErrorDetails,T1> error)
=> await (await result.ConfigureAwait(false)).Match(ok,error).ConfigureAwait(false);
public static async Task<T1> Match<T,T1>(
this Task<ResultSave<T>> result,
Func<T,T1> ok,
Func<ErrorDetails,T1> error)
=> (await result.ConfigureAwait(false)).Match(ok,error);
#endregion
public static ResultSave<T> Flatten<T>(this ResultSave<ResultSave<T>> result) => result.Bind(r => r);
public static ResultSave<T1> As<T,T1>(this ResultSave<T> result,Func<ErrorDetails> errorTIsNotT1) =>
result.Bind(r =>
{
if (r is T1 converted)
return converted;
return ResultSave.Error<T1>(errorTIsNotT1());
});
public static ResultSave<T1> As<T1>(this ResultSave<object> result,Func<ErrorDetails> errorIsNotT1) =>
result.As<object,T1>(errorIsNotT1);
#region query-expression pattern
public static ResultSave<T1> Select<T,T1>(this ResultSave<T> result,Func<T,T1> selector) => result.Map(selector);
public static Task<ResultSave<T1>> Select<T,T1>(this Task<ResultSave<T>> result,Func<T,T1> selector) => result.Map(selector);
public static ResultSave<T2> SelectMany<T,T1,T2>(this ResultSave<T> result,Func<T,ResultSave<T1>> selector,Func<T,T1,T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t,t1)));
public static Task<ResultSave<T2>> SelectMany<T,T1,T2>(this Task<ResultSave<T>> result,Func<T,Task<ResultSave<T1>>> selector,Func<T,T1,T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t,t1)));
public static Task<ResultSave<T2>> SelectMany<T,T1,T2>(this Task<ResultSave<T>> result,Func<T,ResultSave<T1>> selector,Func<T,T1,T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t,t1)));
public static Task<ResultSave<T2>> SelectMany<T,T1,T2>(this ResultSave<T> result,Func<T,Task<ResultSave<T1>>> selector,Func<T,T1,T2> resultSelector) => result.Bind(t => selector(t).Map(t1 => resultSelector(t,t1)));
#endregion
}
}
namespace Union.Extensions
{
public static partial class ResultSaveExtension
{
public static IEnumerable<T1> Choose<T,T1>(
this IEnumerable<T> items,
Func<T,ResultSave<T1>> choose,
Action<ErrorDetails> onError)
=> items
.Select(i => choose(i))
.Choose(onError);
public static IEnumerable<T> Choose<T>(
this IEnumerable<ResultSave<T>> results,
Action<ErrorDetails> onError)
=> results
.Where(r =>
r.Match(_ => true,error =>
{
onError(error);
return false;
}))
.Select(r => r.GetValueOrThrow());
public static ResultSave<T> As<T>(this object item,Func<ErrorDetails> error) =>
!(item is T t) ? ResultSave.Error<T>(error()) : t;
public static ResultSave<T> NotNull<T>(this T? item,Func<ErrorDetails> error) =>
item ?? ResultSave.Error<T>(error());
public static ResultSave<string> NotNullOrEmpty(this string? s,Func<ErrorDetails> error)
=> string.IsNullOrEmpty(s) ? ResultSave.Error<string>(error()) : s!;
public static ResultSave<string> NotNullOrWhiteSpace(this string? s,Func<ErrorDetails> error)
=> string.IsNullOrWhiteSpace(s) ? ResultSave.Error<string>(error()) : s!;
public static ResultSave<T> First<T>(this IEnumerable<T> candidates,Func<T,bool> predicate,Func<ErrorDetails> noMatch) =>
candidates
.FirstOrDefault(i => predicate(i))
.NotNull(noMatch);
}
#pragma warning restore 1591
}
using System;
// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface,Inherited = false)]
sealed class UnionTypeAttribute : Attribute
{
public CaseOrder CaseOrder { get; set; } = CaseOrder.Alphabetic;
public bool StaticFactoryMethods { get; set; } = true;
}
enum CaseOrder
{
Alphabetic,
AsDeclared,
Explicit
}
[AttributeUsage(AttributeTargets.Class,Inherited = false)]
sealed class UnionCaseAttribute : Attribute
{
public UnionCaseAttribute(int index) => Index = index;
public int Index { get; }
}
}
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/FunicularSwitch
Leave a Reply