From c6e3b33c3503a0e4ac14620340098d13b0e00603 Mon Sep 17 00:00:00 2001 From: sergei-boguslavski <54648384+sergei-boguslavski@users.noreply.github.com> Date: Thu, 29 Aug 2019 10:35:45 -0700 Subject: [PATCH] add flag converters nullable serialization (#857) --- .../AssemblyInfo.cs | 3 + .../Utilities/FlagsIntConverter.cs | 33 ++++++---- .../Utilities/FlagsStringConverter.cs | 35 +++++----- .../Utilities/NullableUtils.cs | 36 ++++++++++ .../Utilities/FlagsIntConverterTests.cs | 65 +++++++++++++++++++ .../Utilities/FlagsStringConverterTests.cs | 65 +++++++++++++++++++ 6 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.SqlTools.DataProtocol.Contracts/AssemblyInfo.cs create mode 100644 src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/NullableUtils.cs create mode 100644 test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsIntConverterTests.cs create mode 100644 test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsStringConverterTests.cs diff --git a/src/Microsoft.SqlTools.DataProtocol.Contracts/AssemblyInfo.cs b/src/Microsoft.SqlTools.DataProtocol.Contracts/AssemblyInfo.cs new file mode 100644 index 0000000000..18de488c91 --- /dev/null +++ b/src/Microsoft.SqlTools.DataProtocol.Contracts/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.SqlTools.Hosting.UnitTests")] \ No newline at end of file diff --git a/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsIntConverter.cs b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsIntConverter.cs index 32bede37b6..1b332a3ca8 100644 --- a/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsIntConverter.cs +++ b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsIntConverter.cs @@ -18,7 +18,7 @@ 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; @@ -26,16 +26,21 @@ public override bool CanConvert(Type objectType) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - // TODO: Fix to handle nullables properly - - int[] values = JArray.Load(reader).Values().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().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(); int searchValue = serializeValue?.Value ?? enumValue; @@ -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) @@ -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(); - 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 @@ -82,6 +87,6 @@ public SerializeValueAttribute(int value) { Value = value; } - } + } } } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsStringConverter.cs b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsStringConverter.cs index d91501c8de..827e5cb84b 100644 --- a/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsStringConverter.cs +++ b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/FlagsStringConverter.cs @@ -14,33 +14,32 @@ 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().ToArray(); + string[] values = ((JArray)jToken).Values().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(); string searchValue = serializeValue?.Value ?? enumField.Name; @@ -48,15 +47,15 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { 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) @@ -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(); string flagValue = serializeValue?.Value ?? enumField.Name; @@ -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 { diff --git a/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/NullableUtils.cs b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/NullableUtils.cs new file mode 100644 index 0000000000..b84934dca7 --- /dev/null +++ b/src/Microsoft.SqlTools.DataProtocol.Contracts/Utilities/NullableUtils.cs @@ -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 + { + /// + /// Determines whether the type is . + /// + /// The type. + /// + /// true if is ; otherwise, false. + /// + public static bool IsNullable(Type t) + { + return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + /// + /// Unwraps the if necessary and returns the underlying value type. + /// + /// The type. + /// The underlying value type the type was produced from, + /// or the type if the type is not . + /// + public static Type GetUnderlyingTypeIfNullable(Type t) + { + return IsNullable(t) ? Nullable.GetUnderlyingType(t) : t; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsIntConverterTests.cs b/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsIntConverterTests.cs new file mode 100644 index 0000000000..a67b363076 --- /dev/null +++ b/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsIntConverterTests.cs @@ -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(); + 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(); + 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(); + 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; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsStringConverterTests.cs b/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsStringConverterTests.cs new file mode 100644 index 0000000000..aaa539706d --- /dev/null +++ b/test/Microsoft.SqlTools.Hosting.UnitTests/Contracts/Utilities/FlagsStringConverterTests.cs @@ -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(); + 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(); + 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(); + 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; } + } + } +} \ No newline at end of file