diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b59a86..5db3e570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` extension methods to remove LINQ dependency from generated code. diff --git a/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs b/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs new file mode 100644 index 00000000..64932ad9 --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs @@ -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("0"); + + Assert.Equal(TestEnum.First, result); + } + + [Fact] + public void EnumWithFlagsGenericIsParsedIfValuesAreIntegers() + { + var result = EnumHelpers.GetEnumValue("1,2"); + + Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, result); + } + + [Fact] + public void EnumGenericIsParsedIfValueIsString() + { + var result = EnumHelpers.GetEnumValue("First"); + + Assert.Equal(TestEnum.First, result); + } + + [Fact] + public void EnumWithFlagsGenericIsParsedIfValuesAreStrings() + { + var result = EnumHelpers.GetEnumValue("Value1,Value3"); + + Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, result); + } + + [Fact] + public void EnumGenericIsParsedIfValueIsFromEnumMember() + { + var result = EnumHelpers.GetEnumValue("Value_2"); + + Assert.Equal(TestEnum.Second, result); + } + + [Fact] + public void EnumWithFlagsGenericIsParsedIfValuesAreFromEnumMember() + { + var result = EnumHelpers.GetEnumValue("Value__2,Value__3"); + + Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, result); + } + + [Fact] + public void IfEnumGenericIsNotParsedThenNullIsReturned() + { + var result = EnumHelpers.GetEnumValue("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); + } + } +} diff --git a/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs index ba4fa818..545ccb5f 100644 --- a/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs +++ b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnum.cs @@ -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, -} \ No newline at end of file +} diff --git a/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnumWithFlags.cs b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnumWithFlags.cs new file mode 100644 index 00000000..8d067cc7 --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnumWithFlags.cs @@ -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 +} \ No newline at end of file diff --git a/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs b/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs index 56604a6f..7e59e6e6 100644 --- a/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs @@ -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() @@ -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() @@ -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() @@ -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()); } diff --git a/src/Helpers/EnumHelpers.cs b/src/Helpers/EnumHelpers.cs new file mode 100644 index 00000000..d98cc3da --- /dev/null +++ b/src/Helpers/EnumHelpers.cs @@ -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 +{ + /// + /// Helper methods for enums + /// + public static class EnumHelpers + { + /// + /// Gets the enum value from the raw value + /// + /// Enum type + /// Raw value + /// +#if NET5_0_OR_GREATER + public static T? GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string rawValue) where T : struct, Enum +#else + public static T? GetEnumValue(string rawValue) where T : struct, Enum +#endif + { + if(string.IsNullOrEmpty(rawValue)) return null; + + rawValue = ToEnumRawName(rawValue!); + if(typeof(T).IsDefined(typeof(FlagsAttribute))) + { + ReadOnlySpan valueSpan = rawValue.AsSpan(); + int value = 0; + while(valueSpan.Length > 0) + { + int commaIndex = valueSpan.IndexOf(','); + ReadOnlySpan valueNameSpan = commaIndex < 0 ? valueSpan : valueSpan.Slice(0, commaIndex); + valueNameSpan = ToEnumRawName(valueNameSpan); +#if NET6_0_OR_GREATER + if(Enum.TryParse(valueNameSpan, true, out var result)) +#else + if(Enum.TryParse(valueNameSpan.ToString(), true, out var result)) +#endif + value |= (int)(object)result; + valueSpan = commaIndex < 0 ? ReadOnlySpan.Empty : valueSpan.Slice(commaIndex + 1); + } + return (T)(object)value; + } + else + return Enum.TryParse(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(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 ToEnumRawName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(ReadOnlySpan span) where T : struct, Enum +#else + private static ReadOnlySpan ToEnumRawName(ReadOnlySpan span) where T : struct, Enum +#endif + { + return TryGetFieldValueName(typeof(T), span.ToString(), out var val) ? val.AsSpan() : span; + } + + /// + /// Gets the enum value from the raw value for the given type + /// + /// Enum type + /// Raw value + /// +#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() is { } attr && rawValue.Equals(attr.Value, StringComparison.Ordinal)) + { + valueName = field.Name; + return true; + } + } + return false; + } + } +} diff --git a/src/IRequestAdapter.cs b/src/IRequestAdapter.cs index e46d8afc..0f6cfe21 100644 --- a/src/IRequestAdapter.cs +++ b/src/IRequestAdapter.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Kiota.Abstractions.Serialization; @@ -49,7 +50,11 @@ public interface IRequestAdapter /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model. +#if NET5_0_OR_GREATER + Task SendPrimitiveAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary>? errorMapping = default, CancellationToken cancellationToken = default); +#else Task SendPrimitiveAsync(RequestInformation requestInfo, Dictionary>? errorMapping = default, CancellationToken cancellationToken = default); +#endif /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. /// @@ -57,7 +62,11 @@ public interface IRequestAdapter /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model collection. +#if NET5_0_OR_GREATER + Task?> SendPrimitiveCollectionAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary>? errorMapping = default, CancellationToken cancellationToken = default); +#else Task?> SendPrimitiveCollectionAsync(RequestInformation requestInfo, Dictionary>? errorMapping = default, CancellationToken cancellationToken = default); +#endif /// /// Executes the HTTP request specified by the given RequestInformation with no return content. /// diff --git a/src/Microsoft.Kiota.Abstractions.csproj b/src/Microsoft.Kiota.Abstractions.csproj index 403fabed..5e0ee497 100644 --- a/src/Microsoft.Kiota.Abstractions.csproj +++ b/src/Microsoft.Kiota.Abstractions.csproj @@ -15,7 +15,7 @@ https://aka.ms/kiota/docs true true - 1.9.6 + 1.9.7 true false