Skip to content

Commit c67b80f

Browse files
committed
feat(yaml): Upgrade to SharpYaml for improved YAML processing
f8
1 parent d4f49c6 commit c67b80f

7 files changed

Lines changed: 198 additions & 62 deletions

File tree

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<PackageVersion Include="NUnit" Version="4.5.1" />
4141
<PackageVersion Include="NUnit.Analyzers" Version="4.12.0" />
4242
<PackageVersion Include="NUnit3TestAdapter" Version="6.1.0" />
43+
<PackageVersion Include="SharpYaml" Version="3.4.0" />
4344
<PackageVersion Include="Shouldly" Version="4.3.0" />
4445
<PackageVersion Include="System.Collections.Immutable" Version="10.0.5" />
4546
<PackageVersion Include="System.Drawing.Common" Version="10.0.5" />
@@ -48,7 +49,6 @@
4849
<PackageVersion Include="System.Reflection.Metadata" Version="10.0.5" />
4950
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.5" />
5051
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
51-
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
5252
</ItemGroup>
5353
<ItemGroup Condition=" '$(MicrosoftBuildVersion)' != '' ">
5454
<PackageVersion Include="Microsoft.Build" Version="$(MicrosoftBuildVersion)" />

src/GitVersion.Configuration.Tests/Configuration/IgnoreConfigurationTests.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using System.Globalization;
12
using GitVersion.Core.Tests.Helpers;
2-
using YamlDotNet.Core;
3+
using GitVersion.VersionCalculation;
4+
using SharpYaml;
35

46
namespace GitVersion.Configuration.Tests;
57

@@ -71,6 +73,17 @@ public void WhenBadDateFormatShouldFail()
7173
Should.Throw<YamlException>(() => serializer.ReadConfiguration(yaml));
7274
}
7375

76+
[Test]
77+
public void ShouldSupportScalarVersionStrategiesOverrideFormat()
78+
{
79+
const string yaml = "strategies: ConfiguredNextVersion, TaggedCommit";
80+
81+
var configuration = serializer.ReadConfiguration(yaml);
82+
83+
configuration.ShouldNotBeNull();
84+
configuration.VersionStrategy.ShouldBe(VersionStrategies.ConfiguredNextVersion | VersionStrategies.TaggedCommit);
85+
}
86+
7487
[Test]
7588
public void NewInstanceShouldBeEmpty()
7689
{

src/GitVersion.Configuration/ConfigurationProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using GitVersion.Configuration.Workflows;
33
using GitVersion.Extensions;
44
using GitVersion.Logging;
5-
using YamlDotNet.Core;
5+
using SharpYaml;
66

77
namespace GitVersion.Configuration;
88

Lines changed: 95 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,111 @@
1-
using YamlDotNet.Serialization;
2-
using YamlDotNet.Serialization.NamingConventions;
3-
using YamlDotNet.Serialization.TypeInspectors;
1+
using SharpYaml;
42

53
namespace GitVersion.Configuration;
64

75
internal class ConfigurationSerializer : IConfigurationSerializer
86
{
9-
private static IDeserializer Deserializer => new DeserializerBuilder()
10-
.WithNamingConvention(HyphenatedNamingConvention.Instance)
11-
.WithTypeConverter(VersionStrategiesConverter.Instance)
12-
.WithTypeInspector(inspector => new JsonPropertyNameInspector(inspector))
13-
.Build();
14-
15-
private static ISerializer Serializer => new SerializerBuilder()
16-
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
17-
.WithTypeInspector(inspector => new JsonPropertyNameInspector(inspector))
18-
.WithNamingConvention(HyphenatedNamingConvention.Instance).Build();
19-
20-
public T Deserialize<T>(string input) => Deserializer.Deserialize<T>(input);
21-
public string Serialize(object graph) => Serializer.Serialize(graph);
7+
private static readonly ConfigurationYamlContext GeneratedContext = ConfigurationYamlContext.Default;
8+
9+
private static readonly YamlSerializerOptions SerializerOptions = new()
10+
{
11+
PropertyNamingPolicy = HyphenatedJsonNamingPolicy.Instance,
12+
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull,
13+
Converters = [VersionStrategiesConverter.Instance]
14+
};
15+
16+
public T Deserialize<T>(string input)
17+
{
18+
if (typeof(T) == typeof(Dictionary<object, object?>))
19+
{
20+
var graph = YamlSerializer.Deserialize<Dictionary<string, object?>>(input, SerializerOptions);
21+
return (T)(object)ConvertToObjectDictionary(graph);
22+
}
23+
24+
if (typeof(T) == typeof(GitVersionConfiguration))
25+
{
26+
try
27+
{
28+
return (T)(object)YamlSerializer.Deserialize<GitVersionConfiguration>(input, GeneratedContext)!;
29+
}
30+
catch (Exception exception) when (exception is not YamlException)
31+
{
32+
throw new YamlException(exception.Message, exception);
33+
}
34+
}
35+
36+
return YamlSerializer.Deserialize<T>(input, SerializerOptions)!;
37+
}
38+
39+
public string Serialize(object graph)
40+
=> YamlSerializer.Serialize(graph, SerializerOptions);
41+
2242
public IGitVersionConfiguration? ReadConfiguration(string input) => Deserialize<GitVersionConfiguration?>(input);
2343

24-
private sealed class JsonPropertyNameInspector(ITypeInspector innerTypeDescriptor) : TypeInspectorSkeleton
44+
private static Dictionary<object, object?> ConvertToObjectDictionary(IReadOnlyDictionary<string, object?>? source)
2545
{
26-
public override string GetEnumName(Type enumType, string name) => innerTypeDescriptor.GetEnumName(enumType, name);
46+
if (source is null) return [];
2747

28-
public override string GetEnumValue(object enumValue) => innerTypeDescriptor.GetEnumValue(enumValue);
48+
Dictionary<object, object?> result = [];
49+
foreach (var item in source)
50+
{
51+
result[item.Key] = ConvertValue(item.Value);
52+
}
2953

30-
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object? container) =>
31-
innerTypeDescriptor.GetProperties(type, container)
32-
.Where(p => p.GetCustomAttribute<JsonIgnoreAttribute>() == null)
33-
.Select(IPropertyDescriptor (p) =>
54+
return result;
55+
}
56+
57+
private static object? ConvertValue(object? value) => value switch
58+
{
59+
IReadOnlyDictionary<string, object?> dictionary => ConvertToObjectDictionary(dictionary),
60+
IDictionary<string, object?> dictionary => ConvertToObjectDictionary(new Dictionary<string, object?>(dictionary)),
61+
IList list => ConvertList(list),
62+
_ => value
63+
};
64+
65+
private static List<object?> ConvertList(IList list)
66+
{
67+
List<object?> result = [];
68+
foreach (var item in list)
69+
{
70+
result.Add(ConvertValue(item));
71+
}
72+
73+
return result;
74+
}
75+
76+
private sealed class HyphenatedJsonNamingPolicy : JsonNamingPolicy
77+
{
78+
public static JsonNamingPolicy Instance { get; } = new HyphenatedJsonNamingPolicy();
79+
80+
public override string ConvertName(string name)
81+
{
82+
if (string.IsNullOrEmpty(name)) return name;
83+
84+
var builder = new StringBuilder(name.Length + 8);
85+
for (var index = 0; index < name.Length; index++)
86+
{
87+
var current = name[index];
88+
if (char.IsUpper(current))
3489
{
35-
var descriptor = new PropertyDescriptor(p);
36-
var member = p.GetCustomAttribute<JsonPropertyNameAttribute>();
37-
if (member is not null)
90+
var hasPrevious = index > 0;
91+
var hasNext = index + 1 < name.Length;
92+
var previousIsLowerOrDigit = hasPrevious && (char.IsLower(name[index - 1]) || char.IsDigit(name[index - 1]));
93+
var nextIsLower = hasNext && char.IsLower(name[index + 1]);
94+
95+
if (hasPrevious && (previousIsLowerOrDigit || nextIsLower))
3896
{
39-
descriptor.Name = member.Name;
97+
builder.Append('-');
4098
}
4199

42-
return descriptor;
43-
})
44-
.OrderBy(p => p.Order);
100+
builder.Append(char.ToLowerInvariant(current));
101+
}
102+
else
103+
{
104+
builder.Append(current);
105+
}
106+
}
107+
108+
return builder.ToString();
109+
}
45110
}
46111
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using SharpYaml;
2+
using SharpYaml.Serialization;
3+
4+
namespace GitVersion.Configuration;
5+
6+
[YamlSourceGenerationOptions(
7+
DefaultIgnoreCondition = YamlIgnoreCondition.WhenWritingNull,
8+
Converters = [typeof(VersionStrategiesConverter)])]
9+
[YamlSerializable(typeof(GitVersionConfiguration))]
10+
[YamlSerializable(typeof(BranchConfiguration))]
11+
[YamlSerializable(typeof(PreventIncrementConfiguration))]
12+
[YamlSerializable(typeof(IgnoreConfiguration))]
13+
internal partial class ConfigurationYamlContext : YamlSerializerContext;

src/GitVersion.Configuration/GitVersion.Configuration.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</ItemGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="YamlDotNet" />
12+
<PackageReference Include="SharpYaml" />
1313
</ItemGroup>
1414

1515
<ItemGroup>
Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,99 @@
11
using GitVersion.VersionCalculation;
2-
using YamlDotNet.Core;
3-
using YamlDotNet.Core.Events;
4-
using YamlDotNet.Serialization;
5-
using YamlDotNet.Serialization.NamingConventions;
2+
using SharpYaml;
3+
using SharpYaml.Serialization;
64

75
namespace GitVersion.Configuration;
86

9-
internal class VersionStrategiesConverter : IYamlTypeConverter
7+
internal sealed class VersionStrategiesConverter : YamlConverter<VersionStrategies[]>
108
{
11-
public static readonly IYamlTypeConverter Instance = new VersionStrategiesConverter();
9+
public static YamlConverter Instance { get; } = new VersionStrategiesConverter();
1210

13-
public bool Accepts(Type type) => type == typeof(VersionStrategies[]);
14-
15-
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
11+
public override VersionStrategies[] Read(YamlReader reader)
1612
{
1713
List<VersionStrategies> strategies = [];
1814

19-
if (parser.TryConsume<SequenceStart>(out _))
15+
if (reader.TokenType == YamlTokenType.StartSequence)
2016
{
21-
while (!parser.TryConsume<SequenceEnd>(out _))
17+
reader.Read();
18+
while (reader.TokenType != YamlTokenType.EndSequence)
2219
{
23-
var data = parser.Consume<Scalar>().Value;
20+
if (reader.TokenType != YamlTokenType.Scalar)
21+
{
22+
throw new YamlException(reader.SourceName, reader.Start, reader.End, "Expected a scalar value while reading version strategies.");
23+
}
2424

25-
var strategy = Enum.Parse<VersionStrategies>(data);
26-
strategies.Add(strategy);
25+
strategies.Add(ParseStrategy(reader.GetScalarValue()));
26+
reader.Read();
2727
}
28+
29+
reader.Read();
30+
return [.. strategies];
2831
}
29-
else
32+
33+
if (reader.TokenType != YamlTokenType.Scalar)
3034
{
31-
var data = parser.Consume<Scalar>().Value;
35+
throw new YamlException(reader.SourceName, reader.Start, reader.End, "Expected a scalar or sequence while reading version strategies.");
36+
}
3237

33-
var deserializer = new DeserializerBuilder()
34-
.WithNamingConvention(UnderscoredNamingConvention.Instance)
35-
.Build();
38+
var scalar = reader.GetScalarValue();
39+
reader.Read();
3640

37-
strategies = deserializer.Deserialize<List<VersionStrategies>>(data);
41+
foreach (var item in SplitScalarList(scalar))
42+
{
43+
strategies.Add(ParseStrategy(item));
3844
}
3945

40-
return strategies.ToArray();
46+
return [.. strategies];
4147
}
4248

43-
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
49+
public override void Write(YamlWriter writer, VersionStrategies[]? value)
4450
{
45-
var strategies = (VersionStrategies[])value!;
51+
if (value is null)
52+
{
53+
writer.WriteNullValue();
54+
return;
55+
}
4656

47-
var s = new SerializerBuilder()
48-
.JsonCompatible()
49-
.Build();
50-
var data = s.Serialize(strategies);
57+
writer.WriteStartSequence();
58+
foreach (var strategy in value)
59+
{
60+
writer.WriteString(strategy.ToString());
61+
}
62+
writer.WriteEndSequence();
63+
}
5164

52-
emitter.Emit(new Scalar(data));
65+
private static IEnumerable<string> SplitScalarList(string scalar)
66+
{
67+
var trimmed = scalar.Trim();
68+
if (trimmed.Length == 0) yield break;
69+
70+
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
71+
{
72+
trimmed = trimmed[1..^1];
73+
}
74+
75+
foreach (var item in trimmed.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
76+
{
77+
yield return item;
78+
}
5379
}
80+
81+
private static VersionStrategies ParseStrategy(string value)
82+
{
83+
if (Enum.TryParse<VersionStrategies>(value, ignoreCase: false, out var exactMatch))
84+
{
85+
return exactMatch;
86+
}
87+
88+
var normalizedValue = Normalize(value);
89+
foreach (var enumValue in Enum.GetValues<VersionStrategies>().Where(enumValue => Normalize(enumValue.ToString()) == normalizedValue))
90+
{
91+
return enumValue;
92+
}
93+
94+
throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown version strategy value.");
95+
}
96+
97+
private static string Normalize(string value)
98+
=> new(value.Where(char.IsLetterOrDigit).Select(char.ToUpperInvariant).ToArray());
5499
}

0 commit comments

Comments
 (0)