Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helper for parsing enums from string #259

Merged
merged 4 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.9.7] - 2024-06-28

- Add helper methods to parse enums from strings.

## [1.9.6] - 2024-06-12

- Add `IEnumerable<T>` extension methods to remove LINQ dependency from generated code.
Expand Down
121 changes: 121 additions & 0 deletions Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Microsoft.Kiota.Abstractions.Helpers;
MartinM85 marked this conversation as resolved.
Show resolved Hide resolved
using Microsoft.Kiota.Abstractions.Tests.Mocks;
using Xunit;

namespace Microsoft.Kiota.Abstractions.Tests
{
public class EnumHelperTests
{
[Fact]
public void EnumGenericIsParsedIfValueIsInteger()
{
var result = EnumHelpers.GetEnumValue<TestEnum>("0");

Assert.Equal(TestEnum.First, result);
}

[Fact]
public void EnumWithFlagsGenericIsParsedIfValuesAreIntegers()
{
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("1,2");

Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, result);
}

[Fact]
public void EnumGenericIsParsedIfValueIsString()
{
var result = EnumHelpers.GetEnumValue<TestEnum>("First");

Assert.Equal(TestEnum.First, result);
}

[Fact]
public void EnumWithFlagsGenericIsParsedIfValuesAreStrings()
{
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("Value1,Value3");

Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, result);
}

[Fact]
public void EnumGenericIsParsedIfValueIsFromEnumMember()
{
var result = EnumHelpers.GetEnumValue<TestEnum>("Value_2");

Assert.Equal(TestEnum.Second, result);
}

[Fact]
public void EnumWithFlagsGenericIsParsedIfValuesAreFromEnumMember()
{
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("Value__2,Value__3");

Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, result);
}

[Fact]
public void IfEnumGenericIsNotParsedThenNullIsReturned()
{
var result = EnumHelpers.GetEnumValue<TestEnum>("Value_5");

Assert.Null(result);
}

[Fact]
public void EnumIsParsedIfValueIsInteger()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "0");

Assert.Equal(TestEnum.First, result);
}

[Fact]
public void EnumWithFlagsIsParsedIfValuesAreIntegers()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "1,2");

Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, result);
}

[Fact]
public void EnumIsParsedIfValueIsString()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "First");

Assert.Equal(TestEnum.First, result);
}

[Fact]
public void EnumWithFlagsIsParsedIfValuesAreStrings()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value1,Value3");

Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, result);
}

[Fact]
public void EnumIsParsedIfValueIsFromEnumMember()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "Value_2");

Assert.Equal(TestEnum.Second, result);
}

[Fact]
public void EnumWithFlagsIsParsedIfValuesAreFromEnumMember()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value__2,Value__3");

Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, result);
}

[Fact]
public void IfEnumIsNotParsedThenNullIsReturned()
{
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "Value_5");

Assert.Null(result);
}
}
}
7 changes: 4 additions & 3 deletions Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using System.Runtime.Serialization;

namespace Microsoft.Kiota.Abstractions.Tests.Mocks;

public enum TestEnum
{
[EnumMember(Value = "1")]
[EnumMember(Value = "Value_1")]
First,
[EnumMember(Value = "2")]
[EnumMember(Value = "Value_2")]
Second,
}
}
15 changes: 15 additions & 0 deletions Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnumWithFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Runtime.Serialization;

namespace Microsoft.Kiota.Abstractions.Tests.Mocks;

[Flags]
public enum TestEnumWithFlags
{
[EnumMember(Value = "Value__1")]
Value1 = 0x01,
[EnumMember(Value = "Value__2")]
Value2 = 0x02,
[EnumMember(Value = "Value__3")]
Value3 = 0x04
}
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ public void SetsEnumValueInQueryParameters()
// Act
testRequest.AddQueryParameters(new GetQueryParameters { DataSet = TestEnum.First });
// Assert
Assert.Equal("http://localhost/me?dataset=1", testRequest.URI.ToString());
Assert.Equal("http://localhost/me?dataset=Value_1", testRequest.URI.ToString());
}
[Fact]
public void SetsEnumValuesInQueryParameters()
Expand All @@ -567,7 +567,7 @@ public void SetsEnumValuesInQueryParameters()
// Act
testRequest.AddQueryParameters(new GetQueryParameters { DataSets = new TestEnum[] { TestEnum.First, TestEnum.Second } });
// Assert
Assert.Equal("http://localhost/me?datasets=1,2", testRequest.URI.ToString());
Assert.Equal("http://localhost/me?datasets=Value_1,Value_2", testRequest.URI.ToString());
}
[Fact]
public void SetsEnumValueInPathParameters()
Expand All @@ -581,7 +581,7 @@ public void SetsEnumValueInPathParameters()
// Act
testRequest.PathParameters.Add("dataset", TestEnum.First);
// Assert
Assert.Equal("http://localhost/1", testRequest.URI.ToString());
Assert.Equal("http://localhost/Value_1", testRequest.URI.ToString());
}
[Fact]
public void SetsEnumValuesInPathParameters()
Expand All @@ -595,7 +595,7 @@ public void SetsEnumValuesInPathParameters()
// Act
testRequest.PathParameters.Add("dataset", new TestEnum[] { TestEnum.First, TestEnum.Second });
// Assert
Assert.Equal("http://localhost/1,2", testRequest.URI.ToString());
Assert.Equal("http://localhost/Value_1,Value_2", testRequest.URI.ToString());
}


Expand Down
171 changes: 171 additions & 0 deletions src/Helpers/EnumHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System;
using System.Reflection;
using System.Runtime.Serialization;

#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif

namespace Microsoft.Kiota.Abstractions.Helpers
{
/// <summary>
/// Helper methods for enums
/// </summary>
public static class EnumHelpers
{
/// <summary>
/// Gets the enum value from the raw value
/// </summary>
/// <typeparam name="T">Enum type</typeparam>
/// <param name="rawValue">Raw value</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static T? GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string rawValue) where T : struct, Enum
#else
public static T? GetEnumValue<T>(string rawValue) where T : struct, Enum
#endif
{
if(string.IsNullOrEmpty(rawValue)) return null;

var type = typeof(T);
MartinM85 marked this conversation as resolved.
Show resolved Hide resolved
rawValue = ToEnumRawName<T>(rawValue!);
if(typeof(T).IsDefined(typeof(FlagsAttribute)))
{
ReadOnlySpan<char> valueSpan = rawValue.AsSpan();
int value = 0;
while(valueSpan.Length > 0)
{
int commaIndex = valueSpan.IndexOf(',');
ReadOnlySpan<char> valueNameSpan = commaIndex < 0 ? valueSpan : valueSpan.Slice(0, commaIndex);
valueNameSpan = ToEnumRawName<T>(valueNameSpan);
#if NET6_0_OR_GREATER
if(Enum.TryParse<T>(valueNameSpan, true, out var result))
#else
if(Enum.TryParse<T>(valueNameSpan.ToString(), true, out var result))
#endif
value |= (int)(object)result;
valueSpan = commaIndex < 0 ? ReadOnlySpan<char>.Empty : valueSpan.Slice(commaIndex + 1);
}
return (T)(object)value;
}
else
return Enum.TryParse<T>(rawValue, true, out var result) ? result : null;
}

#if NET5_0_OR_GREATER
private static string ToEnumRawName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
#else
private static string ToEnumRawName<T>(string value) where T : struct, Enum
#endif
{
if(TryGetFieldValueName(typeof(T), value, out var val))
{
value = val;
}

return value;
MartinM85 marked this conversation as resolved.
Show resolved Hide resolved
}

#if NET5_0_OR_GREATER
private static ReadOnlySpan<char> ToEnumRawName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(ReadOnlySpan<char> span) where T : struct, Enum
#else
private static ReadOnlySpan<char> ToEnumRawName<T>(ReadOnlySpan<char> span) where T : struct, Enum
#endif
{
var value = span.ToString();
if(TryGetFieldValueName(typeof(T), value, out var val))
{
value = val;
}

return value.AsSpan();
MartinM85 marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Gets the enum value from the raw value for the given type
/// </summary>
/// <param name="type">Enum type</param>
/// <param name="rawValue">Raw value</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
public static object? GetEnumValue([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type? type, string rawValue)
andrueastman marked this conversation as resolved.
Show resolved Hide resolved
#else
public static object? GetEnumValue(Type? type, string rawValue)
#endif
{
object? result;
if(type == null)
{
return null;
}
if(type.IsDefined(typeof(FlagsAttribute)))
{
int intValue = 0;
while(rawValue.Length > 0)
{
int commaIndex = rawValue.IndexOf(',');
var valueName = commaIndex < 0 ? rawValue : rawValue.Substring(0, commaIndex);
if(TryGetFieldValueName(type, valueName, out var value))
{
valueName = value;
}
#if NET5_0_OR_GREATER
if(Enum.TryParse(type, valueName, true, out var enumPartResult))
intValue |= (int)enumPartResult!;
#else
try
{
intValue |= (int)Enum.Parse(type, valueName, true);
}
catch { }
#endif

rawValue = commaIndex < 0 ? string.Empty : rawValue.Substring(commaIndex + 1);
}
result = intValue > 0 ? Enum.Parse(type, intValue.ToString(), true) : null;
}
else
{
if(TryGetFieldValueName(type, rawValue, out var value))
{
rawValue = value;
}

#if NET5_0_OR_GREATER
Enum.TryParse(type, rawValue, true, out object? enumResult);
result = enumResult;
#else
try
{
result = Enum.Parse(type, rawValue, true);
}
catch
{
result = null;
}
#endif
}
return result;


}

#if NET5_0_OR_GREATER
private static bool TryGetFieldValueName([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type, string rawValue, out string valueName)
baywet marked this conversation as resolved.
Show resolved Hide resolved
#else
private static bool TryGetFieldValueName(Type type, string rawValue, out string valueName)
#endif
{
valueName = string.Empty;
foreach(var field in type.GetFields())
{
if(field.GetCustomAttribute<EnumMemberAttribute>() is { } attr && rawValue.Equals(attr.Value, StringComparison.Ordinal))
{
valueName = field.Name;
return true;
}
}
return false;
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.Kiota.Abstractions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageProjectUrl>https://aka.ms/kiota/docs</PackageProjectUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<Deterministic>true</Deterministic>
<VersionPrefix>1.9.6</VersionPrefix>
<VersionPrefix>1.9.7</VersionPrefix>
<VersionSuffix></VersionSuffix>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<SignAssembly>false</SignAssembly>
Expand Down
Loading