From decc50afbc65a30bd0620dc6657771735bf6ddbf Mon Sep 17 00:00:00 2001 From: Unai Zorrilla Date: Fri, 27 Mar 2020 20:08:59 +0100 Subject: [PATCH] Fix for issue #207 (#237) * Created a object graph visitor to ignore also empty collections on serialization. Unify creation of serilizer/deserialization objects with specified configuration * Update OmitDefaultAndEmptyArrayObjectGraphVisitor.cs * Update YamlSerializer.cs * Fix tests with EOL issues on different SO #235 Co-authored-by: Justin Kotalik --- .../ConfigModel/ConfigFactory.cs | 7 +-- ...tDefaultAndEmptyArrayObjectGraphVisitor.cs | 46 +++++++++++++++++++ .../Serialization/YamlSerializer.cs | 28 +++++++++++ src/tye/InitHost.cs | 11 +---- .../Infrastructure/StringExtensions.cs | 12 +++++ test/E2ETest/TyeGenerateTests.cs | 10 ++-- test/E2ETest/TyeInitTests.cs | 7 +-- 7 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.Tye.Core/Serialization/OmitDefaultAndEmptyArrayObjectGraphVisitor.cs create mode 100644 src/Microsoft.Tye.Core/Serialization/YamlSerializer.cs create mode 100644 test/E2ETest/Infrastructure/StringExtensions.cs diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs index ecf0aaf6..1fd2f116 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs @@ -4,8 +4,7 @@ using System.Collections.Generic; using System.IO; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; +using Microsoft.Tye.Serialization; namespace Microsoft.Tye.ConfigModel { @@ -75,9 +74,7 @@ namespace Microsoft.Tye.ConfigModel private static ConfigApplication FromYaml(FileInfo file) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); + var deserializer = YamlSerializer.CreateDeserializer(); using var reader = file.OpenText(); var application = deserializer.Deserialize(reader); diff --git a/src/Microsoft.Tye.Core/Serialization/OmitDefaultAndEmptyArrayObjectGraphVisitor.cs b/src/Microsoft.Tye.Core/Serialization/OmitDefaultAndEmptyArrayObjectGraphVisitor.cs new file mode 100644 index 00000000..43383696 --- /dev/null +++ b/src/Microsoft.Tye.Core/Serialization/OmitDefaultAndEmptyArrayObjectGraphVisitor.cs @@ -0,0 +1,46 @@ +// 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; +using System.Collections; +using System.ComponentModel; +using System.Reflection; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.ObjectGraphVisitors; + +namespace Microsoft.Tye.Serialization +{ + internal sealed class OmitDefaultAndEmptyArrayObjectGraphVisitor + : ChainedObjectGraphVisitor + { + public OmitDefaultAndEmptyArrayObjectGraphVisitor(IObjectGraphVisitor nextVisitor) + : base(nextVisitor) { } + + private static object? GetDefault(Type type) + { + return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; + } + + private static bool IsEmptyArray(Type type, object? value) + { + return value is object + && value is ICollection + && ((ICollection)value).Count == 0; + } + + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + { + var defaultValueAttribute = key.GetCustomAttribute(); + + var defaultValue = defaultValueAttribute != null + ? defaultValueAttribute.Value + : GetDefault(key.Type); + + return !Equals(value.Value, defaultValue) + && !IsEmptyArray(value.Type, value.Value) + && base.EnterMapping(key, value, context); + } + } +} diff --git a/src/Microsoft.Tye.Core/Serialization/YamlSerializer.cs b/src/Microsoft.Tye.Core/Serialization/YamlSerializer.cs new file mode 100644 index 00000000..bc331ae2 --- /dev/null +++ b/src/Microsoft.Tye.Core/Serialization/YamlSerializer.cs @@ -0,0 +1,28 @@ +// 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 YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Microsoft.Tye.Serialization +{ + public static class YamlSerializer + { + public static ISerializer CreateSerializer() + { + return new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .WithEmissionPhaseObjectGraphVisitor(args => new OmitDefaultAndEmptyArrayObjectGraphVisitor(args.InnerVisitor)) + .Build(); + } + + public static IDeserializer CreateDeserializer() + { + return new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + } + } +} diff --git a/src/tye/InitHost.cs b/src/tye/InitHost.cs index 84c47694..6c7d57fe 100644 --- a/src/tye/InitHost.cs +++ b/src/tye/InitHost.cs @@ -1,14 +1,11 @@ using System.IO; using Microsoft.Tye.ConfigModel; -using Microsoft.Tye.Hosting.Dashboard; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; +using Microsoft.Tye.Serialization; namespace Microsoft.Tye { public class InitHost { - public static string CreateTyeFile(FileInfo? path, bool force) { var (content, outputFilePath) = CreateTyeFileContent(path, force); @@ -58,10 +55,7 @@ services: if (path is FileInfo && path.Exists) { var application = ConfigFactory.FromFile(path); - var serializer = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .Build(); + var serializer = YamlSerializer.CreateSerializer(); var extension = path.Extension.ToLowerInvariant(); var directory = path.Directory; @@ -106,7 +100,6 @@ services: return (template, outputFilePath); } - private static void ThrowIfTyeFilePresent(FileInfo? path, string yml) { var tyeYaml = Path.Combine(path!.DirectoryName, yml); diff --git a/test/E2ETest/Infrastructure/StringExtensions.cs b/test/E2ETest/Infrastructure/StringExtensions.cs new file mode 100644 index 00000000..bed0e4ce --- /dev/null +++ b/test/E2ETest/Infrastructure/StringExtensions.cs @@ -0,0 +1,12 @@ +namespace System +{ + internal static class StringExtensions + { + public static string NormalizeNewLines(this string value) + { + return value + .Replace("\r\n", "\n") + .Replace("\n", Environment.NewLine); + } + } +} diff --git a/test/E2ETest/TyeGenerateTests.cs b/test/E2ETest/TyeGenerateTests.cs index 3076c4f6..40a1f5ec 100644 --- a/test/E2ETest/TyeGenerateTests.cs +++ b/test/E2ETest/TyeGenerateTests.cs @@ -2,10 +2,10 @@ // 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; using System.IO; using System.Threading.Tasks; using Microsoft.Tye; -using Microsoft.Tye.ConfigModel; using Xunit; using Xunit.Abstractions; @@ -51,7 +51,7 @@ namespace E2ETest var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); await DockerAssert.AssertImageExistsAsync(output, "test/test-project"); } finally @@ -90,7 +90,7 @@ namespace E2ETest var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); await DockerAssert.AssertImageExistsAsync(output, "test/backend"); await DockerAssert.AssertImageExistsAsync(output, "test/frontend"); @@ -133,7 +133,7 @@ namespace E2ETest var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); var expectedContent = File.ReadAllText($"testassets/generate/{projectName}.yaml"); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); await DockerAssert.AssertImageExistsAsync(output, "test/backend"); await DockerAssert.AssertImageExistsAsync(output, "test/frontend"); @@ -173,7 +173,7 @@ namespace E2ETest var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); var expectedContent = File.ReadAllText($"testassets/generate/{projectName}-noregistry.yaml"); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); await DockerAssert.AssertImageExistsAsync(output, "test-project"); } diff --git a/test/E2ETest/TyeInitTests.cs b/test/E2ETest/TyeInitTests.cs index 2fea382b..203f07a9 100644 --- a/test/E2ETest/TyeInitTests.cs +++ b/test/E2ETest/TyeInitTests.cs @@ -2,6 +2,7 @@ // 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; using System.IO; using Microsoft.Tye; using Xunit; @@ -36,7 +37,7 @@ namespace E2ETest output.WriteLine(content); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); } [Fact] @@ -56,7 +57,7 @@ namespace E2ETest output.WriteLine(content); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); } [Fact] @@ -76,7 +77,7 @@ namespace E2ETest output.WriteLine(content); - Assert.Equal(expectedContent, content); + Assert.Equal(expectedContent.NormalizeNewLines(), content.NormalizeNewLines()); } } }