Skip to content

Commit

Permalink
Add custom YamlDotNet emitter that makes all fields with newlines use…
Browse files Browse the repository at this point in the history
… literal style. (#281)
  • Loading branch information
jedieaston authored Jul 18, 2022
1 parent 375420f commit 49a1fb4
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 64 deletions.
153 changes: 90 additions & 63 deletions src/WingetCreateCore/Common/Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
namespace Microsoft.WingetCreateCore
{
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using Microsoft.WingetCreateCore.Models;
Expand All @@ -20,9 +20,10 @@ namespace Microsoft.WingetCreateCore
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization.TypeInspectors;

using YamlDotNet.Serialization.EventEmitters;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization.TypeInspectors;

/// <summary>
/// Provides functionality for the serialization of JSON objects to yaml.
/// </summary>
Expand All @@ -39,13 +40,14 @@ public static class Serialization
/// <returns>ISerializer object.</returns>
public static ISerializer CreateSerializer()
{
var serializer = new SerializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
var serializer = new SerializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.WithTypeConverter(new YamlStringEnumConverter())
.WithEmissionPhaseObjectGraphVisitor(args => new YamlSkipPropertyVisitor(args.InnerVisitor))
.WithEventEmitter(nextEmitter => new MultilineScalarFlowStyleEmitter(nextEmitter))
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull);
return serializer.Build();
}
}

/// <summary>
/// Helper to build a YAML deserializer.
Expand All @@ -55,9 +57,9 @@ public static IDeserializer CreateDeserializer()
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(PascalCaseNamingConvention.Instance)
.WithTypeConverter(new YamlStringEnumConverter())
.WithTypeInspector(inspector => new AliasTypeInspector(inspector))
.IgnoreUnmatchedProperties();
.WithTypeConverter(new YamlStringEnumConverter())
.WithTypeInspector(inspector => new AliasTypeInspector(inspector))
.IgnoreUnmatchedProperties();
return deserializer.Build();
}

Expand Down Expand Up @@ -199,68 +201,68 @@ private static string RemoveBom(string value)
{
string bomMarkUtf8 = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
return value.StartsWith(bomMarkUtf8, StringComparison.OrdinalIgnoreCase) ? value.Remove(0, bomMarkUtf8.Length) : value;
}

/// <summary>
/// Custom TypeInspector to priorize properties that have a defined YamlMemberAttribute for custom override.
/// </summary>
private class AliasTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector innerTypeDescriptor;

public AliasTypeInspector(ITypeInspector innerTypeDescriptor)
{
this.innerTypeDescriptor = innerTypeDescriptor;
}

/// <summary>
/// Because certain properties were generated incorrectly, we needed to create custom fields for those properties.
/// Therefore to resolve naming conflicts during deserialization, we prioritize fields that have the YamlMemberAttribute defined
/// as that attribute indicates an override.
/// </summary>
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
{
var propertyDescriptors = this.innerTypeDescriptor.GetProperties(type, container);
var aliasDefinedProps = type.GetProperties().ToList()
.Where(p =>
{
var yamlMemberAttribute = p.GetCustomAttribute<YamlMemberAttribute>();
return yamlMemberAttribute != null && !string.IsNullOrEmpty(yamlMemberAttribute.Alias);
})
.ToList();

if (aliasDefinedProps.Any())
{
var overriddenProps = propertyDescriptors
.Where(prop => aliasDefinedProps.Any(aliasProp =>
prop.Name == aliasProp.GetCustomAttribute<YamlMemberAttribute>().Alias && // Use Alias name (ex. ReleaseDate) instead of property name (ex. ReleaseDateString).
prop.Type != aliasProp.PropertyType))
.ToList();

// Remove overridden properties from the returned list of deserializable properties.
return propertyDescriptors
.Where(prop => !overriddenProps.Any(overridenProp =>
prop.Name == overridenProp.Name &&
prop.Type == overridenProp.Type))
.ToList();
}
else
{
return propertyDescriptors;
}
}
}

/// <summary>
/// Custom TypeInspector to priorize properties that have a defined YamlMemberAttribute for custom override.
/// </summary>
private class AliasTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector innerTypeDescriptor;

public AliasTypeInspector(ITypeInspector innerTypeDescriptor)
{
this.innerTypeDescriptor = innerTypeDescriptor;
}

/// <summary>
/// Because certain properties were generated incorrectly, we needed to create custom fields for those properties.
/// Therefore to resolve naming conflicts during deserialization, we prioritize fields that have the YamlMemberAttribute defined
/// as that attribute indicates an override.
/// </summary>
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
{
var propertyDescriptors = this.innerTypeDescriptor.GetProperties(type, container);
var aliasDefinedProps = type.GetProperties().ToList()
.Where(p =>
{
var yamlMemberAttribute = p.GetCustomAttribute<YamlMemberAttribute>();
return yamlMemberAttribute != null && !string.IsNullOrEmpty(yamlMemberAttribute.Alias);
})
.ToList();

if (aliasDefinedProps.Any())
{
var overriddenProps = propertyDescriptors
.Where(prop => aliasDefinedProps.Any(aliasProp =>
prop.Name == aliasProp.GetCustomAttribute<YamlMemberAttribute>().Alias && // Use Alias name (ex. ReleaseDate) instead of property name (ex. ReleaseDateString).
prop.Type != aliasProp.PropertyType))
.ToList();

// Remove overridden properties from the returned list of deserializable properties.
return propertyDescriptors
.Where(prop => !overriddenProps.Any(overridenProp =>
prop.Name == overridenProp.Name &&
prop.Type == overridenProp.Type))
.ToList();
}
else
{
return propertyDescriptors;
}
}
}

private class YamlStringEnumConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
{
Type u = Nullable.GetUnderlyingType(type);
return type.IsEnum || ((u != null) && u.IsEnum);
}

public object ReadYaml(IParser parser, Type type)
{
{
Type u = Nullable.GetUnderlyingType(type);
if (u != null)
{
Expand Down Expand Up @@ -304,5 +306,30 @@ public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor val
return base.EnterMapping(key, value, context);
}
}

/// <summary>
/// A custom emitter for YamlDotNet which ensures all multiline fields use a <see cref="ScalarStyle.Literal"/>.
/// </summary>
private class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
{
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter) : base(nextEmitter) { }

public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
{
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
{
var outString = eventInfo.Source.Value as string;
if (!string.IsNullOrEmpty(outString))
{
bool isMultiLine = new[] { '\r', '\n', '\x85', '\x2028', '\x2029' }.Any(outString.Contains);
if (isMultiLine)
{
eventInfo = new ScalarEventInfo(eventInfo.Source) {Style = ScalarStyle.Literal};
}
}
}
this.nextEmitter.Emit(eventInfo, emitter);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

namespace Microsoft.WingetCreateUnitTests
{
using System;
using System.IO;
using System.Linq;
using Microsoft.WingetCreateCore;
using Microsoft.WingetCreateCore.Models.Singleton;
using NUnit.Framework;

/// <summary>
/// Unit tests for verifying unicode text and directionality.
/// </summary>
Expand Down Expand Up @@ -48,5 +50,37 @@ public void VerifyTextSupport()
File.Delete(testManifestFilePath);
}
}

/// <summary>
/// Verifies that we aren't adding more newlines than expected.
/// </summary>
[Test]
public void VerfiyNewLineSupport()
{
string[] stringsWithNewLines =
{
"This\n has\n some newlines.",
"So\r\n does this.",
"And this does\x85.",
"As does this\x2028.",
"Me too!\x2029:)",
};

string testManifestFilePath = Path.Combine(Path.GetTempPath(), "TestManifest.yaml");

foreach (var i in stringsWithNewLines)
{
SingletonManifest written = new SingletonManifest { Description = i };
File.WriteAllText(testManifestFilePath, written.ToYaml());
SingletonManifest read = Serialization.DeserializeFromPath<SingletonManifest>(testManifestFilePath);

// we know when written that \r\n and \x85 characters are replaced with \n.
var writtenFixed = string.Join('\n', written.Description.Split(new string[] { "\n", "\r\n", "\x85" }, StringSplitOptions.None));

Assert.AreEqual(writtenFixed, read.Description, $"String {read.Description} had the wrong number of newlines :(.");
File.Delete(testManifestFilePath);
}
}

}
}

0 comments on commit 49a1fb4

Please sign in to comment.