From 869fa14c4df8806317b388b1b3a98232029bfc2d Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 9 Sep 2020 17:24:12 +0800 Subject: [PATCH 1/3] Copy snake case implementation from Json.NET. --- .../src/System.Text.Json.csproj | 2 + .../Json/Serialization/JsonNamingPolicy.cs | 5 ++ .../JsonSeparatedCaseNamingPolicy.cs | 82 +++++++++++++++++++ .../JsonSnakeCaseNamingPolicy.cs | 10 +++ 4 files changed, 99 insertions(+) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSeparatedCaseNamingPolicy.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index e7bec05b27ee0..b0cd66d8227b3 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -144,6 +144,7 @@ + @@ -161,6 +162,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs index 30971aecce26b..d305e5dc29441 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs @@ -20,6 +20,11 @@ protected JsonNamingPolicy() { } internal static JsonNamingPolicy Default { get; } = new JsonDefaultNamingPolicy(); + /// + /// Returns the naming policy for snake-casing. + /// + public static JsonNamingPolicy SnakeCase { get; } = new JsonSnakeCaseNamingPolicy(); + /// /// When overridden in a derived class, converts the specified name according to the policy. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSeparatedCaseNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSeparatedCaseNamingPolicy.cs new file mode 100644 index 0000000000000..030833ac837c8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSeparatedCaseNamingPolicy.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json +{ + internal abstract class JsonSeparatedCaseNamingPolicy : JsonNamingPolicy + { + private enum SeparatedCaseState + { + Start, + NewWord, + Upper, + Lower + } + + protected static string ToSeparatedCase(string s, char separator) + { + if (string.IsNullOrEmpty(s)) + { + return s; + } + + StringBuilder sb = new StringBuilder(); + SeparatedCaseState state = SeparatedCaseState.Start; + + for (int i = 0; i < s.Length; i++) + { + if (s[i] == ' ') + { + if (state != SeparatedCaseState.Start) + { + state = SeparatedCaseState.NewWord; + } + } + else if (char.IsUpper(s[i])) + { + switch (state) + { + case SeparatedCaseState.Upper: + bool hasNext = (i + 1 < s.Length); + if (i > 0 && hasNext) + { + char nextChar = s[i + 1]; + if (!char.IsUpper(nextChar) && nextChar != separator) + { + sb.Append(separator); + } + } + break; + case SeparatedCaseState.Lower: + case SeparatedCaseState.NewWord: + sb.Append(separator); + break; + } + + char c; + c = char.ToLowerInvariant(s[i]); + sb.Append(c); + + state = SeparatedCaseState.Upper; + } + else if (s[i] == separator) + { + sb.Append(separator); + state = SeparatedCaseState.Start; + } + else + { + if (state == SeparatedCaseState.NewWord) + { + sb.Append(separator); + } + + sb.Append(s[i]); + state = SeparatedCaseState.Lower; + } + } + + return sb.ToString(); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs new file mode 100644 index 0000000000000..cf23a6168fdd8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json +{ + internal class JsonSnakeCaseNamingPolicy : JsonSeparatedCaseNamingPolicy + { + public override string ConvertName(string name) => ToSeparatedCase(name, '_'); + } +} From 02979c915a2f48390946c4cfa44b411b62f9a114 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 9 Sep 2020 17:28:37 +0800 Subject: [PATCH 2/3] Also add KebabCase but keep internal. --- .../System.Text.Json/src/System.Text.Json.csproj | 1 + .../Json/Serialization/JsonKebabCaseNamingPolicy.cs | 10 ++++++++++ .../System/Text/Json/Serialization/JsonNamingPolicy.cs | 4 ++++ 3 files changed, 15 insertions(+) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonKebabCaseNamingPolicy.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index b0cd66d8227b3..f01c7db77b65f 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -137,6 +137,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonKebabCaseNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonKebabCaseNamingPolicy.cs new file mode 100644 index 0000000000000..5d79165c556a2 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonKebabCaseNamingPolicy.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json +{ + internal class JsonKebabCaseNamingPolicy : JsonSeparatedCaseNamingPolicy + { + public override string ConvertName(string name) => ToSeparatedCase(name, '-'); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs index d305e5dc29441..0035a31b28b00 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs @@ -25,6 +25,10 @@ protected JsonNamingPolicy() { } /// public static JsonNamingPolicy SnakeCase { get; } = new JsonSnakeCaseNamingPolicy(); + // KebabCase was added together with SnakeCase because sharing implementation. + // Once approved it can be made public. + internal static JsonNamingPolicy KebabCase { get; } = new JsonKebabCaseNamingPolicy(); + /// /// When overridden in a derived class, converts the specified name according to the policy. /// From cf9a7ee42b39048bb932bdb04de19f6044ac4484 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 9 Sep 2020 17:56:23 +0800 Subject: [PATCH 3/3] Add tests for snake case. --- .../System.Text.Json/ref/System.Text.Json.cs | 1 + .../tests/Serialization/SnakeCaseUnitTests.cs | 48 +++++++++++++++++++ .../tests/System.Text.Json.Tests.csproj | 1 + 3 files changed, 50 insertions(+) create mode 100644 src/libraries/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 0e70895b1b272..af8c17d942930 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -154,6 +154,7 @@ public abstract partial class JsonNamingPolicy { protected JsonNamingPolicy() { } public static System.Text.Json.JsonNamingPolicy CamelCase { get { throw null; } } + public static System.Text.Json.JsonNamingPolicy SnakeCase { get { throw null; } } public abstract string ConvertName(string name); } public readonly partial struct JsonProperty diff --git a/src/libraries/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs b/src/libraries/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs new file mode 100644 index 0000000000000..cf0028cd916b6 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public class SnakeCaseUnitTests + { + [Fact] + public static void ToCamelCaseTest() + { + // These test cases were copied from Json.NET. + Assert.Equal("url_value", ConvertToSnakeCase("URLValue")); + Assert.Equal("url", ConvertToSnakeCase("URL")); + Assert.Equal("id", ConvertToSnakeCase("ID")); + Assert.Equal("i", ConvertToSnakeCase("I")); + Assert.Equal("", ConvertToSnakeCase("")); + Assert.Null(ConvertToSnakeCase(null)); + Assert.Equal("person", ConvertToSnakeCase("Person")); + Assert.Equal("i_phone", ConvertToSnakeCase("iPhone")); + Assert.Equal("i_phone", ConvertToSnakeCase("IPhone")); + Assert.Equal("i_phone", ConvertToSnakeCase("I Phone")); + Assert.Equal("i_phone", ConvertToSnakeCase("I Phone")); + Assert.Equal("i_phone", ConvertToSnakeCase(" IPhone")); + Assert.Equal("i_phone", ConvertToSnakeCase(" IPhone ")); + Assert.Equal("is_cia", ConvertToSnakeCase("IsCIA")); + Assert.Equal("vm_q", ConvertToSnakeCase("VmQ")); + Assert.Equal("xml2_json", ConvertToSnakeCase("Xml2Json")); + Assert.Equal("sn_ak_ec_as_e", ConvertToSnakeCase("SnAkEcAsE")); + Assert.Equal("sn_a__k_ec_as_e", ConvertToSnakeCase("SnA__kEcAsE")); + Assert.Equal("sn_a__k_ec_as_e", ConvertToSnakeCase("SnA__ kEcAsE")); + Assert.Equal("already_snake_case_", ConvertToSnakeCase("already_snake_case_ ")); + Assert.Equal("is_json_property", ConvertToSnakeCase("IsJSONProperty")); + Assert.Equal("shouting_case", ConvertToSnakeCase("SHOUTING_CASE")); + Assert.Equal("9999-12-31_t23:59:59.9999999_z", ConvertToSnakeCase("9999-12-31T23:59:59.9999999Z")); + Assert.Equal("hi!!_this_is_text._time_to_test.", ConvertToSnakeCase("Hi!! This is text. Time to test.")); + } + + // Use a helper method since the method is not public. + private static string ConvertToSnakeCase(string name) + { + JsonNamingPolicy policy = JsonNamingPolicy.SnakeCase; + string value = policy.ConvertName(name); + return value; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 6e1e66c690248..d5c6ad177162d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -114,6 +114,7 @@ +