From c54ddc76da61c50576eff92e3add9caf7107f8af Mon Sep 17 00:00:00 2001 From: Steven He Date: Sun, 4 Aug 2019 11:36:06 +0800 Subject: [PATCH] Add support for JsonSnakeNamingPolicy * Add JsonSnakeNamingPolicy support * Add tests for JsonSnakeNamingPolicy --- src/System.Text.Json/ref/System.Text.Json.cs | 1 + .../src/System.Text.Json.csproj | 1 + .../Json/Serialization/JsonNamingPolicy.cs | 5 + .../JsonSnakeCaseNamingPolicy.cs | 81 ++++++++++++++++ .../tests/NewtonsoftTests/CamelCaseTests.cs | 20 ---- .../tests/NewtonsoftTests/NamingTestBase.cs | 46 ++++++++++ .../tests/NewtonsoftTests/SnakeCaseTests.cs | 92 +++++++++++++++++++ .../tests/Serialization/SnakeCaseUnitTests.cs | 50 ++++++++++ .../tests/System.Text.Json.Tests.csproj | 3 + 9 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 src/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs create mode 100644 src/System.Text.Json/tests/NewtonsoftTests/NamingTestBase.cs create mode 100644 src/System.Text.Json/tests/NewtonsoftTests/SnakeCaseTests.cs create mode 100644 src/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs diff --git a/src/System.Text.Json/ref/System.Text.Json.cs b/src/System.Text.Json/ref/System.Text.Json.cs index c9560f4e005e..d670fdacba3d 100644 --- a/src/System.Text.Json/ref/System.Text.Json.cs +++ b/src/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/System.Text.Json/src/System.Text.Json.csproj b/src/System.Text.Json/src/System.Text.Json.csproj index 8d2f4ddc7fb4..33b38624956e 100644 --- a/src/System.Text.Json/src/System.Text.Json.csproj +++ b/src/System.Text.Json/src/System.Text.Json.csproj @@ -129,6 +129,7 @@ + diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs index 00c2f55b2e24..d461e6885319 100644 --- a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs @@ -19,6 +19,11 @@ protected JsonNamingPolicy() { } /// public static JsonNamingPolicy CamelCase { get; } = new JsonCamelCaseNamingPolicy(); + /// + /// Returns the naming policy for snake-casing. + /// + public static JsonNamingPolicy SnakeCase { get; } = new JsonSnakeCaseNamingPolicy(); + internal static JsonNamingPolicy Default { get; } = new JsonDefaultNamingPolicy(); /// diff --git a/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs new file mode 100644 index 000000000000..5f56f07c2460 --- /dev/null +++ b/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSnakeCaseNamingPolicy.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Text.Json +{ + internal sealed class JsonSnakeCaseNamingPolicy : JsonNamingPolicy + { + internal enum SnakeCaseState + { + Start, + Lower, + Upper, + NewWord + } + + public override string ConvertName(string name) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + + var sb = new StringBuilder(); + var state = SnakeCaseState.Start; + + var nameSpan = name.AsSpan(); + + for (int i = 0; i < nameSpan.Length; i++) + { + if (nameSpan[i] == ' ') + { + if (state != SnakeCaseState.Start) + { + state = SnakeCaseState.NewWord; + } + } + else if (char.IsUpper(nameSpan[i])) + { + switch (state) + { + case SnakeCaseState.Upper: + bool hasNext = (i + 1 < nameSpan.Length); + if (i > 0 && hasNext) + { + char nextChar = nameSpan[i + 1]; + if (!char.IsUpper(nextChar) && nextChar != '_') + { + sb.Append('_'); + } + } + break; + case SnakeCaseState.Lower: + case SnakeCaseState.NewWord: + sb.Append('_'); + break; + } + sb.Append(char.ToLowerInvariant(nameSpan[i])); + state = SnakeCaseState.Upper; + } + else if (nameSpan[i] == '_') + { + sb.Append('_'); + state = SnakeCaseState.Start; + } + else + { + if (state == SnakeCaseState.NewWord) + { + sb.Append('_'); + } + + sb.Append(nameSpan[i]); + state = SnakeCaseState.Lower; + } + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/System.Text.Json/tests/NewtonsoftTests/CamelCaseTests.cs b/src/System.Text.Json/tests/NewtonsoftTests/CamelCaseTests.cs index d30d654bd03d..2a70e46bfed9 100644 --- a/src/System.Text.Json/tests/NewtonsoftTests/CamelCaseTests.cs +++ b/src/System.Text.Json/tests/NewtonsoftTests/CamelCaseTests.cs @@ -30,26 +30,6 @@ namespace System.Text.Json.Tests { - internal class Person - { - public string Name { get; set; } - - public DateTime BirthDate { get; set; } - - public DateTime LastModified { get; set; } - - [JsonIgnore] - public string Department { get; set; } - } - - internal class Product - { - public string Name { get; set; } - public DateTime ExpiryDate { get; set;} - public decimal Price { get; set; } - public string[] Sizes { get; set; } - } - public class CamelCaseTests { private static readonly JsonSerializerOptions s_camelCaseAndIndentedOption = new JsonSerializerOptions diff --git a/src/System.Text.Json/tests/NewtonsoftTests/NamingTestBase.cs b/src/System.Text.Json/tests/NewtonsoftTests/NamingTestBase.cs new file mode 100644 index 000000000000..bf4f59747ca5 --- /dev/null +++ b/src/System.Text.Json/tests/NewtonsoftTests/NamingTestBase.cs @@ -0,0 +1,46 @@ +// 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. + +using System.Text.Json.Serialization; +namespace System.Text.Json.Tests +{ + internal class Person + { + public string Name { get; set; } + + public DateTime BirthDate { get; set; } + + public DateTime LastModified { get; set; } + + [JsonIgnore] + public string Department { get; set; } + } + + internal class Product + { + public string Name { get; set; } + public DateTime ExpiryDate { get; set;} + public decimal Price { get; set; } + public string[] Sizes { get; set; } + } +} diff --git a/src/System.Text.Json/tests/NewtonsoftTests/SnakeCaseTests.cs b/src/System.Text.Json/tests/NewtonsoftTests/SnakeCaseTests.cs new file mode 100644 index 000000000000..6891853d0346 --- /dev/null +++ b/src/System.Text.Json/tests/NewtonsoftTests/SnakeCaseTests.cs @@ -0,0 +1,92 @@ +// 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. + +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.Tests +{ + public class SnakeCaseTests + { + private static readonly JsonSerializerOptions s_snakeCaseAndIndentedOption = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCase, + WriteIndented = true, + }; + + [Fact] + public void JsonSerializerSnakeCaseSettings() + { + Person person = new Person(); + person.BirthDate = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.LastModified = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.Name = "Name!"; + + string json = JsonSerializer.Serialize(person, s_snakeCaseAndIndentedOption); + + Assert.Equal(@"{ + ""name"": ""Name!"", + ""birth_date"": ""2000-11-20T23:55:44Z"", + ""last_modified"": ""2000-11-20T23:55:44Z"" +}", json); + + Person deserializedPerson = JsonSerializer.Deserialize(json, s_snakeCaseAndIndentedOption); + + Assert.Equal(person.BirthDate, deserializedPerson.BirthDate); + Assert.Equal(person.LastModified, deserializedPerson.LastModified); + Assert.Equal(person.Name, deserializedPerson.Name); + + json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true }); + Assert.Equal(@"{ + ""Name"": ""Name!"", + ""BirthDate"": ""2000-11-20T23:55:44Z"", + ""LastModified"": ""2000-11-20T23:55:44Z"" +}", json); + } + + [Fact] + public void BlogPostExample() + { + Product product = new Product + { + ExpiryDate = new DateTime(2010, 12, 20, 18, 1, 0, DateTimeKind.Utc), + Name = "Widget", + Price = 9.99m, + Sizes = new[] { "Small", "Medium", "Large" } + }; + + string json = JsonSerializer.Serialize(product, s_snakeCaseAndIndentedOption); + + Assert.Equal(@"{ + ""name"": ""Widget"", + ""expiry_date"": ""2010-12-20T18:01:00Z"", + ""price"": 9.99, + ""sizes"": [ + ""Small"", + ""Medium"", + ""Large"" + ] +}", json); + } + } +} \ No newline at end of file diff --git a/src/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs b/src/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs new file mode 100644 index 000000000000..3145644861cb --- /dev/null +++ b/src/System.Text.Json/tests/Serialization/SnakeCaseUnitTests.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static class SnakeCaseUnitTests + { + [Fact] + public static void ConvertToSnakeCaseTest() + { + // 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/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj index c60ca78db8fe..b10c99c19f86 100644 --- a/src/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -32,6 +32,8 @@ + + @@ -68,6 +70,7 @@ +