-Refactored enum name writing

-Improved performance of reading and writing enum names
-Tests
This commit is contained in:
James Newton-King
2018-01-01 13:15:08 +13:00
parent 2244a82282
commit eb85ba3f7c
7 changed files with 454 additions and 47 deletions
@@ -201,6 +201,32 @@ namespace Newtonsoft.Json.Tests
]
}", sw.ToString());
}
[Test]
public async Task WriteLazy_Property()
{
LazyStringWriter sw = new LazyStringWriter(CultureInfo.InvariantCulture);
using (JsonTextWriter writer = new JsonTextWriter(sw))
{
writer.Indentation = 4;
writer.Formatting = Formatting.Indented;
await writer.WriteStartArrayAsync();
await writer.WriteStartObjectAsync();
await writer.WritePropertyNameAsync("IncompleteProp");
await writer.WriteEndArrayAsync();
}
StringAssert.AreEqual(@"[
{
""IncompleteProp"": null
}
]", sw.ToString());
}
#endif
[Test]
@@ -0,0 +1,311 @@
#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion
#if !(NET20 || NET35)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json.Utilities;
#if DNXCORE50
using Xunit;
using Test = Xunit.FactAttribute;
using Assert = Newtonsoft.Json.Tests.XUnitAssert;
using TestCaseSource = Xunit.MemberDataAttribute;
#else
using NUnit.Framework;
#endif
namespace Newtonsoft.Json.Tests.Utilities
{
public class EnumUtilsTests : TestFixtureBase
{
#if DNXCORE50
[Theory]
#endif
[TestCaseSource(nameof(Parse_TestData))]
public void Parse(string value, object expected)
{
Type enumType = expected.GetType();
Enum result = (Enum)EnumUtils.ParseEnumName(value, false, false, enumType);
Assert.AreEqual(expected, result);
}
#if DNXCORE50
[Theory]
#endif
[TestCaseSource(nameof(Parse_Invalid_TestData))]
public void Parse_Invalid(Type enumType, string value, Type exceptionType)
{
try
{
EnumUtils.ParseEnumName(value, false, false, enumType);
}
catch (Exception ex) when (ex.GetType() == exceptionType)
{
// nom nom nom
return;
}
Assert.Fail($"Expected {exceptionType.FullName} exception.");
}
#if DNXCORE50
[Theory]
#endif
[TestCaseSource(nameof(ToString_Format_TestData))]
public static void ToString_Format(Enum e, string expected)
{
EnumUtils.TryToString(e.GetType(), e, false, out string result);
Assert.AreEqual(expected, result);
}
#region Test data
// test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs
public static IEnumerable<object[]> Parse_TestData()
{
// SByte
yield return new object[] { "Min", SByteEnum.Min };
yield return new object[] { "mAx", SByteEnum.Max };
yield return new object[] { "1", SByteEnum.One };
yield return new object[] { "5", (SByteEnum)5 };
// Byte
yield return new object[] { "Min", ByteEnum.Min };
yield return new object[] { "mAx", ByteEnum.Max };
yield return new object[] { "1", ByteEnum.One };
yield return new object[] { "5", (ByteEnum)5 };
// Int16
yield return new object[] { "Min", Int16Enum.Min };
yield return new object[] { "mAx", Int16Enum.Max };
yield return new object[] { "1", Int16Enum.One };
yield return new object[] { "5", (Int16Enum)5 };
// UInt16
yield return new object[] { "Min", UInt16Enum.Min };
yield return new object[] { "mAx", UInt16Enum.Max };
yield return new object[] { "1", UInt16Enum.One };
yield return new object[] { "5", (UInt16Enum)5 };
// Int32
yield return new object[] { "Min", Int32Enum.Min };
yield return new object[] { "mAx", Int32Enum.Max };
yield return new object[] { "1", Int32Enum.One };
yield return new object[] { "5", (Int32Enum)5 };
// UInt32
yield return new object[] { "Min", UInt32Enum.Min };
yield return new object[] { "mAx", UInt32Enum.Max };
yield return new object[] { "1", UInt32Enum.One };
yield return new object[] { "5", (UInt32Enum)5 };
// Int64
yield return new object[] { "Min", Int64Enum.Min };
yield return new object[] { "mAx", Int64Enum.Max };
yield return new object[] { "1", Int64Enum.One };
yield return new object[] { "5", (Int64Enum)5 };
// UInt64
yield return new object[] { "Min", UInt64Enum.Min };
yield return new object[] { "mAx", UInt64Enum.Max };
yield return new object[] { "1", UInt64Enum.One };
yield return new object[] { "5", (UInt64Enum)5 };
// SimpleEnum
yield return new object[] { "Red", SimpleEnum.Red };
yield return new object[] { " Red", SimpleEnum.Red };
yield return new object[] { "Red ", SimpleEnum.Red };
yield return new object[] { " red ", SimpleEnum.Red };
yield return new object[] { "B", SimpleEnum.B };
yield return new object[] { "B,B", SimpleEnum.B };
yield return new object[] { " Red , Blue ", SimpleEnum.Red | SimpleEnum.Blue };
yield return new object[] { "Blue,Red,Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green };
yield return new object[] { "Blue,Red,Red,Red,Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green };
yield return new object[] { "Red,Blue, Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green };
yield return new object[] { "1", SimpleEnum.Red };
yield return new object[] { " 1 ", SimpleEnum.Red };
yield return new object[] { "2", SimpleEnum.Blue };
yield return new object[] { "99", (SimpleEnum)99 };
yield return new object[] { "-42", (SimpleEnum)(-42) };
yield return new object[] { " -42", (SimpleEnum)(-42) };
yield return new object[] { " -42 ", (SimpleEnum)(-42) };
}
// test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs
public static IEnumerable<object[]> Parse_Invalid_TestData()
{
// SimpleEnum
yield return new object[] { null, "", typeof(ArgumentNullException) };
yield return new object[] { typeof(SimpleEnum), null, typeof(ArgumentNullException) };
yield return new object[] { typeof(object), "", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), " \t", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Purple", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), ",Red", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Red,", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "B,", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), " , , ,", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Red,Blue,", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Red,,Blue", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Red,Blue, ", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Red Blue", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "1,Blue", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Blue,1", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "Blue, 1", typeof(ArgumentException) };
yield return new object[] { typeof(SimpleEnum), "2147483649", typeof(OverflowException) };
yield return new object[] { typeof(SimpleEnum), "2147483648", typeof(OverflowException) };
}
// test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs
public static IEnumerable<object[]> ToString_Format_TestData()
{
yield return new object[] { SByteEnum.Min, "Min" };
yield return new object[] { (SByteEnum)5, null };
yield return new object[] { SByteEnum.Max, "Max" };
yield return new object[] { ByteEnum.Min, "Min" };
yield return new object[] { (ByteEnum)5, null };
yield return new object[] { (ByteEnum)0xff, "Max" };
yield return new object[] { (ByteEnum)3, null };
yield return new object[] { Int16Enum.Min, "Min" };
yield return new object[] { (Int16Enum)5, null };
yield return new object[] { Int16Enum.Max, "Max" };
yield return new object[] { (Int16Enum)3, null };
yield return new object[] { UInt16Enum.Min, "Min" };
yield return new object[] { (UInt16Enum)5, null };
yield return new object[] { UInt16Enum.Max, "Max" };
yield return new object[] { (UInt16Enum)3, null };
yield return new object[] { Int32Enum.Min, "Min" };
yield return new object[] { (Int32Enum)5, null };
yield return new object[] { Int32Enum.Max, "Max" };
yield return new object[] { (Int32Enum)3, null };
yield return new object[] { UInt32Enum.Min, "Min" };
yield return new object[] { (UInt32Enum)5, null };
yield return new object[] { UInt32Enum.Max, "Max" };
yield return new object[] { (UInt32Enum)3, null };
yield return new object[] { Int64Enum.Min, "Min" };
yield return new object[] { (Int64Enum)5, null };
yield return new object[] { Int64Enum.Max, "Max" };
yield return new object[] { (Int64Enum)3, null };
yield return new object[] { UInt64Enum.Min, "Min" };
yield return new object[] { (UInt64Enum)5, null };
yield return new object[] { UInt64Enum.Max, "Max" };
yield return new object[] { (UInt64Enum)3, null };
yield return new object[] { SimpleEnum.Red, "Red" };
yield return new object[] { SimpleEnum.Blue, "Blue" };
yield return new object[] { (SimpleEnum)99, null };
yield return new object[] { (SimpleEnum)0, null };
yield return new object[] { AttributeTargets.Class | AttributeTargets.Delegate, "Class, Delegate" };
}
#endregion
}
public enum SimpleEnum
{
Red = 1,
Blue = 2,
Green = 3,
Green_a = 3,
Green_b = 3,
B = 4
}
public enum ByteEnum : byte
{
Min = byte.MinValue,
One = 1,
Two = 2,
Max = byte.MaxValue,
}
public enum SByteEnum : sbyte
{
Min = sbyte.MinValue,
One = 1,
Two = 2,
Max = sbyte.MaxValue,
}
public enum UInt16Enum : ushort
{
Min = ushort.MinValue,
One = 1,
Two = 2,
Max = ushort.MaxValue,
}
public enum Int16Enum : short
{
Min = short.MinValue,
One = 1,
Two = 2,
Max = short.MaxValue,
}
public enum UInt32Enum : uint
{
Min = uint.MinValue,
One = 1,
Two = 2,
Max = uint.MaxValue,
}
public enum Int32Enum : int
{
Min = int.MinValue,
One = 1,
Two = 2,
Max = int.MaxValue,
}
public enum UInt64Enum : ulong
{
Min = ulong.MinValue,
One = 1,
Two = 2,
Max = ulong.MaxValue,
}
public enum Int64Enum : long
{
Min = long.MinValue,
One = 1,
Two = 2,
Max = long.MaxValue,
}
}
#endif
@@ -93,13 +93,11 @@ namespace Newtonsoft.Json.Converters
Enum e = (Enum)value;
string enumName = e.ToString("G");
if (char.IsNumber(enumName[0]) || enumName[0] == '-')
if (!EnumUtils.TryToString(e.GetType(), value, CamelCaseText, out string enumName))
{
if (!AllowIntegerValues)
{
throw JsonSerializationException.Create(null, writer.ContainerPath, "Integer value {0} is not allowed.".FormatWith(CultureInfo.InvariantCulture, enumName), null);
throw JsonSerializationException.Create(null, writer.ContainerPath, "Integer value {0} is not allowed.".FormatWith(CultureInfo.InvariantCulture, e.ToString("D")), null);
}
// enum value has no name so write number
@@ -107,11 +105,7 @@ namespace Newtonsoft.Json.Converters
}
else
{
Type enumType = e.GetType();
string finalName = EnumUtils.ToEnumName(enumType, enumName, CamelCaseText);
writer.WriteValue(finalName);
writer.WriteValue(enumName);
}
}
+10 -10
View File
@@ -293,7 +293,7 @@ namespace Newtonsoft.Json
t = WriteNullAsync(cancellationToken);
if (!t.IsCompletedSucessfully())
{
return AwaitProperty(t, levelsToComplete, token, type, cancellationToken);
return AwaitProperty(t, levelsToComplete, token, cancellationToken);
}
}
@@ -304,7 +304,7 @@ namespace Newtonsoft.Json
t = WriteIndentAsync(cancellationToken);
if (!t.IsCompletedSucessfully())
{
return AwaitIndent(t, levelsToComplete, token, type, cancellationToken);
return AwaitIndent(t, levelsToComplete, token, cancellationToken);
}
}
}
@@ -312,7 +312,7 @@ namespace Newtonsoft.Json
t = WriteEndAsync(token, cancellationToken);
if (!t.IsCompletedSucessfully())
{
return AwaitEnd(t, levelsToComplete, type, cancellationToken);
return AwaitEnd(t, levelsToComplete, cancellationToken);
}
UpdateCurrentState();
@@ -321,7 +321,7 @@ namespace Newtonsoft.Json
return AsyncUtils.CompletedTask;
// Local functions, params renamed (capitalized) so as not to capture and allocate when calling async
async Task AwaitProperty(Task task, int LevelsToComplete, JsonToken token, JsonContainerType Type, CancellationToken CancellationToken)
async Task AwaitProperty(Task task, int LevelsToComplete, JsonToken token, CancellationToken CancellationToken)
{
await task.ConfigureAwait(false);
@@ -338,10 +338,10 @@ namespace Newtonsoft.Json
UpdateCurrentState();
await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false);
await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false);
}
async Task AwaitIndent(Task task, int LevelsToComplete, JsonToken token, JsonContainerType Type, CancellationToken CancellationToken)
async Task AwaitIndent(Task task, int LevelsToComplete, JsonToken token, CancellationToken CancellationToken)
{
await task.ConfigureAwait(false);
@@ -351,10 +351,10 @@ namespace Newtonsoft.Json
UpdateCurrentState();
await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false);
await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false);
}
async Task AwaitEnd(Task task, int LevelsToComplete, JsonContainerType Type, CancellationToken CancellationToken)
async Task AwaitEnd(Task task, int LevelsToComplete, CancellationToken CancellationToken)
{
await task.ConfigureAwait(false);
@@ -362,10 +362,10 @@ namespace Newtonsoft.Json
UpdateCurrentState();
await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false);
await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false);
}
async Task AwaitRemaining(int LevelsToComplete, JsonContainerType Type, CancellationToken CancellationToken)
async Task AwaitRemaining(int LevelsToComplete, CancellationToken CancellationToken)
{
while (LevelsToComplete-- > 0)
{
+1 -1
View File
@@ -82,7 +82,7 @@ namespace Newtonsoft.Json
State[] errorStates = StateArrayTempate[0];
State[] valueStates = StateArrayTempate[7];
TypeValuesAndNames enumValuesAndNames = EnumUtils.GetEnumValuesAndNames(typeof(JsonToken));
EnumInfo enumValuesAndNames = EnumUtils.GetEnumValuesAndNames(typeof(JsonToken));
foreach (ulong valueToken in enumValuesAndNames.Values)
{
@@ -309,7 +309,7 @@ namespace Newtonsoft.Json.Schema
{
CurrentSchema.Enum = new List<JToken>();
TypeValuesAndNames enumValues = EnumUtils.GetEnumValuesAndNames(type);
EnumInfo enumValues = EnumUtils.GetEnumValuesAndNames(type);
for (int i = 0; i < enumValues.Names.Length; i++)
{
ulong v = enumValues.Values[i];
+102 -26
View File
@@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Serialization;
#if !HAVE_LINQ
@@ -33,16 +34,18 @@ using Newtonsoft.Json.Utilities.LinqBridge;
using System.Linq;
#endif
using System.Reflection;
using System.Text;
namespace Newtonsoft.Json.Utilities
{
internal static class EnumUtils
{
private const char EnumSeparatorChar = ',';
private const string EnumSeparatorString = ", ";
private static readonly ThreadSafeStore<Type, TypeValuesAndNames> ValuesAndNamesPerEnum = new ThreadSafeStore<Type, TypeValuesAndNames>(InitializeValuesAndNames);
private static readonly ThreadSafeStore<Type, EnumInfo> ValuesAndNamesPerEnum = new ThreadSafeStore<Type, EnumInfo>(InitializeValuesAndNames);
private static TypeValuesAndNames InitializeValuesAndNames(Type enumType)
private static EnumInfo InitializeValuesAndNames(Type enumType)
{
string[] names = Enum.GetNames(enumType);
string[] resolvedNames = new string[names.Length];
@@ -72,7 +75,9 @@ namespace Newtonsoft.Json.Utilities
resolvedNames[i] = resolvedName;
}
return new TypeValuesAndNames(values, names, resolvedNames);
bool isFlags = enumType.IsDefined(typeof(FlagsAttribute), false);
return new EnumInfo(isFlags, values, names, resolvedNames);
}
public static IList<T> GetFlagsValues<T>(T value) where T : struct
@@ -87,7 +92,7 @@ namespace Newtonsoft.Json.Utilities
Type underlyingType = Enum.GetUnderlyingType(value.GetType());
ulong num = ToUInt64(value);
TypeValuesAndNames enumNameValues = GetEnumValuesAndNames(enumType);
EnumInfo enumNameValues = GetEnumValuesAndNames(enumType);
IList<T> selectedFlagsValues = new List<T>();
for (int i = 0; i < enumNameValues.Values.Length; i++)
@@ -118,34 +123,103 @@ namespace Newtonsoft.Json.Utilities
return ParseEnum(t, enumText, disallowValue);
}
public static string ToEnumName(Type enumType, string enumText, bool camelCaseText)
public static bool TryToString(Type enumType, object value, bool camelCaseText, out string name)
{
TypeValuesAndNames enumValuesAndNames = ValuesAndNamesPerEnum.Get(enumType);
EnumInfo enumInfo = ValuesAndNamesPerEnum.Get(enumType);
ulong v = ToUInt64(value);
string[] names = enumText.Split(',');
for (int i = 0; i < names.Length; i++)
if (!enumInfo.IsFlags)
{
string name = names[i].Trim();
int? matchingIndex = FindIndexByName(enumValuesAndNames.Names, name, 0, name.Length, StringComparison.Ordinal);
string resolvedEnumName = matchingIndex != null
? enumValuesAndNames.ResolvedNames[matchingIndex.Value]
: name;
if (camelCaseText)
int index = Array.BinarySearch(enumInfo.Values, v);
if (index >= 0)
{
resolvedEnumName = StringUtils.ToCamelCase(resolvedEnumName);
name = enumInfo.ResolvedNames[index];
if (camelCaseText)
{
name = StringUtils.ToCamelCase(name);
}
return true;
}
names[i] = resolvedEnumName;
// is number value
name = null;
return false;
}
else // These are flags OR'ed together (We treat everything as unsigned types)
{
name = InternalFlagsFormat(enumInfo, v, camelCaseText);
return name != null;
}
string finalName = string.Join(", ", names);
return finalName;
}
public static TypeValuesAndNames GetEnumValuesAndNames(Type enumType)
private static String InternalFlagsFormat(EnumInfo entry, ulong result, bool camelCaseText)
{
string[] resolvedNames = entry.ResolvedNames;
ulong[] values = entry.Values;
int index = values.Length - 1;
StringBuilder sb = new StringBuilder();
bool firstTime = true;
ulong saveResult = result;
// We will not optimize this code further to keep it maintainable. There are some boundary checks that can be applied
// to minimize the comparsions required. This code works the same for the best/worst case. In general the number of
// items in an enum are sufficiently small and not worth the optimization.
while (index >= 0)
{
if (index == 0 && values[index] == 0)
{
break;
}
if ((result & values[index]) == values[index])
{
result -= values[index];
if (!firstTime)
{
sb.Insert(0, EnumSeparatorString);
}
string resolvedName = resolvedNames[index];
sb.Insert(0, camelCaseText ? StringUtils.ToCamelCase(resolvedName) : resolvedName);
firstTime = false;
}
index--;
}
string returnString;
if (result != 0)
{
// We were unable to represent this number as a bitwise or of valid flags
returnString = null; // return null so the caller knows to .ToString() the input
}
else if (saveResult == 0)
{
// For the cases when we have zero
if (values.Length > 0 && values[0] == 0)
{
returnString = resolvedNames[0]; // Zero was one of the enum values.
if (camelCaseText)
{
returnString = StringUtils.ToCamelCase(returnString);
}
}
else
{
returnString = null;
}
}
else
{
returnString = sb.ToString(); // Return the string representation
}
return returnString;
}
public static EnumInfo GetEnumValuesAndNames(Type enumType)
{
return ValuesAndNamesPerEnum.Get(enumType);
}
@@ -193,7 +267,7 @@ namespace Newtonsoft.Json.Utilities
throw new ArgumentException("Type provided must be an Enum.", nameof(enumType));
}
TypeValuesAndNames entry = ValuesAndNamesPerEnum.Get(enumType);
EnumInfo entry = ValuesAndNamesPerEnum.Get(enumType);
string[] enumNames = entry.Names;
string[] resolvedNames = entry.ResolvedNames;
ulong[] enumValues = entry.Values;
@@ -333,15 +407,17 @@ namespace Newtonsoft.Json.Utilities
}
}
internal class TypeValuesAndNames
internal class EnumInfo
{
public TypeValuesAndNames(ulong[] values, string[] names, string[] resolvedNames)
public EnumInfo(bool isFlags, ulong[] values, string[] names, string[] resolvedNames)
{
IsFlags = isFlags;
Values = values;
Names = names;
ResolvedNames = resolvedNames;
}
public readonly bool IsFlags;
public readonly ulong[] Values;
public readonly string[] Names;
public readonly string[] ResolvedNames;