RSCG – FunicularSwitch
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