diff --git a/CHANGELOG.md b/CHANGELOG.md index 90093d1d..dae766be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.10.1] - 2024-08-01 + +- Cleans up enum serialization to read from attributes for form and text serialization [#284](https://github.com/microsoft/kiota-dotnet/issues/284) +- Pass relevant `JsonWriterOptions` from the `KiotaJsonSerializationContext.Options` to the `Utf8JsonWriter` when writing json to enable customization. [#281](https://github.com/microsoft/kiota-dotnet/issues/281) + ## [1.10.0] - 2024-07-17 - Adds Kiota bundle package to provide default adapter with registrations setup. [#290](https://github.com/microsoft/kiota-dotnet/issues/290) diff --git a/Directory.Build.props b/Directory.Build.props index 3ee3a8f1..81999928 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.10.0 + 1.10.1 false diff --git a/src/abstractions/Helpers/EnumHelpers.cs b/src/abstractions/Helpers/EnumHelpers.cs index 72ffce69..86397558 100644 --- a/src/abstractions/Helpers/EnumHelpers.cs +++ b/src/abstractions/Helpers/EnumHelpers.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using System.Runtime.Serialization; +using Microsoft.Kiota.Abstractions.Extensions; #if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; @@ -154,5 +155,29 @@ private static bool TryGetFieldValueName(Type type, string rawValue, out string } return false; } + + /// + /// Gets the enum string representation of the given value. Looks up if there is an and returns the value if found, otherwise returns the enum name in camel case. + /// + /// The Enum type + /// The enum value + /// + /// If value is null +#if NET5_0_OR_GREATER + public static string? GetEnumStringValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(T value) where T : struct, Enum +#else + public static string? GetEnumStringValue(T value) where T : struct, Enum +#endif + { + var type = typeof(T); + + if(Enum.GetName(type, value) is not { } name) + throw new ArgumentException($"Invalid Enum value {value} for enum of type {type}"); + + if(type.GetField(name)?.GetCustomAttribute() is { } attribute) + return attribute.Value; + + return name; + } } } diff --git a/src/serialization/form/FormSerializationWriter.cs b/src/serialization/form/FormSerializationWriter.cs index 93a9325b..1ab45a8c 100644 --- a/src/serialization/form/FormSerializationWriter.cs +++ b/src/serialization/form/FormSerializationWriter.cs @@ -11,6 +11,7 @@ #endif using System; using System.Collections.Generic; +using Microsoft.Kiota.Abstractions.Helpers; namespace Microsoft.Kiota.Serialization.Form; /// Represents a serialization writer that can be used to write a form url encoded string. @@ -247,13 +248,13 @@ public void WriteCollectionOfEnumValues(string? key, IEnumerable? values) StringBuilder? valueNames = null; foreach(var x in values) { - if(x.HasValue && Enum.GetName(typeof(T), x.Value) is string valueName) + if(x.HasValue && EnumHelpers.GetEnumStringValue(x.Value) is string valueName) { if(valueNames == null) valueNames = new StringBuilder(); else valueNames.Append(","); - valueNames.Append(valueName.ToFirstCharacterLowerCase()); + valueNames.Append(valueName); } } @@ -281,16 +282,16 @@ public void WriteEnumValue(string? key, T? value) where T : struct, Enum StringBuilder valueNames = new StringBuilder(); foreach(var x in values) { - if(value.Value.HasFlag(x) && Enum.GetName(typeof(T), x) is string valueName) + if(value.Value.HasFlag(x) && EnumHelpers.GetEnumStringValue(x) is string valueName) { if(valueNames.Length > 0) valueNames.Append(","); - valueNames.Append(valueName.ToFirstCharacterLowerCase()); + valueNames.Append(valueName); } } WriteStringValue(key, valueNames.ToString()); } - else WriteStringValue(key, value.Value.ToString().ToFirstCharacterLowerCase()); + else WriteStringValue(key, EnumHelpers.GetEnumStringValue(value.Value)); } } } diff --git a/src/serialization/json/JsonSerializationWriter.cs b/src/serialization/json/JsonSerializationWriter.cs index 23c41657..30d574b8 100644 --- a/src/serialization/json/JsonSerializationWriter.cs +++ b/src/serialization/json/JsonSerializationWriter.cs @@ -7,12 +7,11 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -using System.Runtime.Serialization; using System.Text; using System.Text.Json; using System.Xml; using Microsoft.Kiota.Abstractions; -using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Helpers; using Microsoft.Kiota.Abstractions.Serialization; #if NET5_0_OR_GREATER @@ -49,7 +48,11 @@ public JsonSerializationWriter() public JsonSerializationWriter(KiotaJsonSerializationContext kiotaJsonSerializationContext) { _kiotaJsonSerializationContext = kiotaJsonSerializationContext; - writer = new Utf8JsonWriter(_stream); + writer = new Utf8JsonWriter(_stream, new JsonWriterOptions + { + Encoder = kiotaJsonSerializationContext.Options.Encoder, + Indented = kiotaJsonSerializationContext.Options.WriteIndented + }); } /// @@ -290,7 +293,7 @@ public void WriteEnumValue(string? key, T? value) where T : struct, Enum StringBuilder valueNames = new StringBuilder(); foreach(var x in values) { - if(value.Value.HasFlag(x) && GetEnumName(x) is string valueName) + if(value.Value.HasFlag(x) && EnumHelpers.GetEnumStringValue(x) is string valueName) { if(valueNames.Length > 0) valueNames.Append(","); @@ -299,7 +302,7 @@ public void WriteEnumValue(string? key, T? value) where T : struct, Enum } WriteStringValue(null, valueNames.ToString()); } - else WriteStringValue(null, GetEnumName(value.Value)); + else WriteStringValue(null, EnumHelpers.GetEnumStringValue(value.Value)); } } @@ -559,22 +562,7 @@ public void Dispose() writer.Dispose(); GC.SuppressFinalize(this); } -#if NET5_0_OR_GREATER - private static string? GetEnumName<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(T value) where T : struct, Enum -#else - private static string? GetEnumName(T value) where T : struct, Enum -#endif - { - var type = typeof(T); - - if(Enum.GetName(type, value) is not { } name) - throw new ArgumentException($"Invalid Enum value {value} for enum of type {type}"); - if(type.GetField(name)?.GetCustomAttribute() is { } attribute) - return attribute.Value; - - return name.ToFirstCharacterLowerCase(); - } /// /// Writes a untyped value for the specified key. /// diff --git a/src/serialization/text/TextSerializationWriter.cs b/src/serialization/text/TextSerializationWriter.cs index 15d600c1..bee04237 100644 --- a/src/serialization/text/TextSerializationWriter.cs +++ b/src/serialization/text/TextSerializationWriter.cs @@ -5,6 +5,7 @@ using System.Xml; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Helpers; using Microsoft.Kiota.Abstractions.Serialization; #if NET5_0_OR_GREATER @@ -118,5 +119,5 @@ public void WriteCollectionOfEnumValues(string? key, IEnumerable? values) #else public void WriteEnumValue(string? key, T? value) where T : struct, Enum #endif - => WriteStringValue(key, value.HasValue ? value.Value.ToString().ToFirstCharacterLowerCase() : null); + => WriteStringValue(key, value.HasValue ? EnumHelpers.GetEnumStringValue(value.Value) : null); } diff --git a/tests/serialization/form/FormSerializationWriterTests.cs b/tests/serialization/form/FormSerializationWriterTests.cs index 45822417..ffc29db2 100644 --- a/tests/serialization/form/FormSerializationWriterTests.cs +++ b/tests/serialization/form/FormSerializationWriterTests.cs @@ -51,7 +51,7 @@ public void WritesSampleObjectValue() // Assert var expectedString = "id=48d31887-5fad-4d73-a9f5-3c356e68a038&" + - "numbers=one%2Ctwo&" + // serializes enums + "numbers=One%2CTwo&" + // serializes enums "workDuration=PT1H&" + // Serializes timespans "birthDay=2017-09-04&" + // Serializes dates "startWorkTime=08%3A00%3A00&" + //Serializes times @@ -432,7 +432,25 @@ public void WriteEnumValue_IsWrittenCorrectly() var serializedString = reader.ReadToEnd(); // Assert - Assert.Equal("prop1=sixteen", serializedString); + Assert.Equal("prop1=Sixteen", serializedString); + } + + [Fact] + public void WriteEnumValueWithAttribute_IsWrittenCorrectly() + { + // Arrange + var value = TestNamingEnum.Item2SubItem1; + + using var formSerializationWriter = new FormSerializationWriter(); + + // Act + formSerializationWriter.WriteEnumValue("prop1", value); + var contentStream = formSerializationWriter.GetSerializedContent(); + using var reader = new StreamReader(contentStream, Encoding.UTF8); + var serializedString = reader.ReadToEnd(); + + // Assert + Assert.Equal("prop1=Item2%3ASubItem1", serializedString); } [Fact] @@ -450,6 +468,6 @@ public void WriteCollectionOfEnumValues_IsWrittenCorrectly() var serializedString = reader.ReadToEnd(); // Assert - Assert.Equal("prop1=sixteen%2Ctwo", serializedString); + Assert.Equal("prop1=Sixteen%2CTwo", serializedString); } } diff --git a/tests/serialization/form/Mocks/TestNamingEnum.cs b/tests/serialization/form/Mocks/TestNamingEnum.cs new file mode 100644 index 00000000..e7f94e5e --- /dev/null +++ b/tests/serialization/form/Mocks/TestNamingEnum.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Microsoft.Kiota.Serialization.Form.Tests.Mocks +{ + public enum TestNamingEnum + { + Item1, + [EnumMember(Value = "Item2:SubItem1")] + Item2SubItem1, + [EnumMember(Value = "Item3:SubItem1")] + Item3SubItem1 + } +} diff --git a/tests/serialization/json/JsonSerializationWriterTests.cs b/tests/serialization/json/JsonSerializationWriterTests.cs index 252e5eba..d0ea10c2 100644 --- a/tests/serialization/json/JsonSerializationWriterTests.cs +++ b/tests/serialization/json/JsonSerializationWriterTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using Microsoft.Kiota.Abstractions; @@ -156,7 +157,7 @@ public void WritesSampleCollectionOfObjectValues() // Assert var expectedString = "[{" + "\"id\":\"48d31887-5fad-4d73-a9f5-3c356e68a038\"," + - "\"numbers\":\"one,two\"," + + "\"numbers\":\"One,Two\"," + "\"testNamingEnum\":\"Item2:SubItem1\"," + "\"mobilePhone\":null," + "\"accountEnabled\":false," + @@ -205,7 +206,7 @@ public void WritesEnumValuesAsCamelCasedIfNotEscaped() // Assert var expectedString = "[{" + - "\"testNamingEnum\":\"item1\"" + // Camel Cased + "\"testNamingEnum\":\"Item1\"" + // Camel Cased "}]"; Assert.Equal(expectedString, serializedJsonString); } @@ -258,6 +259,68 @@ public void WriteGuidUsingConverter() Assert.Equal(expectedString, serializedJsonString); } + [Fact] + public void ForwardsOptionsToWriterFromSerializationContext() + { + // Arrange + var testEntity = new TestEntity + { + Id = "testId", + AdditionalData = new Dictionary() + { + {"href", "https://graph.microsoft.com/users/{user-id}"}, + {"unicodeName", "你好"} + } + }; + var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.General) + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + var serializationContext = new KiotaJsonSerializationContext(serializerOptions); + using var jsonSerializerWriter = new JsonSerializationWriter(serializationContext); + + // Act + jsonSerializerWriter.WriteObjectValue(string.Empty, testEntity); + var serializedStream = jsonSerializerWriter.GetSerializedContent(); + using var reader = new StreamReader(serializedStream, Encoding.UTF8); + var serializedJsonString = reader.ReadToEnd(); + + // Assert + const string expectedString = "{\n \"id\": \"testId\",\n \"href\": \"https://graph.microsoft.com/users/{user-id}\",\n \"unicodeName\": \"你好\"\n}"; + Assert.Contains("\n", serializedJsonString); // string is indented and not escaped + Assert.Contains("你好", serializedJsonString); // string is indented and not escaped + Assert.Equal(expectedString, serializedJsonString.Replace("\r", string.Empty)); // string is indented and not escaped + } + + [Fact] + public void UsesDefaultOptionsToWriterFromSerializationContext() + { + // Arrange + var testEntity = new TestEntity + { + Id = "testId", + AdditionalData = new Dictionary() + { + {"href", "https://graph.microsoft.com/users/{user-id}"}, + {"unicodeName", "你好"} + } + }; + using var jsonSerializerWriter = new JsonSerializationWriter(new KiotaJsonSerializationContext()); + + // Act + jsonSerializerWriter.WriteObjectValue(string.Empty, testEntity); + var serializedStream = jsonSerializerWriter.GetSerializedContent(); + using var reader = new StreamReader(serializedStream, Encoding.UTF8); + var serializedJsonString = reader.ReadToEnd(); + + // Assert + var expectedString = $"{{\"id\":\"testId\",\"href\":\"https://graph.microsoft.com/users/{{user-id}}\",\"unicodeName\":\"\\u4F60\\u597D\"}}"; + Assert.DoesNotContain("\n", serializedJsonString); // string is not indented and not escaped + Assert.DoesNotContain("你好", serializedJsonString); // string is not indented and not escaped + Assert.Contains("\\u4F60\\u597D", serializedJsonString); // string is not indented and not escaped + Assert.Equal(expectedString, serializedJsonString); // string is indented and not escaped + } [Fact] public void WriteGuidUsingNoConverter() { diff --git a/tests/serialization/text/Mocks/TestEnum.cs b/tests/serialization/text/Mocks/TestEnum.cs index ce83dd39..edd8bab9 100644 --- a/tests/serialization/text/Mocks/TestEnum.cs +++ b/tests/serialization/text/Mocks/TestEnum.cs @@ -1,12 +1,8 @@ -using System.Runtime.Serialization; - -namespace Microsoft.Kiota.Serialization.Text.Tests.Mocks +namespace Microsoft.Kiota.Serialization.Text.Tests.Mocks { public enum TestEnum { - [EnumMember(Value = "Value_1")] FirstItem, - [EnumMember(Value = "Value_2")] SecondItem, } } diff --git a/tests/serialization/text/Mocks/TestNamingEnum.cs b/tests/serialization/text/Mocks/TestNamingEnum.cs new file mode 100644 index 00000000..7a16d30a --- /dev/null +++ b/tests/serialization/text/Mocks/TestNamingEnum.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Microsoft.Kiota.Serialization.Text.Tests.Mocks +{ + public enum TestNamingEnum + { + Item1, + [EnumMember(Value = "Item2:SubItem1")] + Item2SubItem1, + [EnumMember(Value = "Item3:SubItem1")] + Item3SubItem1 + } +} diff --git a/tests/serialization/text/TextParseNodeTests.cs b/tests/serialization/text/TextParseNodeTests.cs index 4ac1dbae..39cfab12 100644 --- a/tests/serialization/text/TextParseNodeTests.cs +++ b/tests/serialization/text/TextParseNodeTests.cs @@ -41,12 +41,12 @@ public void TextParseNode_GetEnumFromString() [Fact] public void TextParseNode_GetEnumFromEnumMember() { - var text = "Value_2"; + var text = "Item2:SubItem1"; var parseNode = new TextParseNode(text); - var result = parseNode.GetEnumValue(); + var result = parseNode.GetEnumValue(); - Assert.Equal(TestEnum.SecondItem, result); + Assert.Equal(TestNamingEnum.Item2SubItem1, result); } [Fact] diff --git a/tests/serialization/text/TextSerializationWriterTests.cs b/tests/serialization/text/TextSerializationWriterTests.cs index 2a309712..cfd170bf 100644 --- a/tests/serialization/text/TextSerializationWriterTests.cs +++ b/tests/serialization/text/TextSerializationWriterTests.cs @@ -350,7 +350,26 @@ public void WriteEnumValue_IsWrittenCorrectly() var serializedString = reader.ReadToEnd(); // Assert - Assert.Equal("firstItem", serializedString); + Assert.Equal("FirstItem", serializedString); + } + + + [Fact] + public void WriteEnumValueWithAttribute_IsWrittenCorrectly() + { + // Arrange + var value = TestNamingEnum.Item2SubItem1; + + using var formSerializationWriter = new TextSerializationWriter(); + + // Act + formSerializationWriter.WriteEnumValue(null, value); + var contentStream = formSerializationWriter.GetSerializedContent(); + using var reader = new StreamReader(contentStream, Encoding.UTF8); + var serializedString = reader.ReadToEnd(); + + // Assert + Assert.Equal("Item2:SubItem1", serializedString); } [Fact]