diff --git a/src/GitVersionCore.Tests/JsonVersionBuilderTests.cs b/src/GitVersionCore.Tests/JsonVersionBuilderTests.cs index 9ae1113834..3fbf73ad18 100644 --- a/src/GitVersionCore.Tests/JsonVersionBuilderTests.cs +++ b/src/GitVersionCore.Tests/JsonVersionBuilderTests.cs @@ -1,7 +1,6 @@ using System; using NUnit.Framework; using Shouldly; -using GitVersion.OutputFormatters; using GitVersion.OutputVariables; using GitVersion; using GitVersionCore.Tests.Helpers; @@ -38,7 +37,7 @@ public void Json() var variableProvider = sp.GetService(); var variables = variableProvider.GetVariablesFor(semanticVersion, config, false); - var json = JsonOutputFormatter.ToJson(variables); + var json = variables.ToString(); json.ShouldMatchApproved(c => c.SubFolder("Approved")); } } diff --git a/src/GitVersionCore.Tests/VariableProviderTests.cs b/src/GitVersionCore.Tests/VariableProviderTests.cs index 7556b8837c..e61b2e1ad4 100644 --- a/src/GitVersionCore.Tests/VariableProviderTests.cs +++ b/src/GitVersionCore.Tests/VariableProviderTests.cs @@ -1,6 +1,5 @@ using GitVersion; using GitVersion.Logging; -using GitVersion.OutputFormatters; using GitVersion.OutputVariables; using GitVersion.VersioningModes; using GitVersionCore.Tests.Helpers; @@ -74,7 +73,7 @@ public void ProvidesVariablesInContinuousDeliveryModeForPreRelease() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -101,7 +100,7 @@ public void ProvidesVariablesInContinuousDeliveryModeForPreReleaseWithPadding() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -127,7 +126,7 @@ public void ProvidesVariablesInContinuousDeploymentModeForPreRelease() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -152,7 +151,7 @@ public void ProvidesVariablesInContinuousDeliveryModeForStable() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -177,7 +176,7 @@ public void ProvidesVariablesInContinuousDeploymentModeForStable() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -205,7 +204,7 @@ public void ProvidesVariablesInContinuousDeploymentModeForStableWhenCurrentCommi var vars = variableProvider.GetVariablesFor(semVer, config, true); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -277,7 +276,7 @@ public void ProvidesVariablesInContinuousDeliveryModeForFeatureBranch() var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } [Test] @@ -304,7 +303,7 @@ public void ProvidesVariablesInContinuousDeliveryModeForFeatureBranchWithCustomA var vars = variableProvider.GetVariablesFor(semVer, config, false); - JsonOutputFormatter.ToJson(vars).ShouldMatchApproved(c => c.SubFolder("Approved")); + vars.ToString().ShouldMatchApproved(c => c.SubFolder("Approved")); } } } diff --git a/src/GitVersionCore/Helpers/JsonSerializer.cs b/src/GitVersionCore/Helpers/JsonSerializer.cs new file mode 100644 index 0000000000..42760d0913 --- /dev/null +++ b/src/GitVersionCore/Helpers/JsonSerializer.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace GitVersion.Helpers +{ + // Credit to https://github.com/neuecc + // Inspired by https://gist.github.com/neuecc/7d728cd99d2a1e613362 + public static class JsonSerializer + { + private const string INDENT_STRING = " "; + private static readonly Encoding UTF8 = new UTF8Encoding(false); + + public static string Serialize(object obj) + { + using var ms = new MemoryStream(); + using var sw = new StreamWriter(ms, UTF8); + Serialize(sw, obj); + sw.Flush(); + return UTF8.GetString(ms.ToArray()); + } + + public static void Serialize(TextWriter tw, object obj) + { + SerializeObject(tw, obj); + } + + enum JsonType + { + @string, + number, + boolean, + @object, + array, + @null + } + + static JsonType GetJsonType(object obj) + { + if (obj == null) return JsonType.@null; + + return Type.GetTypeCode(obj.GetType()) switch + { + TypeCode.Boolean => JsonType.boolean, + TypeCode.String => JsonType.@string, + TypeCode.Char => JsonType.@string, + TypeCode.DateTime => JsonType.@string, + TypeCode.Int16 => JsonType.number, + TypeCode.Int32 => JsonType.number, + TypeCode.Int64 => JsonType.number, + TypeCode.UInt16 => JsonType.number, + TypeCode.UInt32 => JsonType.number, + TypeCode.UInt64 => JsonType.number, + TypeCode.Single => JsonType.number, + TypeCode.Double => JsonType.number, + TypeCode.Decimal => JsonType.number, + TypeCode.SByte => JsonType.number, + TypeCode.Byte => JsonType.number, + TypeCode.Object => (obj switch + { + // specialized for well known types + Uri _ => JsonType.@string, + DateTimeOffset _ => JsonType.@string, + Guid _ => JsonType.@string, + StringBuilder _ => JsonType.@string, + IDictionary _ => JsonType.@object, + _ => ((obj is IEnumerable) ? JsonType.array : JsonType.@object) + }), + TypeCode.DBNull => JsonType.@null, + TypeCode.Empty => JsonType.@null, + _ => JsonType.@null + }; + } + + static void SerializeObject(TextWriter tw, object o) + { + switch (GetJsonType(o)) + { + case JsonType.@string: + switch (o) + { + case string s when NotAPaddedNumber(s) && int.TryParse(s, out var n): + WriteNumber(tw, n); + break; + case string s: + WriteString(tw, s); + break; + case DateTime time: + { + var s = time.ToString("o"); + WriteString(tw, s); + break; + } + case DateTimeOffset offset: + { + var s = offset.ToString("o"); + WriteString(tw, s); + break; + } + default: + WriteString(tw, o.ToString()); + break; + } + + break; + case JsonType.number: + WriteNumber(tw, o); + break; + case JsonType.boolean: + WriteBoolean(tw, (bool) o); + break; + case JsonType.@object: + WriteObject(tw, o); + break; + case JsonType.array: + WriteArray(tw, (IEnumerable) o); + break; + case JsonType.@null: + WriteNull(tw); + break; + default: + break; + } + } + + static void WriteString(TextWriter tw, string o) + { + tw.Write('\"'); + + foreach (var c in o) + { + switch (c) + { + case '"': + tw.Write("\\\""); + break; + case '\\': + tw.Write("\\\\"); + break; + case '\b': + tw.Write("\\b"); + break; + case '\f': + tw.Write("\\f"); + break; + case '\n': + tw.Write("\\n"); + break; + case '\r': + tw.Write("\\r"); + break; + case '\t': + tw.Write("\\t"); + break; + default: + tw.Write(c); + break; + } + } + + tw.Write('\"'); + } + + static void WriteNumber(TextWriter tw, object o) + { + tw.Write(o.ToString()); + } + + static void WriteBoolean(TextWriter tw, bool o) + { + tw.Write(o ? "true" : "false"); + } + + static void WriteObject(TextWriter tw, object o) + { + tw.Write('{'); + + if (o is IDictionary dict) + { + // Dictionary + var isFirst = true; + foreach (DictionaryEntry item in dict) + { + if (!isFirst) tw.Write(","); + else isFirst = false; + + tw.Write('\"'); + tw.Write(item.Key); + tw.Write('\"'); + tw.Write(":"); + SerializeObject(tw, item.Value); + } + } + else + { + // Object + var isFirst = true; + foreach (var item in o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)) + { + if (!isFirst) tw.Write(","); + else isFirst = false; + + var key = item.Name; + var value = item.GetGetMethod().Invoke(o, null); // safe reflection for unity + tw.Write('\"'); + tw.Write(key); + tw.Write('\"'); + tw.Write(":"); + SerializeObject(tw, value); + } + + isFirst = true; + foreach (var item in o.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetField)) + { + if (!isFirst) tw.Write(","); + else isFirst = false; + + var key = item.Name; + var value = item.GetValue(o); + tw.Write('\"'); + tw.Write(key); + tw.Write('\"'); + tw.Write(":"); + SerializeObject(tw, value); + } + } + + tw.Write('}'); + } + + static void WriteArray(TextWriter tw, IEnumerable o) + { + tw.Write("["); + var isFirst = true; + foreach (var item in o) + { + if (!isFirst) tw.Write(","); + else isFirst = false; + + SerializeObject(tw, item); + } + + tw.Write("]"); + } + + static void WriteNull(TextWriter tw) + { + tw.Write("null"); + } + + private static bool NotAPaddedNumber(string value) => value == "0" || !value.StartsWith("0"); + + public static string FormatJson(string json) + { + var indentation = 0; + var quoteCount = 0; + var result = + from ch in json + let quotes = ch == '"' ? quoteCount++ : quoteCount + let lineBreak = ch == ',' && quotes % 2 == 0 ? ch + System.Environment.NewLine + string.Concat(Enumerable.Repeat(INDENT_STRING, indentation)) : null + let openChar = ch == '{' || ch == '[' ? ch + System.Environment.NewLine + string.Concat(Enumerable.Repeat(INDENT_STRING, ++indentation)) : ch.ToString() + let closeChar = ch == '}' || ch == ']' ? System.Environment.NewLine + string.Concat(Enumerable.Repeat(INDENT_STRING, --indentation)) + ch : ch.ToString() + select lineBreak ?? + (openChar.Length > 1 + ? openChar + : closeChar); + + return string.Concat(result); + } + } +} diff --git a/src/GitVersionCore/OutputFormatters/JsonOutputFormatter.cs b/src/GitVersionCore/OutputFormatters/JsonOutputFormatter.cs deleted file mode 100644 index cf4b52cb04..0000000000 --- a/src/GitVersionCore/OutputFormatters/JsonOutputFormatter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using GitVersion.OutputVariables; -using GitVersion.Extensions; - -namespace GitVersion.OutputFormatters -{ - public static class JsonOutputFormatter - { - public static string ToJson(VersionVariables variables) - { - var builder = new StringBuilder(); - builder.AppendLine("{"); - var last = variables.Last().Key; - foreach (var variable in variables) - { - var isLast = (variable.Key == last); - // preserve leading zeros for padding - if (int.TryParse(variable.Value, out var value) && NotAPaddedNumber(variable)) - builder.AppendLineFormat(" \"{0}\":{1}{2}", variable.Key, value, isLast ? string.Empty : ","); - else - builder.AppendLineFormat(" \"{0}\":\"{1}\"{2}", variable.Key, variable.Value, isLast ? string.Empty : ","); - } - - builder.Append("}"); - return builder.ToString(); - } - - private static bool NotAPaddedNumber(KeyValuePair variable) - { - if (variable.Value == "0") - return true; - - return !variable.Value.StartsWith("0"); - } - } -} diff --git a/src/GitVersionCore/OutputVariables/VersionVariables.cs b/src/GitVersionCore/OutputVariables/VersionVariables.cs index a89f0180a6..89205c6c1a 100644 --- a/src/GitVersionCore/OutputVariables/VersionVariables.cs +++ b/src/GitVersionCore/OutputVariables/VersionVariables.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using GitVersion.Helpers; using YamlDotNet.Serialization; namespace GitVersion.OutputVariables @@ -181,5 +182,11 @@ public bool ContainsKey(string variable) private sealed class ReflectionIgnoreAttribute : Attribute { } + + public override string ToString() + { + var json = JsonSerializer.Serialize(this); + return JsonSerializer.FormatJson(json); + } } } diff --git a/src/GitVersionExe/ExecCommand.cs b/src/GitVersionExe/ExecCommand.cs index 38a40ff958..8ffc471773 100644 --- a/src/GitVersionExe/ExecCommand.cs +++ b/src/GitVersionExe/ExecCommand.cs @@ -54,7 +54,7 @@ public void Execute() switch (arguments.ShowVariable) { case null: - Console.WriteLine(JsonOutputFormatter.ToJson(variables)); + Console.WriteLine(variables.ToString()); break; default: