diff --git a/JsonBinder.sln b/JsonBinder.sln index aea0e5d..572d46a 100644 --- a/JsonBinder.sln +++ b/JsonBinder.sln @@ -7,6 +7,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder.Console", "src\Riverside.JsonBinder.Console\Riverside.JsonBinder.Console.csproj", "{A0C21F10-32C7-4D6D-A8F1-022C808A4615}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FDCA6BBE-B3FF-4CF8-AC2F-AEA0F5A2171E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{A9D4D6A1-C565-435B-8CE2-36FDF6D163F8}" + ProjectSection(SolutionItems) = preProject + eng\AdditionalFiles.props = eng\AdditionalFiles.props + eng\CurrentVersion.props = eng\CurrentVersion.props + eng\PackageLogo.png = eng\PackageLogo.png + eng\PackageMetadata.props = eng\PackageMetadata.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riverside.JsonBinder.Tests", "tests\Riverside.JsonBinder.Tests\Riverside.JsonBinder.Tests.csproj", "{4197DF7D-233B-45E6-911F-485B188A6B79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,8 +35,20 @@ Global {A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0C21F10-32C7-4D6D-A8F1-022C808A4615}.Release|Any CPU.Build.0 = Release|Any CPU + {4197DF7D-233B-45E6-911F-485B188A6B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4197DF7D-233B-45E6-911F-485B188A6B79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4197DF7D-233B-45E6-911F-485B188A6B79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4197DF7D-233B-45E6-911F-485B188A6B79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0B9E827E-F59A-419F-9BAB-DBB5512627D9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {A0C21F10-32C7-4D6D-A8F1-022C808A4615} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4197DF7D-233B-45E6-911F-485B188A6B79} = {FDCA6BBE-B3FF-4CF8-AC2F-AEA0F5A2171E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A7D91CF4-FBEE-4762-A037-CFA995F795FC} + EndGlobalSection EndGlobal diff --git a/src/Riverside.JsonBinder/Properties/AssemblyInfo.cs b/src/Riverside.JsonBinder/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..09daa24 --- /dev/null +++ b/src/Riverside.JsonBinder/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Riverside.JsonBinder.Tests")] \ No newline at end of file diff --git a/src/Riverside.JsonBinder/Serialization/PHPSerializer.cs b/src/Riverside.JsonBinder/Serialization/PHPSerializer.cs index 024489d..09d5e86 100644 --- a/src/Riverside.JsonBinder/Serialization/PHPSerializer.cs +++ b/src/Riverside.JsonBinder/Serialization/PHPSerializer.cs @@ -30,7 +30,7 @@ private void ProcessNode(JsonNode node, string className, List classes) { if (node is JsonObject obj) { - var classDef = $"class {className} {{\n"; + var classDef = $"class {className} {{"; foreach (var property in obj) { classDef += $"\n public ${property.Key};"; diff --git a/src/Riverside.JsonBinder/Serialization/RubySerializer.cs b/src/Riverside.JsonBinder/Serialization/RubySerializer.cs index 4b8bcb9..9d2280d 100644 --- a/src/Riverside.JsonBinder/Serialization/RubySerializer.cs +++ b/src/Riverside.JsonBinder/Serialization/RubySerializer.cs @@ -32,6 +32,7 @@ private void ProcessNode(JsonNode node, string className, List classes) { var classDef = $"class {className}\n attr_accessor "; classDef += string.Join(", ", obj.Select(property => $":{property.Key}")); + classDef += "\nend"; classes.Add(classDef); foreach (var property in obj) diff --git a/tests/Riverside.JsonBinder.Tests/JsonSerializerTests.cs b/tests/Riverside.JsonBinder.Tests/JsonSerializerTests.cs new file mode 100644 index 0000000..0c80337 --- /dev/null +++ b/tests/Riverside.JsonBinder.Tests/JsonSerializerTests.cs @@ -0,0 +1,90 @@ +namespace Riverside.JsonBinder.Tests; + +[TestClass] +public class JsonSerializerTests +{ + [TestMethod] + [DataRow("{\"name\":\"John\"}", SerializableLanguage.CSharp, "public class Root\n{\n public string name { get; set; }\n}")] + [DataRow("{\"name\":\"John\"}", SerializableLanguage.Python, "class Root:\n def __init__(self):\n self.name: str = None")] + [DataRow("{\"name\":\"John\"}", SerializableLanguage.Java, "public class Root {\n private String name;\n public String getname() { return name; }\n public void setname(String name) { this.name = name; }\n}")] + public void ConvertTo_ValidJson_ReturnsExpectedResult(string json, SerializableLanguage language, string expected) + { + // Act + var result = JsonSerializer.ConvertTo(json, language); + + // Assert + Assert.AreEqual(Normalize(expected), Normalize(result)); + } + + [TestMethod] + public void ConvertTo_InvalidJson_ThrowsException() + { + // Arrange + string invalidJson = "invalid json"; + + // Act + try + { + // Stupidly, JsonReaderException is internal, so it can't be caught. + // Instead, we need to compare the exception message with the expected message. + JsonSerializer.ConvertTo(invalidJson, SerializableLanguage.CSharp); + } + catch (Exception ex) + { + // Assert + Assert.AreEqual("'i' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.", ex.Message); + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void ConvertTo_NullJson_ThrowsException() + { + // Arrange + string? invalidJson = null; + + JsonSerializer.ConvertTo(invalidJson, SerializableLanguage.CSharp); + } + + [TestMethod] + [ExpectedException(typeof(NotSupportedException))] + public void ConvertTo_UnsupportedLanguage_ThrowsNotSupportedException() + { + // Arrange + string json = "{\"name\":\"John\"}"; + + // Act + JsonSerializer.ConvertTo(json, (SerializableLanguage)0xFFFFFF); + } + + [TestMethod] + public void ConvertTo_JsonArray_WrapsInRootObject() + { + // Arrange + string jsonArray = "[{\"name\":\"John\"}]"; + string expected = """ + public class Root + { + public List Items { get; set; } + } + + public class ItemsItem + { + public string name { get; set; } + } + + public class Items + { + public List Items { get; set; } = new List(); + } + """; + // Act + var result = JsonSerializer.ConvertTo(jsonArray, SerializableLanguage.CSharp); + + // Assert + Assert.AreEqual(Normalize(expected), Normalize(result)); + } + + private static string Normalize(string input) + => input.Replace("\r\n", "\n").Trim(); +} diff --git a/tests/Riverside.JsonBinder.Tests/LanguageSerializerTests.cs b/tests/Riverside.JsonBinder.Tests/LanguageSerializerTests.cs new file mode 100644 index 0000000..cba97ff --- /dev/null +++ b/tests/Riverside.JsonBinder.Tests/LanguageSerializerTests.cs @@ -0,0 +1,97 @@ +using System.Text.Json.Nodes; +using Riverside.JsonBinder.Serialization; + +namespace Riverside.JsonBinder.Tests; + +[TestClass] +public class LanguageSerializerTests +{ + private static readonly Dictionary SerializerTypes = new() + { + { SerializableLanguage.CSharp, typeof(CSharpSerializer) }, + { SerializableLanguage.Python, typeof(PythonSerializer) }, + { SerializableLanguage.Java, typeof(JavaSerializer) }, + { SerializableLanguage.JavaScript, typeof(JavaScriptSerializer) }, + { SerializableLanguage.TypeScript, typeof(TypeScriptSerializer) }, + { SerializableLanguage.PHP, typeof(PHPSerializer) }, + { SerializableLanguage.Ruby, typeof(RubySerializer) }, + { SerializableLanguage.Swift, typeof(SwiftSerializer) } + }; + + [TestMethod] + [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] + public void GenerateClasses_ValidJson_ReturnsExpectedResult(SerializableLanguage language, string json, string expected) + { + // Arrange + if (!SerializerTypes.TryGetValue(language, out var serializerType) || serializerType == null) + { + Assert.Fail($"Serializer for language {language} not found."); + return; + } + + var serializer = (LanguageSerializer?)Activator.CreateInstance(serializerType); + Assert.IsNotNull(serializer, $"Failed to create instance of {serializerType}"); + + // Act + var jsonNode = JsonNode.Parse(json); + Assert.IsNotNull(jsonNode, "Failed to parse JSON"); + + var result = serializer.GenerateClasses(jsonNode, "Root"); + + // Assert + Assert.AreEqual(expected.Normalize(), result.Normalize()); + } + + [TestMethod] + [DynamicData(nameof(GetInvalidJsonTestData), DynamicDataSourceType.Method)] + public void GenerateClasses_InvalidJson_ThrowsException(SerializableLanguage language, string invalidJson) + { + // Arrange + if (!SerializerTypes.TryGetValue(language, out var serializerType) || serializerType == null) + { + Assert.Fail($"Serializer for language {language} not found."); + return; + } + + var serializer = (LanguageSerializer?)Activator.CreateInstance(serializerType); + Assert.IsNotNull(serializer, $"Failed to create instance of {serializerType}"); + + // Act + try + { + var jsonNode = JsonNode.Parse(invalidJson); + Assert.IsNotNull(jsonNode, "Failed to parse JSON"); + + serializer.GenerateClasses(jsonNode, "Root"); + } + catch (Exception ex) + { + // Assert + Assert.AreEqual("'i' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.", ex.Message); + } + } + + public static IEnumerable GetTestData() + { + yield return new object[] { SerializableLanguage.CSharp, "{\"name\":\"John\"}", "public class Root\n{\n public string name { get; set; }\n}" }; + yield return new object[] { SerializableLanguage.Python, "{\"name\":\"John\"}", "class Root:\n def __init__(self):\n self.name: str = None" }; + yield return new object[] { SerializableLanguage.Java, "{\"name\":\"John\"}", "public class Root {\n private String name;\n public String getname() { return name; }\n public void setname(String name) { this.name = name; }\n}" }; + yield return new object[] { SerializableLanguage.JavaScript, "{\"name\":\"John\"}", "class Root {\n constructor() {\n this.name = null;\n }\n}" }; + yield return new object[] { SerializableLanguage.TypeScript, "{\"name\":\"John\"}", "class Root {\n constructor() {\n this.name = null;\n }\n}" }; + yield return new object[] { SerializableLanguage.PHP, "{\"name\":\"John\"}", "class Root {\n public $name;\n}" }; + yield return new object[] { SerializableLanguage.Ruby, "{\"name\":\"John\"}", "class Root\n attr_accessor :name\nend" }; + yield return new object[] { SerializableLanguage.Swift, "{\"name\":\"John\"}", "struct Root {\n var name: String?\n}" }; + } + + public static IEnumerable GetInvalidJsonTestData() + { + yield return new object[] { SerializableLanguage.CSharp, "invalid json" }; + yield return new object[] { SerializableLanguage.Python, "invalid json" }; + yield return new object[] { SerializableLanguage.Java, "invalid json" }; + yield return new object[] { SerializableLanguage.JavaScript, "invalid json" }; + yield return new object[] { SerializableLanguage.TypeScript, "invalid json" }; + yield return new object[] { SerializableLanguage.PHP, "invalid json" }; + yield return new object[] { SerializableLanguage.Ruby, "invalid json" }; + yield return new object[] { SerializableLanguage.Swift, "invalid json" }; + } +} diff --git a/tests/Riverside.JsonBinder.Tests/MSTestSettings.cs b/tests/Riverside.JsonBinder.Tests/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/tests/Riverside.JsonBinder.Tests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/tests/Riverside.JsonBinder.Tests/Riverside.JsonBinder.Tests.csproj b/tests/Riverside.JsonBinder.Tests/Riverside.JsonBinder.Tests.csproj new file mode 100644 index 0000000..e765e60 --- /dev/null +++ b/tests/Riverside.JsonBinder.Tests/Riverside.JsonBinder.Tests.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + latest + enable + enable + + true + + + + 1701;1702;CA1707;CS1591 + False + + + + + + +