diff --git a/Src/Newtonsoft.Json.Tests/Documentation/Samples/Serializer/NamingStrategyKebabCase.cs b/Src/Newtonsoft.Json.Tests/Documentation/Samples/Serializer/NamingStrategyKebabCase.cs new file mode 100644 index 000000000..dfe50dba3 --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Documentation/Samples/Serializer/NamingStrategyKebabCase.cs @@ -0,0 +1,86 @@ +#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 + +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +#if DNXCORE50 +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +#else +using NUnit.Framework; +#endif + +namespace Newtonsoft.Json.Tests.Documentation.Samples.Serializer +{ + [TestFixture] + public class NamingStrategyKebabCase : TestFixtureBase + { + #region Types + public class User + { + public string UserName { get; set; } + public bool Enabled { get; set; } + } + #endregion + + [Test] + public void Example() + { + #region Usage + User user1 = new User + { + UserName = "jamesn", + Enabled = true + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(user1, new JsonSerializerSettings + { + ContractResolver = contractResolver, + Formatting = Formatting.Indented + }); + + Console.WriteLine(json); + // { + // "user-name": "jamesn", + // "enabled": true + // } + #endregion + + StringAssert.AreEqual(@"{ + ""user-name"": ""jamesn"", + ""enabled"": true +}", json); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Serialization/KebabCaseNamingStrategyTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/KebabCaseNamingStrategyTests.cs new file mode 100644 index 000000000..5737607a7 --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Serialization/KebabCaseNamingStrategyTests.cs @@ -0,0 +1,324 @@ +#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 + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +#if DNXCORE50 +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +#else +using NUnit.Framework; +#endif +using Newtonsoft.Json.Tests.TestObjects; +using Newtonsoft.Json.Tests.TestObjects.Organization; +using Newtonsoft.Json.Linq; +using System.Reflection; +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Tests.Serialization +{ + [TestFixture] + public class KebabCaseNamingStrategyTests : TestFixtureBase + { + [Test] + public void JsonConvertSerializerSettings() + { + 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!"; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(person, Formatting.Indented, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""name"": ""Name!"", + ""birth-date"": ""2000-11-20T23:55:44Z"", + ""last-modified"": ""2000-11-20T23:55:44Z"" +}", json); + + Person deserializedPerson = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + Assert.AreEqual(person.BirthDate, deserializedPerson.BirthDate); + Assert.AreEqual(person.LastModified, deserializedPerson.LastModified); + Assert.AreEqual(person.Name, deserializedPerson.Name); + + json = JsonConvert.SerializeObject(person, Formatting.Indented); + StringAssert.AreEqual(@"{ + ""Name"": ""Name!"", + ""BirthDate"": ""2000-11-20T23:55:44Z"", + ""LastModified"": ""2000-11-20T23:55:44Z"" +}", json); + } + + [Test] + public void JTokenWriter_OverrideSpecifiedName() + { + JsonIgnoreAttributeOnClassTestClass ignoreAttributeOnClassTestClass = new JsonIgnoreAttributeOnClassTestClass(); + ignoreAttributeOnClassTestClass.Field = int.MinValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy + { + OverrideSpecifiedNames = true + } + }; + + JsonSerializer serializer = new JsonSerializer(); + serializer.ContractResolver = contractResolver; + + JTokenWriter writer = new JTokenWriter(); + + serializer.Serialize(writer, ignoreAttributeOnClassTestClass); + + JObject o = (JObject)writer.Token; + JProperty p = o.Property("the-field"); + + Assert.IsNotNull(p); + Assert.AreEqual(int.MinValue, (int)p.Value); + } + + [Test] + 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" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + + string json = + JsonConvert.SerializeObject( + product, + Formatting.Indented, + new JsonSerializerSettings { ContractResolver = contractResolver } + ); + + //{ + // "name": "Widget", + // "expiryDate": "\/Date(1292868060000)\/", + // "price": 9.99, + // "sizes": [ + // "Small", + // "Medium", + // "Large" + // ] + //} + + StringAssert.AreEqual(@"{ + ""name"": ""Widget"", + ""expiry-date"": ""2010-12-20T18:01:00Z"", + ""price"": 9.99, + ""sizes"": [ + ""Small"", + ""Medium"", + ""Large"" + ] +}", json); + } + +#if !(NET35 || NET20 || PORTABLE40) + [Test] + public void DynamicKebabCasePropertyNames() + { + dynamic o = new TestDynamicObject(); + o.Text = "Text!"; + o.Integer = int.MaxValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(o, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""explicit"": false, + ""text"": ""Text!"", + ""integer"": 2147483647, + ""int"": 0, + ""child-object"": null +}", json); + } +#endif + + [Test] + public void DictionaryKebabCasePropertyNames_Disabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""First"": ""Value1!"", + ""Second"": ""Value2!"" +}", json); + } + + [Test] + public void DictionaryKebabCasePropertyNames_Enabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""first"": ""Value1!"", + ""second"": ""Value2!"" +}", json); + } + + public class PropertyAttributeNamingStrategyTestClass + { + [JsonProperty] + public string HasNoAttributeNamingStrategy { get; set; } + + [JsonProperty(NamingStrategyType = typeof(KebabCaseNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonPropertyAttribute_NamingStrategyType() + { + PropertyAttributeNamingStrategyTestClass c = new PropertyAttributeNamingStrategyTestClass + { + HasNoAttributeNamingStrategy = "Value1!", + HasAttributeNamingStrategy = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""HasNoAttributeNamingStrategy"": ""Value1!"", + ""has-attribute-naming-strategy"": ""Value2!"" +}", json); + } + + [JsonObject(NamingStrategyType = typeof(KebabCaseNamingStrategy))] + public class ContainerAttributeNamingStrategyTestClass + { + public string Prop1 { get; set; } + public string Prop2 { get; set; } + [JsonProperty(NamingStrategyType = typeof(DefaultNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonObjectAttribute_NamingStrategyType() + { + ContainerAttributeNamingStrategyTestClass c = new ContainerAttributeNamingStrategyTestClass + { + Prop1 = "Value1!", + Prop2 = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""prop1"": ""Value1!"", + ""prop2"": ""Value2!"", + ""HasAttributeNamingStrategy"": null +}", json); + } + + [JsonDictionary(NamingStrategyType = typeof(KebabCaseNamingStrategy), NamingStrategyParameters = new object[] { true, true })] + public class DictionaryAttributeNamingStrategyTestClass : Dictionary + { + } + + [Test] + public void JsonDictionaryAttribute_NamingStrategyType() + { + DictionaryAttributeNamingStrategyTestClass c = new DictionaryAttributeNamingStrategyTestClass + { + ["Key1"] = "Value1!", + ["Key2"] = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""key1"": ""Value1!"", + ""key2"": ""Value2!"" +}", json); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Serialization/NamingStrategyEquality.cs b/Src/Newtonsoft.Json.Tests/Serialization/NamingStrategyEquality.cs index 3bb0af55d..591a2906e 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/NamingStrategyEquality.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/NamingStrategyEquality.cs @@ -97,6 +97,25 @@ public void SnakeCaseNamingStrategyEqualityVariants() CheckInequality(true, true, true); } + [Test] + public void KebabCaseStrategyEquality() + { + var s1 = new KebabCaseNamingStrategy(); + var s2 = new KebabCaseNamingStrategy(); + Assert.IsTrue(s1.Equals(s2)); + Assert.IsTrue(s1.GetHashCode() == s2.GetHashCode()); + } + + [Test] + public void KebabCaseNamingStrategyEqualityVariants() + { + CheckInequality(false, false, true); + CheckInequality(false, true, false); + CheckInequality(true, false, false); + CheckInequality(false, true, true); + CheckInequality(true, true, false); + CheckInequality(true, true, true); + } [Test] public void DifferentStrategyEquality() diff --git a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs index 85ff18655..4d8965ace 100644 --- a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs +++ b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs @@ -98,5 +98,34 @@ public void ToSnakeCaseTest() Assert.AreEqual("9999-12-31_t23:59:59.9999999_z", StringUtils.ToSnakeCase("9999-12-31T23:59:59.9999999Z")); Assert.AreEqual("hi!!_this_is_text._time_to_test.", StringUtils.ToSnakeCase("Hi!! This is text. Time to test.")); } + + [Test] + public void ToKebabCaseTest() + { + Assert.AreEqual("url-value", StringUtils.ToKebabCase("URLValue")); + Assert.AreEqual("url", StringUtils.ToKebabCase("URL")); + Assert.AreEqual("id", StringUtils.ToKebabCase("ID")); + Assert.AreEqual("i", StringUtils.ToKebabCase("I")); + Assert.AreEqual("", StringUtils.ToKebabCase("")); + Assert.AreEqual(null, StringUtils.ToKebabCase(null)); + Assert.AreEqual("person", StringUtils.ToKebabCase("Person")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase("iPhone")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase("IPhone")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase("I Phone")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase("I Phone")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase(" IPhone")); + Assert.AreEqual("i-phone", StringUtils.ToKebabCase(" IPhone ")); + Assert.AreEqual("is-cia", StringUtils.ToKebabCase("IsCIA")); + Assert.AreEqual("vm-q", StringUtils.ToKebabCase("VmQ")); + Assert.AreEqual("xml2-json", StringUtils.ToKebabCase("Xml2Json")); + Assert.AreEqual("ke-ba-bc-as-e", StringUtils.ToKebabCase("KeBaBcAsE")); + Assert.AreEqual("ke-b--a-bc-as-e", StringUtils.ToKebabCase("KeB--aBcAsE")); + Assert.AreEqual("ke-b--a-bc-as-e", StringUtils.ToKebabCase("KeB-- aBcAsE")); + Assert.AreEqual("already-kebab-case-", StringUtils.ToKebabCase("already-kebab-case- ")); + Assert.AreEqual("is-json-property", StringUtils.ToKebabCase("IsJSONProperty")); + Assert.AreEqual("shouting-case", StringUtils.ToKebabCase("SHOUTING-CASE")); + Assert.AreEqual("9999-12-31-t23:59:59.9999999-z", StringUtils.ToKebabCase("9999-12-31T23:59:59.9999999Z")); + Assert.AreEqual("hi!!-this-is-text.-time-to-test.", StringUtils.ToKebabCase("Hi!! This is text. Time to test.")); + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/KebabCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/KebabCaseNamingStrategy.cs new file mode 100644 index 000000000..16ac282b7 --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/KebabCaseNamingStrategy.cs @@ -0,0 +1,84 @@ +#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 + +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Serialization +{ + /// + /// A kebab case naming strategy. + /// + public class KebabCaseNamingStrategy : NamingStrategy + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A flag indicating whether dictionary keys should be processed. + /// + /// + /// A flag indicating whether explicitly specified property names should be processed, + /// e.g. a property name customized with a . + /// + public KebabCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames) + { + ProcessDictionaryKeys = processDictionaryKeys; + OverrideSpecifiedNames = overrideSpecifiedNames; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A flag indicating whether dictionary keys should be processed. + /// + /// + /// A flag indicating whether explicitly specified property names should be processed, + /// e.g. a property name customized with a . + /// + /// + /// A flag indicating whether extension data names should be processed. + /// + public KebabCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) + : this(processDictionaryKeys, overrideSpecifiedNames) + { + ProcessExtensionDataNames = processExtensionDataNames; + } + + /// + /// Initializes a new instance of the class. + /// + public KebabCaseNamingStrategy() + { + } + + /// + /// Resolves the specified property name. + /// + /// The property name to resolve. + /// The resolved property name. + protected override string ResolvePropertyName(string name) => StringUtils.ToKebabCase(name); + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Utilities/StringUtils.cs b/Src/Newtonsoft.Json/Utilities/StringUtils.cs index f07ffc4c1..a3765f06e 100644 --- a/Src/Newtonsoft.Json/Utilities/StringUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/StringUtils.cs @@ -203,7 +203,11 @@ private static char ToLower(char c) return c; } - internal enum SnakeCaseState + public static string ToSnakeCase(string s) => ToSeparatedCase(s, '_'); + + public static string ToKebabCase(string s) => ToSeparatedCase(s, '-'); + + private enum SeparatedCaseState { Start, Lower, @@ -211,7 +215,7 @@ internal enum SnakeCaseState NewWord } - public static string ToSnakeCase(string s) + private static string ToSeparatedCase(string s, char separator) { if (StringUtils.IsNullOrEmpty(s)) { @@ -219,35 +223,35 @@ public static string ToSnakeCase(string s) } StringBuilder sb = new StringBuilder(); - SnakeCaseState state = SnakeCaseState.Start; + SeparatedCaseState state = SeparatedCaseState.Start; for (int i = 0; i < s.Length; i++) { if (s[i] == ' ') { - if (state != SnakeCaseState.Start) + if (state != SeparatedCaseState.Start) { - state = SnakeCaseState.NewWord; + state = SeparatedCaseState.NewWord; } } else if (char.IsUpper(s[i])) { switch (state) { - case SnakeCaseState.Upper: + case SeparatedCaseState.Upper: bool hasNext = (i + 1 < s.Length); if (i > 0 && hasNext) { char nextChar = s[i + 1]; - if (!char.IsUpper(nextChar) && nextChar != '_') + if (!char.IsUpper(nextChar) && nextChar != separator) { - sb.Append('_'); + sb.Append(separator); } } break; - case SnakeCaseState.Lower: - case SnakeCaseState.NewWord: - sb.Append('_'); + case SeparatedCaseState.Lower: + case SeparatedCaseState.NewWord: + sb.Append(separator); break; } @@ -259,22 +263,22 @@ public static string ToSnakeCase(string s) #endif sb.Append(c); - state = SnakeCaseState.Upper; + state = SeparatedCaseState.Upper; } - else if (s[i] == '_') + else if (s[i] == separator) { - sb.Append('_'); - state = SnakeCaseState.Start; + sb.Append(separator); + state = SeparatedCaseState.Start; } else { - if (state == SnakeCaseState.NewWord) + if (state == SeparatedCaseState.NewWord) { - sb.Append('_'); + sb.Append(separator); } sb.Append(s[i]); - state = SnakeCaseState.Lower; + state = SeparatedCaseState.Lower; } }