diff --git a/README.md b/README.md index 36ea743..52b88be 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,518 @@ [VariantEnum](https://github.com/prozolic/VariantEnum) === + +[![MIT License](https://img.shields.io/github/license/prozolic/Utf8StringSplitter)](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`, static abstract interfaces and Collection expressions). +This library is distributed via NuGet. + +> PM> Install-Package [VariantEnum](https://www.nuget.org/packages/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: + +```csharp +public enum IpAddrVariant : byte +{ + [VariantValueType(typeof(byte), typeof(byte), typeof(byte), typeof(byte))] + V4, + [VariantValueType(typeof(string))] + V6, + None +} +``` + +This creates `IpAddr` record class. + +```csharp +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. + +```csharp +public abstract record IpAddr : ISpanFormattable, ISpanParsable +{ + 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 destination, out int charsWritten, ReadOnlySpan 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 destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null); + } + + public sealed record None : IpAddr + { + public static None Default => new None(); + public override bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null); + } + + // ISpanFormattable + public abstract bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null); + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + // ISpanParsable + public static IpAddr Parse(ReadOnlySpan s, IFormatProvider? provider = default); + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result); + + // IParsable + 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 s, bool ignoreCase, IFormatProvider? provider = default); + public static bool TryParse(ReadOnlySpan s, out IpAddr result); + public static bool TryParse(ReadOnlySpan s, bool ignoreCase, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result); + public static bool IsDefined(ReadOnlySpan s); + public static bool IsDefined([NotNullWhen(true)] IpAddr? value); +} +``` + +
Generated All Code(IpAddr.g.cs) + +```csharp + +// This .cs file is generated by VariantEnum. +#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 +{ + 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 destination, out int charsWritten, ReadOnlySpan 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 destination, out int charsWritten, ReadOnlySpan 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 destination, out int charsWritten, ReadOnlySpan 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 destination, out int charsWritten, ReadOnlySpan 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() + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IpAddrVariant ConvertEnum(IpAddr ipaddr) + { + return ipaddr switch + { + V4 => IpAddrVariant.V4, + V6 => IpAddrVariant.V6, + None => IpAddrVariant.None, + _ => ThrowInvalidType() + }; + } + + 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 s, IFormatProvider? provider = default) + { + return Parse(s, false, provider); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IpAddr Parse(ReadOnlySpan 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 s, out IpAddr result) + { + return TryParse(s, false, null, out result); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out IpAddr result) + { + return TryParse(s, false, null, out result); + } + + public static bool TryParse(ReadOnlySpan 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 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 s) + { + throw new ArgumentException($"Requested value '{s}' was not found."); + } + + [DoesNotReturn] + private static T ThrowInvalidType() + { + throw new ArgumentException($"Requested value was invalid type.'"); + } +} +``` + +
+ +Auto-generated API +--- + +### Parse TryParse + +```csharp +var v4 = IpAddr.Parse("V4"); +var v4IgnoreCase = IpAddr.Parse("v4", true); +``` + +```csharp +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 + +```csharp +var count = IpAddr.Count; +Console.WriteLine(count); // 3 +``` + +### GetName + +```csharp +var name = IpAddr.GetName(new IpAddr.V4(127, 0, 0, 1)); +Console.WriteLine(name); // V4 +``` + +### GetNames + +```csharp +var names = IpAddr.GetNames(); +foreach (var name in names) +{ + Console.WriteLine(name); +} +// V4 +// V6 +// None +``` + +### IsDefined + +```csharp +var result = IpAddr.IsDefined("V4"); // true +var result2 = IpAddr.IsDefined(new IpAddr.V4(127, 0, 0, 1)); // true +``` + +### GetNumericValue + +```csharp +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. + +```csharp +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.