Skip to content

Commit

Permalink
add flag converters nullable serialization (#857)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergei-boguslavski authored and kevcunnane committed Aug 29, 2019
1 parent 55c82fb commit c6e3b33
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 32 deletions.
3 changes: 3 additions & 0 deletions src/Microsoft.SqlTools.DataProtocol.Contracts/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.SqlTools.Hosting.UnitTests")]
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,29 @@ internal class FlagsIntConverter : JsonConverter
public override bool CanRead => true;

#region Public Methods

public override bool CanConvert(Type objectType)
{
return objectType.IsEnum && objectType.GetCustomAttribute(typeof(FlagsAttribute)) != null;
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// TODO: Fix to handle nullables properly

int[] values = JArray.Load(reader).Values<int>().ToArray();
var jToken = JToken.Load(reader);
if (jToken.Type == JTokenType.Null)
{
return null;
}

FieldInfo[] enumFields = objectType.GetFields(BindingFlags.Public | BindingFlags.Static);
int[] values = ((JArray)jToken).Values<int>().ToArray();
var pureType = NullableUtils.GetUnderlyingTypeIfNullable(objectType);

FieldInfo[] enumFields = pureType.GetFields(BindingFlags.Public | BindingFlags.Static);
int setFlags = 0;
foreach (FieldInfo enumField in enumFields)
{
int enumValue = (int) enumField.GetValue(null);
int enumValue = (int)enumField.GetValue(null);

// If there is a serialize value set for the enum value, look for that instead of the int value
SerializeValueAttribute serializeValue = enumField.GetCustomAttribute<SerializeValueAttribute>();
int searchValue = serializeValue?.Value ?? enumValue;
Expand All @@ -45,7 +50,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
}
}

return Enum.ToObject(objectType, setFlags);
return Enum.ToObject(pureType, setFlags);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
Expand All @@ -56,22 +61,22 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
{
// Make sure the flag is set before doing expensive reflection
int enumValue = (int)enumField.GetValue(null);
if (((int) value & enumValue) == 0)
if (((int)value & enumValue) == 0)
{
continue;
}

// If there is a serialize value set for the member, use that instead of the int value
SerializeValueAttribute serializeValue = enumField.GetCustomAttribute<SerializeValueAttribute>();
int flagValue = serializeValue?.Value ?? enumValue;
int flagValue = serializeValue?.Value ?? enumValue;
setFlags.Add(flagValue);
}

string joinedFlags = string.Join(", ", setFlags);
writer.WriteRawValue($"[{joinedFlags}]");
}
#endregion

#endregion Public Methods

[AttributeUsage(AttributeTargets.Field)]
internal class SerializeValueAttribute : Attribute
Expand All @@ -82,6 +87,6 @@ public SerializeValueAttribute(int value)
{
Value = value;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,48 @@
namespace Microsoft.SqlTools.DataProtocol.Contracts.Utilities
{
internal class FlagsStringConverter : JsonConverter
{
{
public override bool CanWrite => true;
public override bool CanRead => true;

#region Public Methods

public override bool CanConvert(Type objectType)
{
return objectType.IsEnum && objectType.GetCustomAttribute(typeof(FlagsAttribute)) != null;
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// TODO: Fix to handle nullables properly

JToken jToken = JToken.Load(reader);
if (jToken.Type == JTokenType.Null)
{
return null;
}

string[] values = ((JArray) jToken).Values<string>().ToArray();
string[] values = ((JArray)jToken).Values<string>().ToArray();
var pureType = NullableUtils.GetUnderlyingTypeIfNullable(objectType);

FieldInfo[] enumFields = objectType.GetFields(BindingFlags.Public | BindingFlags.Static);
FieldInfo[] enumFields = pureType.GetFields(BindingFlags.Public | BindingFlags.Static);
int setFlags = 0;
foreach (FieldInfo enumField in enumFields)
{
{
// If there is a serialize value set for the enum value, look for the instead of the int value
SerializeValueAttribute serializeValue = enumField.GetCustomAttribute<SerializeValueAttribute>();
string searchValue = serializeValue?.Value ?? enumField.Name;
if (serializer.ContractResolver is CamelCasePropertyNamesContractResolver)
{
searchValue = char.ToLowerInvariant(searchValue[0]) + searchValue.Substring(1);
}

// If the value is in the json array, or the int value into the flags
if (Array.IndexOf(values, searchValue) >= 0)
{
setFlags |= (int) enumField.GetValue(null);
setFlags |= (int)enumField.GetValue(null);
}
}

return Enum.ToObject(objectType, setFlags);
return Enum.ToObject(pureType, setFlags);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
Expand All @@ -66,12 +65,12 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
foreach (FieldInfo enumField in enumFields)
{
// Make sure the flag is set before doing any other work
int enumValue = (int) enumField.GetValue(null);
if (((int) value & enumValue) == 0)
int enumValue = (int)enumField.GetValue(null);
if (((int)value & enumValue) == 0)
{
continue;
}

// If there is a serialize value set for the member, use that instead of the int value
SerializeValueAttribute serializeValue = enumField.GetCustomAttribute<SerializeValueAttribute>();
string flagValue = serializeValue?.Value ?? enumField.Name;
Expand All @@ -85,9 +84,9 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
string joinedFlags = string.Join(", ", setFlags);
writer.WriteRawValue($"[{joinedFlags}]");
}
#endregion

#endregion Public Methods

[AttributeUsage(AttributeTargets.Field)]
internal class SerializeValueAttribute : Attribute
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;

namespace Microsoft.SqlTools.DataProtocol.Contracts.Utilities
{
internal static class NullableUtils
{
/// <summary>
/// Determines whether the type is <see cref="Nullable{T}"/>.
/// </summary>
/// <param name="t">The type.</param>
/// <returns>
/// <c>true</c> if <paramref name="t"/> is <see cref="Nullable{T}"/>; otherwise, <c>false</c>.
/// </returns>
public static bool IsNullable(Type t)
{
return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>);
}

/// <summary>
/// Unwraps the <see cref="Nullable{T}"/> if necessary and returns the underlying value type.
/// </summary>
/// <param name="t">The type.</param>
/// <returns>The underlying value type the <see cref="Nullable{T}"/> type was produced from,
/// or the <paramref name="t"/> type if the type is not <see cref="Nullable{T}"/>.
/// </returns>
public static Type GetUnderlyingTypeIfNullable(Type t)
{
return IsNullable(t) ? Nullable.GetUnderlyingType(t) : t;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.SqlTools.DataProtocol.Contracts.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using Xunit;

namespace Microsoft.SqlTools.Hosting.UnitTests.Contracts.Utilities
{
public class FlagsIntConverterTests
{
[Fact]
public void NullableValueCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"optionalValue\": [1, 2]}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.NotNull(contract.OptionalValue);
Assert.Equal(TestFlags.FirstItem | TestFlags.SecondItem, contract.OptionalValue);
}

[Fact]
public void RegularValueCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"Value\": [1, 3]}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.Equal(TestFlags.FirstItem | TestFlags.ThirdItem, contract.Value);
}

[Fact]
public void ExplicitNullCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"optionalValue\": null}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.Null(contract.OptionalValue);
}

[Flags]
[JsonConverter(typeof(FlagsIntConverter))]
private enum TestFlags
{
[FlagsIntConverter.SerializeValue(1)]
FirstItem = 1 << 0,

[FlagsIntConverter.SerializeValue(2)]
SecondItem = 1 << 1,

[FlagsIntConverter.SerializeValue(3)]
ThirdItem = 1 << 2,
}

private class DataContract
{
public TestFlags? OptionalValue { get; set; }

public TestFlags Value { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using Microsoft.SqlTools.DataProtocol.Contracts.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;

namespace Microsoft.SqlTools.Hosting.UnitTests.Contracts.Utilities
{
public class FlagsStringConverterTests
{
[Fact]
public void NullableValueCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"optionalValue\": [\"First\", \"Second\"]}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.NotNull(contract.OptionalValue);
Assert.Equal(TestFlags.FirstItem | TestFlags.SecondItem, contract.OptionalValue);
}

[Fact]
public void RegularValueCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"Value\": [\"First\", \"Third\"]}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.Equal(TestFlags.FirstItem | TestFlags.ThirdItem, contract.Value);
}

[Fact]
public void ExplicitNullCanBeDeserialized()
{
var jsonObject = JObject.Parse("{\"optionalValue\": null}");
var contract = jsonObject.ToObject<DataContract>();
Assert.NotNull(contract);
Assert.Null(contract.OptionalValue);
}

[Flags]
[JsonConverter(typeof(FlagsStringConverter))]
private enum TestFlags
{
[FlagsStringConverter.SerializeValue("First")]
FirstItem = 1 << 0,

[FlagsStringConverter.SerializeValue("Second")]
SecondItem = 1 << 1,

[FlagsStringConverter.SerializeValue("Third")]
ThirdItem = 1 << 2,
}

private class DataContract
{
public TestFlags? OptionalValue { get; set; }

public TestFlags Value { get; set; }
}
}
}

0 comments on commit c6e3b33

Please sign in to comment.