Skip to content

Latest commit

 

History

History
527 lines (441 loc) · 16.2 KB

README.md

File metadata and controls

527 lines (441 loc) · 16.2 KB

MIT License

VariantEnum is C# Source Generator that automatically creates a Rust Enum-like record class from an Enum, where each variant can have a value.
The automatically created record class makes use of .NET 8 and C# 12 language features (Incremental Generator, ISpanParsable<T>, static abstract interfaces and Collection expressions).
This library is distributed via NuGet.

PM> Install-Package VariantEnum

How to use

If the Enum definition name ends with 'Variant', a record class is automatically created with the same name as the enum definition name without 'Variant'. Enum members can be assigned the [TVariantValueType] attribute so that they have their own specific data.. Values can be accessed as argsXXX.
For example, if an IpAddrVariant Enum is defined:

public enum IpAddrVariant : byte
{
    [VariantValueType(typeof(byte), typeof(byte), typeof(byte), typeof(byte))]
    V4,
    [VariantValueType(typeof(string))]
    V6,
    None
}

This creates IpAddr record class.

var ip = new IpAddr.V4(127, 0, 0, 1);

var value = (IpAddr)ip switch
{
    IpAddr.V4 v4 => $"{v4.args0}.{v4.args1}.{v4.args2}.{v4.args3}",
    IpAddr.V6 v6 => v6.args0,
    _ => throw new Exception(),
};

Console.WriteLine(value); // 127.0.0.1

IpAddr record class automatically generates the following APIs.

public abstract record IpAddr : ISpanFormattable, ISpanParsable<IpAddr>
{
    public sealed record V4(byte args0, byte args1, byte args2, byte args3) : IpAddr
    {
        public static V4 Default => new V4(args0: default, args1: default, args2: default, args3: default);
        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
    }

    public sealed record V6(string args0) : IpAddr
    {
        public static V6 Default => new V6(args0: default);
        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
    }

    public sealed record None : IpAddr
    {
        public static None Default => new None();
        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
    }

    // ISpanFormattable
    public abstract bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);
    public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

    // ISpanParsable<T>
    public static IpAddr Parse(ReadOnlySpan<char> s, IFormatProvider? provider = default);
    public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result);

    // IParsable<T>
    public static IpAddr Parse(string s, IFormatProvider? provider = default);
    public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result);

    public static int Count { get; } => 3;
    public static string? GetName(IpAddr t); 
    public static string[] GetNames() => [nameof(V4), nameof(V6), nameof(None)];
    public static byte GetNumericValue(IpAddr t);
    public static IpAddrVariant ConvertEnum(IpAddr t);
    public static bool TryConvertEnum([NotNullWhen(true)] IpAddr? t, [MaybeNullWhen(false)] out IpAddrVariant result);
    public static IpAddr Parse(ReadOnlySpan<char> s, bool ignoreCase, IFormatProvider? provider = default);
    public static bool TryParse(ReadOnlySpan<char> s, out IpAddr result);
    public static bool TryParse(ReadOnlySpan<char> s, bool ignoreCase, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result);
    public static bool IsDefined(ReadOnlySpan<char> s);
    public static bool IsDefined([NotNullWhen(true)] IpAddr? value);
}
Generated All Code(IpAddr.g.cs)
// <auto-generated> This .cs file is generated by VariantEnum. </auto-generated>
#nullable enable
#pragma warning disable CS0219 // The variable 'variable' is assigned but its value is never used
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8601 // Possible null reference assignment.
#pragma warning disable CS8602 // Dereference of a possibly null reference.
#pragma warning disable CS8603 // Possible null reference return.
#pragma warning disable CS8604 // Possible null reference argument for parameter.
#pragma warning disable CS8619 // Possible null reference assignment fix

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace ConsoleApp;

public abstract record IpAddr : 
    ISpanFormattable, 
    ISpanParsable<IpAddr>
{
    public sealed record V4(byte args0, byte args1, byte args2, byte args3) : IpAddr
    {
        public static V4 Default => new V4(args0: default, args1: default, args2: default, args3: default);

        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
        {
            var index = 0;
            charsWritten = 0;

            if (destination.Length < 5)
            {
                charsWritten += index;
                return false;
            }
            destination[index++] = 'V';
            destination[index++] = '4';
            destination[index++] = ' ';
            destination[index++] = '{';
            destination[index++] = ' ';
            var handler = new DefaultInterpolatedStringHandler();
            handler.AppendLiteral("args0 = ");
            handler.AppendFormatted(args0);
            handler.AppendFormatted(", " );
            handler.AppendLiteral("args1 = ");
            handler.AppendFormatted(args1);
            handler.AppendFormatted(", " );
            handler.AppendLiteral("args2 = ");
            handler.AppendFormatted(args2);
            handler.AppendFormatted(", " );
            handler.AppendLiteral("args3 = ");
            handler.AppendFormatted(args3);
            var print = handler.ToStringAndClear();
            var printSpan = print.AsSpan();

            if (destination.Length < printSpan.Length + index)
            {
                charsWritten += index;
                return false;
            }
            printSpan.CopyTo(destination.Slice(index, printSpan.Length));
            index += printSpan.Length;
            if (destination.Length < 2 + index)
            {
                charsWritten += index;
                return false;
            }
            destination[index++] = ' ';
            destination[index++] = '}';

            charsWritten = index;
            return true;
        }

    }

    public sealed record V6(string args0) : IpAddr
    {
        public static V6 Default => new V6(args0: default);

        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
        {
            var index = 0;
            charsWritten = 0;

            if (destination.Length < 5)
            {
                charsWritten += index;
                return false;
            }
            destination[index++] = 'V';
            destination[index++] = '6';
            destination[index++] = ' ';
            destination[index++] = '{';
            destination[index++] = ' ';
            var handler = new DefaultInterpolatedStringHandler();
            handler.AppendLiteral("args0 = ");
            handler.AppendFormatted(args0);
            var print = handler.ToStringAndClear();
            var printSpan = print.AsSpan();

            if (destination.Length < printSpan.Length + index)
            {
                charsWritten += index;
                return false;
            }
            printSpan.CopyTo(destination.Slice(index, printSpan.Length));
            index += printSpan.Length;
            if (destination.Length < 2 + index)
            {
                charsWritten += index;
                return false;
            }
            destination[index++] = ' ';
            destination[index++] = '}';

            charsWritten = index;
            return true;
        }

    }

    public sealed record None : IpAddr
    {
        public static None Default => new None();

        public override bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
        {
            var index = 0;
            charsWritten = 0;

            if (destination.Length < 8)
            {
                charsWritten += index;
                return false;
            }
            destination[index++] = 'N';
            destination[index++] = 'o';
            destination[index++] = 'n';
            destination[index++] = 'e';
            destination[index++] = ' ';
            destination[index++] = '{';
            destination[index++] = ' ';
            destination[index++] = '}';

            charsWritten = index;
            return true;
        }

    }


    public abstract bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);

    public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

    public static int Count => 3;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string? GetName(IpAddr ipaddr)
    {
        return ipaddr switch
        {
            V4 => nameof(V4),
            V6 => nameof(V6),
            None => nameof(None),
            _ => null
        };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string[] GetNames() => [nameof(V4), nameof(V6), nameof(None)];

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte GetNumericValue(IpAddr ipaddr)
    {
        return ipaddr switch
        {
            V4 => (byte)IpAddrVariant.V4,
            V6 => (byte)IpAddrVariant.V6,
            None => (byte)IpAddrVariant.None,
            _ => ThrowInvalidType<byte>()
        };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IpAddrVariant ConvertEnum(IpAddr ipaddr)
    {
        return ipaddr switch
        {
            V4 => IpAddrVariant.V4,
            V6 => IpAddrVariant.V6,
            None => IpAddrVariant.None,
            _ => ThrowInvalidType<IpAddrVariant>()
        };
    }

    public static bool TryConvertEnum([NotNullWhen(true)] IpAddr? ipaddr, [MaybeNullWhen(false)] out IpAddrVariant result)
    {
        switch(ipaddr)
        {
            case V4:
                result = IpAddrVariant.V4;
                return true;
            case V6:
                result = IpAddrVariant.V6;
                return true;
            case None:
                result = IpAddrVariant.None;
                return true;
        }
        result = default;
        return false;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IpAddr Parse(string s, IFormatProvider? provider = default)
    {
        return Parse(s.AsSpan(), false, provider);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IpAddr Parse(ReadOnlySpan<char> s, IFormatProvider? provider = default)
    {
        return Parse(s, false, provider);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IpAddr Parse(ReadOnlySpan<char> s, bool ignoreCase, IFormatProvider? provider = default)
    {
        if (TryParse(s, ignoreCase, provider, out var result))
        {
            return result;
        }
        else
        {
            ThrowRequestedValueNotFound(s);
            return default!;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result)
    {
        return TryParse(s.AsSpan(), false, provider, out result);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryParse(ReadOnlySpan<char> s, out IpAddr result)
    {
        return TryParse(s, false, null, out result);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result)
    {
        return TryParse(s, false, null, out result);
    }

    public static bool TryParse(ReadOnlySpan<char> s, bool ignoreCase, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result)
    {
        if (ignoreCase)
        {
            if (s.Equals(nameof(V4), StringComparison.OrdinalIgnoreCase))
            {
                result = V4.Default;
                return true;
            }
            if (s.Equals(nameof(V6), StringComparison.OrdinalIgnoreCase))
            {
                result = V6.Default;
                return true;
            }
            if (s.Equals(nameof(None), StringComparison.OrdinalIgnoreCase))
            {
                result = None.Default;
                return true;
            }

            result = default;
            return false;
        }
        else
        {
            switch (s)
            {
                case "V4":
                    result = V4.Default;
                    return true;
                case "V6":
                    result = V6.Default;
                    return true;
                case "None":
                    result = None.Default;
                    return true;
            }
            result = default;
            return false;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsDefined(ReadOnlySpan<char> s)
    {
        return s switch
        {
            "V4" => true,
            "V6" => true,
            "None" => true,
            _ => false
        };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsDefined([NotNullWhen(true)] IpAddr? value)
    {
        return value switch
        {
            V4 => true,
            V6 => true,
            None => true,
            _ => false
        };
    }

    [DoesNotReturn]
    private static void ThrowRequestedValueNotFound(ReadOnlySpan<char> s)
    {
        throw new ArgumentException($"Requested value '{s}' was not found.");
    }

    [DoesNotReturn]
    private static T ThrowInvalidType<T>()
    {
        throw new ArgumentException($"Requested value was invalid type.'");
    }
}

Ignore automatic generation

If automatic generation is to be ignored, you assign the [IgnoreVariant] attribute.

[IgnoreVariant]
public enum TestVariant
{}

Auto-generated API

Parse TryParse

var v4 = IpAddr.Parse("V4");
var v4IgnoreCase = IpAddr.Parse("v4", true);
if (IpAddr.TryParse("V4", out var v4))
{
    Console.WriteLine(v4); // V4 { args0 = 0, args1 = 0, args2 = 0, args3 = 0 }
}

if (IpAddr.TryParse("v4", true, null, out var v4IgnoreCase))
{
    Console.WriteLine(v4IgnoreCase); // V4 { args0 = 0, args1 = 0, args2 = 0, args3 = 0 }
}

Count

var count = IpAddr.Count;
Console.WriteLine(count); // 3

GetName

var name = IpAddr.GetName(new IpAddr.V4(127, 0, 0, 1));
Console.WriteLine(name); // V4

GetNames

var names = IpAddr.GetNames();
foreach (var name in names)
{
    Console.WriteLine(name);
}
// V4
// V6
// None

IsDefined

var result = IpAddr.IsDefined("V4");                            // true
var result2 = IpAddr.IsDefined(new IpAddr.V4(127, 0, 0, 1));    // true

GetNumericValue

var v4Number = IpAddr.GetNumericValue(new IpAddr.V4(127, 0, 0, 1)); // 0
var v6Number = IpAddr.GetNumericValue(new IpAddr.V6("::1"));        // 1

ConvertEnum TryConvertEnum

Get the enumeration value of the original enum from the specified member.

IpAddrVariant variantEnum = IpAddr.ConvertEnum(new IpAddr.V4(127, 0, 0, 1));
    
if (IpAddr.TryConvertEnum(new IpAddr.V4(127, 0, 0, 1), out IpAddrVariant result))
{
}

License

MIT License.