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