Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aligns Enum serialization across packages and pass relevant JsonWriterOptions from the KiotaJsonSerializationContext #312

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- Common default project properties for ALL projects-->
<PropertyGroup>
<VersionPrefix>1.10.0</VersionPrefix>
<VersionPrefix>1.10.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<!-- This is overidden in test projects by setting to true-->
<IsTestProject>false</IsTestProject>
Expand Down
25 changes: 25 additions & 0 deletions src/abstractions/Helpers/EnumHelpers.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,7 +23,7 @@
#if NET5_0_OR_GREATER
public static T? GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>(string rawValue) where T : struct, Enum
#else
public static T? GetEnumValue<T>(string rawValue) where T : struct, Enum

Check warning on line 26 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

All 'GetEnumValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)

Check warning on line 26 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 26 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

All 'GetEnumValue' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)

Check warning on line 26 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
#endif
{
if(string.IsNullOrEmpty(rawValue)) return null;
Expand Down Expand Up @@ -78,7 +79,7 @@
#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)

Check warning on line 82 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 82 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
#endif
{
object? result;
Expand Down Expand Up @@ -106,7 +107,7 @@
{
intValue |= (int)Enum.Parse(enumType, valueName, true);
}
catch { }

Check warning on line 110 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Handle the exception or explain in a comment why it can be ignored. (https://rules.sonarsource.com/csharp/RSPEC-2486)

Check warning on line 110 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Either remove or fill this block of code. (https://rules.sonarsource.com/csharp/RSPEC-108)

Check warning on line 110 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Handle the exception or explain in a comment why it can be ignored. (https://rules.sonarsource.com/csharp/RSPEC-2486)

Check warning on line 110 in src/abstractions/Helpers/EnumHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Either remove or fill this block of code. (https://rules.sonarsource.com/csharp/RSPEC-108)
#endif

rawValue = commaIndex < 0 ? string.Empty : rawValue.Substring(commaIndex + 1);
Expand Down Expand Up @@ -154,5 +155,29 @@
}
return false;
}

/// <summary>
/// Gets the enum string representation of the given value. Looks up if there is an <see cref="EnumMemberAttribute"/> and returns the value if found, otherwise returns the enum name in camel case.
/// </summary>
/// <typeparam name="T">The Enum type</typeparam>
/// <param name="value">The enum value</param>
/// <returns></returns>
/// <exception cref="ArgumentException">If value is null</exception>
#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>(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<EnumMemberAttribute>() is { } attribute)
return attribute.Value;

return name;
}
}
}
11 changes: 6 additions & 5 deletions src/serialization/form/FormSerializationWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#endif
using System;
using System.Collections.Generic;
using Microsoft.Kiota.Abstractions.Helpers;

namespace Microsoft.Kiota.Serialization.Form;
/// <summary>Represents a serialization writer that can be used to write a form url encoded string.</summary>
Expand Down Expand Up @@ -247,13 +248,13 @@ public void WriteCollectionOfEnumValues<T>(string? key, IEnumerable<T?>? 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);
}
}

Expand Down Expand Up @@ -281,16 +282,16 @@ public void WriteEnumValue<T>(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));
}
}
}
28 changes: 8 additions & 20 deletions src/serialization/json/JsonSerializationWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
});
}

/// <summary>
Expand Down Expand Up @@ -290,7 +293,7 @@ public void WriteEnumValue<T>(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(",");
Expand All @@ -299,7 +302,7 @@ public void WriteEnumValue<T>(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));
}
}

Expand Down Expand Up @@ -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>(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<EnumMemberAttribute>() is { } attribute)
return attribute.Value;

return name.ToFirstCharacterLowerCase();
}
/// <summary>
/// Writes a untyped value for the specified key.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/serialization/text/TextSerializationWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -118,5 +119,5 @@ public void WriteCollectionOfEnumValues<T>(string? key, IEnumerable<T?>? values)
#else
public void WriteEnumValue<T>(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);
}
24 changes: 21 additions & 3 deletions tests/serialization/form/FormSerializationWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TestNamingEnum>("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]
Expand All @@ -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);
}
}
13 changes: 13 additions & 0 deletions tests/serialization/form/Mocks/TestNamingEnum.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
67 changes: 65 additions & 2 deletions tests/serialization/json/JsonSerializationWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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," +
Expand Down Expand Up @@ -205,7 +206,7 @@ public void WritesEnumValuesAsCamelCasedIfNotEscaped()

// Assert
var expectedString = "[{" +
"\"testNamingEnum\":\"item1\"" + // Camel Cased
"\"testNamingEnum\":\"Item1\"" + // Camel Cased
"}]";
Assert.Equal(expectedString, serializedJsonString);
}
Expand Down Expand Up @@ -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<string, object>()
{
{"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<string, object>()
{
{"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()
{
Expand Down
6 changes: 1 addition & 5 deletions tests/serialization/text/Mocks/TestEnum.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
13 changes: 13 additions & 0 deletions tests/serialization/text/Mocks/TestNamingEnum.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 3 additions & 3 deletions tests/serialization/text/TextParseNodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestEnum>();
var result = parseNode.GetEnumValue<TestNamingEnum>();

Assert.Equal(TestEnum.SecondItem, result);
Assert.Equal(TestNamingEnum.Item2SubItem1, result);
}

[Fact]
Expand Down
Loading
Loading