From a53a6f6dd517d33c71184c5a4766caca8dd2e977 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Thu, 27 Jun 2024 13:49:08 +0200 Subject: [PATCH 1/4] Helper for parsing enums from string --- CHANGELOG.md | 4 + .../EnumHelperTests.cs | 121 +++++++++++++ .../Mocks/TestEnum.cs | 7 +- .../Mocks/TestEnumWithFlags.cs | 15 ++ .../RequestInformationTests.cs | 8 +- src/Helpers/EnumHelpers.cs | 171 ++++++++++++++++++ src/Microsoft.Kiota.Abstractions.csproj | 2 +- 7 files changed, 320 insertions(+), 8 deletions(-) create mode 100644 Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs create mode 100644 Microsoft.Kiota.Abstractions.Tests/Mocks/TestEnumWithFlags.cs create mode 100644 src/Helpers/EnumHelpers.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b59a86..99c74bfd 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-06-28 + +- 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..94327263 --- /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 EnumGenericIsParsedIfValuesAreIntegers() + { + 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 EnumGenericIsParsedIfValuesAreStrings() + { + 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 EnumGenericIsParsedIfValuesAreFromEnumMember() + { + 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 EnumIsParsedIfValuesAreIntegers() + { + 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 EnumIsParsedIfValuesAreStrings() + { + 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 EnumIsParsedIfValuesAreFromEnumMember() + { + 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..ef6c0c11 --- /dev/null +++ b/src/Helpers/EnumHelpers.cs @@ -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 +{ + /// + /// 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; + + var type = typeof(T); + 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 + { + if(TryGetFieldValueName(typeof(T), value, out var val)) + { + value = val; + } + + return 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 + { + var value = span.ToString(); + if(TryGetFieldValueName(typeof(T), value, out var val)) + { + value = val; + } + + return value.AsSpan(); + } + + /// + /// Gets the enum value from the raw value for the given type + /// + /// + /// + /// +#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/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 From 989149d82155f832e7cc8417db33aa72a6812007 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Thu, 27 Jun 2024 13:55:22 +0200 Subject: [PATCH 2/4] Helper for parsing enums from string --- .../EnumHelperTests.cs | 12 ++++++------ src/Helpers/EnumHelpers.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs b/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs index 94327263..64932ad9 100644 --- a/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/EnumHelperTests.cs @@ -15,7 +15,7 @@ public void EnumGenericIsParsedIfValueIsInteger() } [Fact] - public void EnumGenericIsParsedIfValuesAreIntegers() + public void EnumWithFlagsGenericIsParsedIfValuesAreIntegers() { var result = EnumHelpers.GetEnumValue("1,2"); @@ -31,7 +31,7 @@ public void EnumGenericIsParsedIfValueIsString() } [Fact] - public void EnumGenericIsParsedIfValuesAreStrings() + public void EnumWithFlagsGenericIsParsedIfValuesAreStrings() { var result = EnumHelpers.GetEnumValue("Value1,Value3"); @@ -47,7 +47,7 @@ public void EnumGenericIsParsedIfValueIsFromEnumMember() } [Fact] - public void EnumGenericIsParsedIfValuesAreFromEnumMember() + public void EnumWithFlagsGenericIsParsedIfValuesAreFromEnumMember() { var result = EnumHelpers.GetEnumValue("Value__2,Value__3"); @@ -71,7 +71,7 @@ public void EnumIsParsedIfValueIsInteger() } [Fact] - public void EnumIsParsedIfValuesAreIntegers() + public void EnumWithFlagsIsParsedIfValuesAreIntegers() { var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "1,2"); @@ -87,7 +87,7 @@ public void EnumIsParsedIfValueIsString() } [Fact] - public void EnumIsParsedIfValuesAreStrings() + public void EnumWithFlagsIsParsedIfValuesAreStrings() { var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value1,Value3"); @@ -103,7 +103,7 @@ public void EnumIsParsedIfValueIsFromEnumMember() } [Fact] - public void EnumIsParsedIfValuesAreFromEnumMember() + public void EnumWithFlagsIsParsedIfValuesAreFromEnumMember() { var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value__2,Value__3"); diff --git a/src/Helpers/EnumHelpers.cs b/src/Helpers/EnumHelpers.cs index ef6c0c11..ab410778 100644 --- a/src/Helpers/EnumHelpers.cs +++ b/src/Helpers/EnumHelpers.cs @@ -84,8 +84,8 @@ private static ReadOnlySpan ToEnumRawName(ReadOnlySpan span) wher /// /// 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) From 97e5d0f57e6520cf4286ff51da4adb9253a1bf68 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 28 Jun 2024 11:57:04 +0200 Subject: [PATCH 3/4] Helper for parsing enums from string --- src/Helpers/EnumHelpers.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Helpers/EnumHelpers.cs b/src/Helpers/EnumHelpers.cs index ab410778..d98cc3da 100644 --- a/src/Helpers/EnumHelpers.cs +++ b/src/Helpers/EnumHelpers.cs @@ -27,7 +27,6 @@ public static class EnumHelpers { if(string.IsNullOrEmpty(rawValue)) return null; - var type = typeof(T); rawValue = ToEnumRawName(rawValue!); if(typeof(T).IsDefined(typeof(FlagsAttribute))) { @@ -58,12 +57,7 @@ public static class EnumHelpers private static string ToEnumRawName(string value) where T : struct, Enum #endif { - if(TryGetFieldValueName(typeof(T), value, out var val)) - { - value = val; - } - - return value; + return TryGetFieldValueName(typeof(T), value, out var val) ? val : value; } #if NET5_0_OR_GREATER @@ -72,13 +66,7 @@ private static string ToEnumRawName(string value) where T : struct, Enum private static ReadOnlySpan ToEnumRawName(ReadOnlySpan span) where T : struct, Enum #endif { - var value = span.ToString(); - if(TryGetFieldValueName(typeof(T), value, out var val)) - { - value = val; - } - - return value.AsSpan(); + return TryGetFieldValueName(typeof(T), span.ToString(), out var val) ? val.AsSpan() : span; } /// From 765aa7fb765a163ae97d7998a10cb6ae5070bc5e Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Wed, 3 Jul 2024 13:54:43 +0200 Subject: [PATCH 4/4] Helper for parsing enums from string --- CHANGELOG.md | 2 +- src/IRequestAdapter.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99c74bfd..5db3e570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.9.7] - 2024-06-28 +## [1.9.7] - 2024-07-04 - Add helper methods to parse enums from strings. 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. ///