Skip to content

Commit 95bc067

Browse files
authored
Merge pull request #259 from MartinM85/feature/258-enum-helper
Helper for parsing enums from string
2 parents 94e2130 + 765aa7f commit 95bc067

8 files changed

Lines changed: 317 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.9.7] - 2024-07-04
11+
12+
- Add helper methods to parse enums from strings.
13+
1014
## [1.9.6] - 2024-06-12
1115

1216
- Add `IEnumerable<T>` extension methods to remove LINQ dependency from generated code.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using Microsoft.Kiota.Abstractions.Helpers;
2+
using Microsoft.Kiota.Abstractions.Tests.Mocks;
3+
using Xunit;
4+
5+
namespace Microsoft.Kiota.Abstractions.Tests
6+
{
7+
public class EnumHelperTests
8+
{
9+
[Fact]
10+
public void EnumGenericIsParsedIfValueIsInteger()
11+
{
12+
var result = EnumHelpers.GetEnumValue<TestEnum>("0");
13+
14+
Assert.Equal(TestEnum.First, result);
15+
}
16+
17+
[Fact]
18+
public void EnumWithFlagsGenericIsParsedIfValuesAreIntegers()
19+
{
20+
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("1,2");
21+
22+
Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, result);
23+
}
24+
25+
[Fact]
26+
public void EnumGenericIsParsedIfValueIsString()
27+
{
28+
var result = EnumHelpers.GetEnumValue<TestEnum>("First");
29+
30+
Assert.Equal(TestEnum.First, result);
31+
}
32+
33+
[Fact]
34+
public void EnumWithFlagsGenericIsParsedIfValuesAreStrings()
35+
{
36+
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("Value1,Value3");
37+
38+
Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, result);
39+
}
40+
41+
[Fact]
42+
public void EnumGenericIsParsedIfValueIsFromEnumMember()
43+
{
44+
var result = EnumHelpers.GetEnumValue<TestEnum>("Value_2");
45+
46+
Assert.Equal(TestEnum.Second, result);
47+
}
48+
49+
[Fact]
50+
public void EnumWithFlagsGenericIsParsedIfValuesAreFromEnumMember()
51+
{
52+
var result = EnumHelpers.GetEnumValue<TestEnumWithFlags>("Value__2,Value__3");
53+
54+
Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, result);
55+
}
56+
57+
[Fact]
58+
public void IfEnumGenericIsNotParsedThenNullIsReturned()
59+
{
60+
var result = EnumHelpers.GetEnumValue<TestEnum>("Value_5");
61+
62+
Assert.Null(result);
63+
}
64+
65+
[Fact]
66+
public void EnumIsParsedIfValueIsInteger()
67+
{
68+
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "0");
69+
70+
Assert.Equal(TestEnum.First, result);
71+
}
72+
73+
[Fact]
74+
public void EnumWithFlagsIsParsedIfValuesAreIntegers()
75+
{
76+
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "1,2");
77+
78+
Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, result);
79+
}
80+
81+
[Fact]
82+
public void EnumIsParsedIfValueIsString()
83+
{
84+
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "First");
85+
86+
Assert.Equal(TestEnum.First, result);
87+
}
88+
89+
[Fact]
90+
public void EnumWithFlagsIsParsedIfValuesAreStrings()
91+
{
92+
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value1,Value3");
93+
94+
Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, result);
95+
}
96+
97+
[Fact]
98+
public void EnumIsParsedIfValueIsFromEnumMember()
99+
{
100+
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "Value_2");
101+
102+
Assert.Equal(TestEnum.Second, result);
103+
}
104+
105+
[Fact]
106+
public void EnumWithFlagsIsParsedIfValuesAreFromEnumMember()
107+
{
108+
var result = EnumHelpers.GetEnumValue(typeof(TestEnumWithFlags), "Value__2,Value__3");
109+
110+
Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, result);
111+
}
112+
113+
[Fact]
114+
public void IfEnumIsNotParsedThenNullIsReturned()
115+
{
116+
var result = EnumHelpers.GetEnumValue(typeof(TestEnum), "Value_5");
117+
118+
Assert.Null(result);
119+
}
120+
}
121+
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
using System;
12
using System.Runtime.Serialization;
23

34
namespace Microsoft.Kiota.Abstractions.Tests.Mocks;
45

56
public enum TestEnum
67
{
7-
[EnumMember(Value = "1")]
8+
[EnumMember(Value = "Value_1")]
89
First,
9-
[EnumMember(Value = "2")]
10+
[EnumMember(Value = "Value_2")]
1011
Second,
11-
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
4+
namespace Microsoft.Kiota.Abstractions.Tests.Mocks;
5+
6+
[Flags]
7+
public enum TestEnumWithFlags
8+
{
9+
[EnumMember(Value = "Value__1")]
10+
Value1 = 0x01,
11+
[EnumMember(Value = "Value__2")]
12+
Value2 = 0x02,
13+
[EnumMember(Value = "Value__3")]
14+
Value3 = 0x04
15+
}

Microsoft.Kiota.Abstractions.Tests/RequestInformationTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ public void SetsEnumValueInQueryParameters()
553553
// Act
554554
testRequest.AddQueryParameters(new GetQueryParameters { DataSet = TestEnum.First });
555555
// Assert
556-
Assert.Equal("http://localhost/me?dataset=1", testRequest.URI.ToString());
556+
Assert.Equal("http://localhost/me?dataset=Value_1", testRequest.URI.ToString());
557557
}
558558
[Fact]
559559
public void SetsEnumValuesInQueryParameters()
@@ -567,7 +567,7 @@ public void SetsEnumValuesInQueryParameters()
567567
// Act
568568
testRequest.AddQueryParameters(new GetQueryParameters { DataSets = new TestEnum[] { TestEnum.First, TestEnum.Second } });
569569
// Assert
570-
Assert.Equal("http://localhost/me?datasets=1,2", testRequest.URI.ToString());
570+
Assert.Equal("http://localhost/me?datasets=Value_1,Value_2", testRequest.URI.ToString());
571571
}
572572
[Fact]
573573
public void SetsEnumValueInPathParameters()
@@ -581,7 +581,7 @@ public void SetsEnumValueInPathParameters()
581581
// Act
582582
testRequest.PathParameters.Add("dataset", TestEnum.First);
583583
// Assert
584-
Assert.Equal("http://localhost/1", testRequest.URI.ToString());
584+
Assert.Equal("http://localhost/Value_1", testRequest.URI.ToString());
585585
}
586586
[Fact]
587587
public void SetsEnumValuesInPathParameters()
@@ -595,7 +595,7 @@ public void SetsEnumValuesInPathParameters()
595595
// Act
596596
testRequest.PathParameters.Add("dataset", new TestEnum[] { TestEnum.First, TestEnum.Second });
597597
// Assert
598-
Assert.Equal("http://localhost/1,2", testRequest.URI.ToString());
598+
Assert.Equal("http://localhost/Value_1,Value_2", testRequest.URI.ToString());
599599
}
600600

601601

src/Helpers/EnumHelpers.cs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Runtime.Serialization;
4+
5+
#if NET5_0_OR_GREATER
6+
using System.Diagnostics.CodeAnalysis;
7+
#endif
8+
9+
namespace Microsoft.Kiota.Abstractions.Helpers
10+
{
11+
/// <summary>
12+
/// Helper methods for enums
13+
/// </summary>
14+
public static class EnumHelpers
15+
{
16+
/// <summary>
17+
/// Gets the enum value from the raw value
18+
/// </summary>
19+
/// <typeparam name="T">Enum type</typeparam>
20+
/// <param name="rawValue">Raw value</param>
21+
/// <returns></returns>
22+
#if NET5_0_OR_GREATER
23+
public static T? GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string rawValue) where T : struct, Enum
24+
#else
25+
public static T? GetEnumValue<T>(string rawValue) where T : struct, Enum
26+
#endif
27+
{
28+
if(string.IsNullOrEmpty(rawValue)) return null;
29+
30+
rawValue = ToEnumRawName<T>(rawValue!);
31+
if(typeof(T).IsDefined(typeof(FlagsAttribute)))
32+
{
33+
ReadOnlySpan<char> valueSpan = rawValue.AsSpan();
34+
int value = 0;
35+
while(valueSpan.Length > 0)
36+
{
37+
int commaIndex = valueSpan.IndexOf(',');
38+
ReadOnlySpan<char> valueNameSpan = commaIndex < 0 ? valueSpan : valueSpan.Slice(0, commaIndex);
39+
valueNameSpan = ToEnumRawName<T>(valueNameSpan);
40+
#if NET6_0_OR_GREATER
41+
if(Enum.TryParse<T>(valueNameSpan, true, out var result))
42+
#else
43+
if(Enum.TryParse<T>(valueNameSpan.ToString(), true, out var result))
44+
#endif
45+
value |= (int)(object)result;
46+
valueSpan = commaIndex < 0 ? ReadOnlySpan<char>.Empty : valueSpan.Slice(commaIndex + 1);
47+
}
48+
return (T)(object)value;
49+
}
50+
else
51+
return Enum.TryParse<T>(rawValue, true, out var result) ? result : null;
52+
}
53+
54+
#if NET5_0_OR_GREATER
55+
private static string ToEnumRawName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
56+
#else
57+
private static string ToEnumRawName<T>(string value) where T : struct, Enum
58+
#endif
59+
{
60+
return TryGetFieldValueName(typeof(T), value, out var val) ? val : value;
61+
}
62+
63+
#if NET5_0_OR_GREATER
64+
private static ReadOnlySpan<char> ToEnumRawName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(ReadOnlySpan<char> span) where T : struct, Enum
65+
#else
66+
private static ReadOnlySpan<char> ToEnumRawName<T>(ReadOnlySpan<char> span) where T : struct, Enum
67+
#endif
68+
{
69+
return TryGetFieldValueName(typeof(T), span.ToString(), out var val) ? val.AsSpan() : span;
70+
}
71+
72+
/// <summary>
73+
/// Gets the enum value from the raw value for the given type
74+
/// </summary>
75+
/// <param name="type">Enum type</param>
76+
/// <param name="rawValue">Raw value</param>
77+
/// <returns></returns>
78+
#if NET5_0_OR_GREATER
79+
public static object? GetEnumValue([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type? type, string rawValue)
80+
#else
81+
public static object? GetEnumValue(Type? type, string rawValue)
82+
#endif
83+
{
84+
object? result;
85+
if(type == null)
86+
{
87+
return null;
88+
}
89+
if(type.IsDefined(typeof(FlagsAttribute)))
90+
{
91+
int intValue = 0;
92+
while(rawValue.Length > 0)
93+
{
94+
int commaIndex = rawValue.IndexOf(',');
95+
var valueName = commaIndex < 0 ? rawValue : rawValue.Substring(0, commaIndex);
96+
if(TryGetFieldValueName(type, valueName, out var value))
97+
{
98+
valueName = value;
99+
}
100+
#if NET5_0_OR_GREATER
101+
if(Enum.TryParse(type, valueName, true, out var enumPartResult))
102+
intValue |= (int)enumPartResult!;
103+
#else
104+
try
105+
{
106+
intValue |= (int)Enum.Parse(type, valueName, true);
107+
}
108+
catch { }
109+
#endif
110+
111+
rawValue = commaIndex < 0 ? string.Empty : rawValue.Substring(commaIndex + 1);
112+
}
113+
result = intValue > 0 ? Enum.Parse(type, intValue.ToString(), true) : null;
114+
}
115+
else
116+
{
117+
if(TryGetFieldValueName(type, rawValue, out var value))
118+
{
119+
rawValue = value;
120+
}
121+
122+
#if NET5_0_OR_GREATER
123+
Enum.TryParse(type, rawValue, true, out object? enumResult);
124+
result = enumResult;
125+
#else
126+
try
127+
{
128+
result = Enum.Parse(type, rawValue, true);
129+
}
130+
catch
131+
{
132+
result = null;
133+
}
134+
#endif
135+
}
136+
return result;
137+
138+
139+
}
140+
141+
#if NET5_0_OR_GREATER
142+
private static bool TryGetFieldValueName([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type type, string rawValue, out string valueName)
143+
#else
144+
private static bool TryGetFieldValueName(Type type, string rawValue, out string valueName)
145+
#endif
146+
{
147+
valueName = string.Empty;
148+
foreach(var field in type.GetFields())
149+
{
150+
if(field.GetCustomAttribute<EnumMemberAttribute>() is { } attr && rawValue.Equals(attr.Value, StringComparison.Ordinal))
151+
{
152+
valueName = field.Name;
153+
return true;
154+
}
155+
}
156+
return false;
157+
}
158+
}
159+
}

src/IRequestAdapter.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ------------------------------------------------------------------------------
44

55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.Kiota.Abstractions.Serialization;
@@ -49,15 +50,23 @@ public interface IRequestAdapter
4950
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
5051
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
5152
/// <returns>The deserialized primitive response model.</returns>
53+
#if NET5_0_OR_GREATER
54+
Task<ModelType?> SendPrimitiveAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
55+
#else
5256
Task<ModelType?> SendPrimitiveAsync<ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
57+
#endif
5358
/// <summary>
5459
/// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection.
5560
/// </summary>
5661
/// <param name="requestInfo">The RequestInformation object to use for the HTTP request.</param>
5762
/// <param name="errorMapping">The error factories mapping to use in case of a failed request.</param>
5863
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use for cancelling the requests.</param>
5964
/// <returns>The deserialized primitive response model collection.</returns>
65+
#if NET5_0_OR_GREATER
66+
Task<IEnumerable<ModelType>?> SendPrimitiveCollectionAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
67+
#else
6068
Task<IEnumerable<ModelType>?> SendPrimitiveCollectionAsync<ModelType>(RequestInformation requestInfo, Dictionary<string, ParsableFactory<IParsable>>? errorMapping = default, CancellationToken cancellationToken = default);
69+
#endif
6170
/// <summary>
6271
/// Executes the HTTP request specified by the given RequestInformation with no return content.
6372
/// </summary>

0 commit comments

Comments
 (0)