Skip to content

Commit

Permalink
Merge pull request #259 from MartinM85/feature/258-enum-helper
Browse files Browse the repository at this point in the history
Helper for parsing enums from string
  • Loading branch information
andrueastman authored Jul 5, 2024
2 parents 94e2130 + 765aa7f commit 95bc067
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 8 deletions.
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-07-04

- 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;
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
}
8 changes: 4 additions & 4 deletions Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs
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
159 changes: 159 additions & 0 deletions src/Helpers/EnumHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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

Check warning on line 25 in src/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

All 'GetEnumValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
#endif
{
if(string.IsNullOrEmpty(rawValue)) return null;

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
{
return TryGetFieldValueName(typeof(T), value, out var val) ? val : value;
}

#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
{
return TryGetFieldValueName(typeof(T), span.ToString(), out var val) ? val.AsSpan() : span;
}

/// <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)
#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)
#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;
}
}
}
9 changes: 9 additions & 0 deletions src/IRequestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Serialization;
Expand Down Expand Up @@ -49,15 +50,23 @@ public interface IRequestAdapter
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized primitive response model.</returns>
#if NET5_0_OR_GREATER
Task<ModelType?> SendPrimitiveAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
#else
Task<ModelType?> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
#endif
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection.
/// </summary>
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
/// <returns>The deserialized primitive response model collection.</returns>
#if NET5_0_OR_GREATER
Task<IEnumerable<ModelType>?> SendPrimitiveCollectionAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
#else
Task<IEnumerable<ModelType>?> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
#endif
/// <summary>
/// Executes the HTTP request specified by the given RequestInformation with no return content.
/// </summary>
Expand Down
Loading

0 comments on commit 95bc067

Please sign in to comment.