diff --git a/CHANGELOG.md b/CHANGELOG.md index 55df23d..a497afa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.4.4] - 2024-06-21 + +### Changed + +- Fixes handling enums by `SendPrimitiveAsync` + ## [1.4.3] - 2024-05-24 ### Changed diff --git a/Microsoft.Kiota.Http.HttpClientLibrary.Tests/RequestAdapterTests.cs b/Microsoft.Kiota.Http.HttpClientLibrary.Tests/RequestAdapterTests.cs index 1c8ae11..ef55e6a 100644 --- a/Microsoft.Kiota.Http.HttpClientLibrary.Tests/RequestAdapterTests.cs +++ b/Microsoft.Kiota.Http.HttpClientLibrary.Tests/RequestAdapterTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -535,5 +536,282 @@ public async Task ThrowsApiExceptionOnMissingMapping(HttpStatusCode statusCode) Assert.Contains("The server returned an unexpected status code and no error factory is registered for this code", apiException.Message); } } + + [Fact] + public async Task SendPrimitiveHandleEnumIfValueIsString() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value1") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value1"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnum.Value1, response); + } + + [Fact] + public async Task SendPrimitiveHandleEnumIfValueIsInteger() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("1") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("1"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnum.Value2, response); + } + + [Fact] + public async Task SendPrimitiveHandleEnumIfValueIsFromEnumMember() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value__3") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value__3"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnum.Value3, response); + } + + [Fact] + public async Task SendPrimitiveReturnsNullIfValueCannotBeParsedToEnum() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value0") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value0"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Null(response); + } + + [Fact] + public async Task SendPrimitiveHandleEnumFlagsIfValuesAreStrings() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value1,Value3") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value1,Value3"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value3, response); + } + + [Fact] + public async Task SendPrimitiveHandleEnumFlagsIfValuesAreIntegers() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("1,2") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("1,2"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnumWithFlags.Value1 | TestEnumWithFlags.Value2, response); + } + + [Fact] + public async Task SendPrimitiveHandleEnumFlagsIfValuesAreFromEnumMember() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value__3,Value__2") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value__3,Value__2"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Equal(TestEnumWithFlags.Value2 | TestEnumWithFlags.Value3, response); + } + + [Fact] + public async Task SendPrimitiveReturnsNullIfFlagValueCannotBeParsedToEnum() + { + var mockHandler = new Mock(); + var client = new HttpClient(mockHandler.Object); + mockHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Value0") + }); + + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetStringValue()) + .Returns("Value0"); + + var mockParseNodeFactory = new Mock(); + mockParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(mockParseNode.Object)); + var adapter = new HttpClientRequestAdapter(_authenticationProvider, mockParseNodeFactory.Object, httpClient: client); + var requestInfo = new RequestInformation + { + HttpMethod = Method.GET, + URI = new Uri("https://example.com") + }; + + var response = await adapter.SendPrimitiveAsync(requestInfo); + + Assert.Null(response); + } + } + + public enum TestEnum + { + [EnumMember(Value = "Value__1")] + Value1, + [EnumMember(Value = "Value__2")] + Value2, + [EnumMember(Value = "Value__3")] + Value3 + } + + [Flags] + public enum TestEnumWithFlags + { + [EnumMember(Value = "Value__1")] + Value1 = 0x01, + [EnumMember(Value = "Value__2")] + Value2 = 0x02, + [EnumMember(Value = "Value__3")] + Value3 = 0x04 } } diff --git a/src/HttpClientRequestAdapter.cs b/src/HttpClientRequestAdapter.cs index 689f705..9e2397c 100644 --- a/src/HttpClientRequestAdapter.cs +++ b/src/HttpClientRequestAdapter.cs @@ -18,6 +18,9 @@ using System.Text.RegularExpressions; using System.Diagnostics; using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; +using System.Reflection; +using System.Runtime.Serialization; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Kiota.Http.HttpClientLibrary { @@ -293,7 +296,16 @@ public string? BaseUrl { result = rootNode.GetDateValue(); } - else throw new InvalidOperationException("error handling the response, unexpected type"); + else if( + Nullable.GetUnderlyingType(modelType) is { IsEnum: true } underlyingType && + rootNode.GetStringValue() is { Length: > 0 } rawValue) + { + result = GetEnumValue(underlyingType, rawValue); + } + else + { + throw new InvalidOperationException("error handling the response, unexpected type"); + } SetResponseType(result, span); return (ModelType)result!; } @@ -312,6 +324,80 @@ public string? BaseUrl return await responseHandler.HandleResponseAsync(response, errorMapping).ConfigureAwait(false); } } +#if NET5_0_OR_GREATER + private static object? GetEnumValue([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] Type? underlyingType, string rawValue) +#else + private static object? GetEnumValue(Type? underlyingType, string rawValue) +#endif + { + object? result; + if(underlyingType == null) + { + return null; + } + if(underlyingType.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(valueName, out var value)) + { + valueName = value; + } +#if NET5_0_OR_GREATER + if(Enum.TryParse(underlyingType, valueName, true, out var enumPartResult)) + intValue |= (int)enumPartResult!; +#else + try + { + intValue |= (int)Enum.Parse(underlyingType, valueName, true); + } + catch { } +#endif + + rawValue = commaIndex < 0 ? string.Empty : rawValue.Substring(commaIndex + 1); + } + result = intValue > 0 ? Enum.Parse(underlyingType, intValue.ToString(), true) : null; + } + else + { + if(TryGetFieldValueName(rawValue, out var value)) + { + rawValue = value; + } + +#if NET5_0_OR_GREATER + Enum.TryParse(underlyingType, rawValue, true, out object? enumResult); + result = enumResult; +#else + try + { + result = Enum.Parse(underlyingType, rawValue, true); + } + catch + { + result = null; + } +#endif + } + return result; + + bool TryGetFieldValueName(string rawValue, out string valueName) + { + valueName = string.Empty; + foreach(var field in underlyingType.GetFields()) + { + if(field.GetCustomAttribute() is { } attr && rawValue.Equals(attr.Value, StringComparison.Ordinal)) + { + valueName = field.Name; + return true; + } + } + return false; + } + } /// /// Send a instance with an empty request body /// diff --git a/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj b/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj index d44b149..e483240 100644 --- a/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj +++ b/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj @@ -15,7 +15,7 @@ https://aka.ms/kiota/docs true true - 1.4.3 + 1.4.4 true