diff --git a/.editorconfig b/.editorconfig index 8c36c97f..9935e9f3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,6 @@ # SA1623: Property summary documentation should match accessors dotnet_diagnostic.SA1623.severity = suggestion + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none diff --git a/src/LEGO.AsyncAPI.Readers/JsonHelper.cs b/src/LEGO.AsyncAPI.Readers/JsonHelper.cs index 5f7fc584..49a54a55 100644 --- a/src/LEGO.AsyncAPI.Readers/JsonHelper.cs +++ b/src/LEGO.AsyncAPI.Readers/JsonHelper.cs @@ -4,15 +4,49 @@ namespace LEGO.AsyncAPI.Readers { using System; using System.Globalization; + using System.IO; + using System.Text.Encodings.Web; + using System.Text.Json; using System.Text.Json.Nodes; using LEGO.AsyncAPI.Exceptions; + /// + /// Contains helper methods for working with Json + /// internal static class JsonHelper { - public static string GetScalarValue(this JsonNode node) + private static readonly JsonWriterOptions WriterOptions; + + static JsonHelper() + { + WriterOptions = new JsonWriterOptions() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false, + MaxDepth = 1, + SkipValidation = true, + }; + } + + /// + /// Takes a and converts it into a string value. + /// + /// The node to convert. + /// The string value. + public static string GetScalarValue(this JsonValue jsonValue) { - var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); - return Convert.ToString(scalarNode.GetValue(), CultureInfo.InvariantCulture); + using (MemoryStream memoryStream = new MemoryStream()) + using (Utf8JsonWriter writer = new Utf8JsonWriter(memoryStream, WriterOptions)) + { + jsonValue.WriteTo(writer); + writer.Flush(); + memoryStream.Position = 0; + using (StreamReader reader = new StreamReader(memoryStream)) + { + string value = reader.ReadToEnd(); + return value.Trim('"'); + } + } } public static JsonNode ParseJsonString(string jsonString) diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs index 508a6cdd..559017db 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -196,7 +196,7 @@ public string GetReferencePointer() return null; } - return refNode.GetScalarValue(); + return refNode.AsValue().GetScalarValue(); } public string GetScalarValue(ValueNode key) diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs index 17ec9ac7..eb1f8183 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -26,7 +26,7 @@ public override string GetScalarValue() { if (this.cachedScalarValue == null) { - this.cachedScalarValue = this.node.GetScalarValue(); + this.cachedScalarValue = this.node.AsValue().GetScalarValue(); } return this.cachedScalarValue; diff --git a/src/LEGO.AsyncAPI.Readers/YamlConverter.cs b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs index e3dfcd43..d7aac128 100644 --- a/src/LEGO.AsyncAPI.Readers/YamlConverter.cs +++ b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs @@ -47,22 +47,41 @@ public static JsonNode ToJsonNode(this YamlNode yaml) }; } - private static JsonValue ToJsonValue(this YamlScalarNode yaml) + public static JsonValue ToJsonValue(this YamlScalarNode yaml) { + string value = yaml.Value; + switch (yaml.Style) { case ScalarStyle.Plain: - return decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) - ? JsonValue.Create(d) - : bool.TryParse(yaml.Value, out var b) - ? JsonValue.Create(b) - : JsonValue.Create(yaml.Value)!; + // We need to guess the types just based on it's format, so that means parsing + if (int.TryParse(value, out int intValue)) + { + return JsonValue.Create(intValue); + } + + if (double.TryParse(value, out double doubleValue)) + { + return JsonValue.Create(doubleValue); + } + + if (DateTime.TryParse(value, out DateTime dateTimeValue)) + { + return JsonValue.Create(dateTimeValue); + } + + if (bool.TryParse(value, out bool boolValue)) + { + return JsonValue.Create(boolValue); + } + + return JsonValue.Create(value); case ScalarStyle.SingleQuoted: case ScalarStyle.DoubleQuoted: case ScalarStyle.Literal: case ScalarStyle.Folded: case ScalarStyle.Any: - return JsonValue.Create(yaml.Value); + return JsonValue.Create(yaml.Value); default: throw new ArgumentOutOfRangeException(); } diff --git a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj index f10a92bc..5c798cb8 100644 --- a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj +++ b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj @@ -1,7 +1,8 @@ - + net6.0 + 11 disable enable false diff --git a/test/LEGO.AsyncAPI.Tests/Readers/YamlConverterTests.cs b/test/LEGO.AsyncAPI.Tests/Readers/YamlConverterTests.cs new file mode 100644 index 00000000..154331e0 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Readers/YamlConverterTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Readers +{ + using System; + using System.Reflection; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + using YamlDotNet.RepresentationModel; + + internal class YamlConverterTests + { + private static readonly MethodInfo GenericGetValueMethodInfo; + + static YamlConverterTests() + { + GenericGetValueMethodInfo = typeof(JsonValue) + .GetMethod("GetValue", BindingFlags.Public | BindingFlags.Instance)!; + } + + [Test] + public void ToJsonValue_PlainString_CanGetStringValue() + => ComposeJsonValue( + input: "hello world", + assertValueType: () => typeof(string)); + + [Test] + [TestCase("true")] + [TestCase("false")] + public void ToJsonValue_PlainBoolean_CanGetBoolValue(string input) + => ComposeJsonValue( + input: input, + assertValueType: () => typeof(bool)); + + [Test] + [TestCase("2022-12-31")] // Canonical + [TestCase("2022-12-31T18:59:59-05:00")] // ISO 8601 + [TestCase("2001-12-14 21:59:43.10 -5")] // Spaced + public void ToJsonValue_PlainDateTime_CanGetDateTimeValue(string input) + => ComposeJsonValue( + input: input, + assertValueType: () => typeof(DateTime)); + + [Test] + public void ToJsonValue_PlainInt_CanGetIntValue() + => ComposeJsonValue( + input: "2022", + assertValueType: () => typeof(int)); + + [Test] + public void ToJsonValue_PlainDouble_CanGetDoubleValue() + => ComposeJsonValue( + input: "2022.20", + assertValueType: () => typeof(double)); + + [Test] + public void GetScalarValue_PlainString_MatchesExpectedScalerValue() + => ComposeJsonValue( + input: "hello world", + assertScalerValue: () => "hello world"); + + [Test] + [TestCase("true")] + [TestCase("false")] + public void GetScalarValue_PlainBoolean_MatchesExpectedScalerValue(string input) + => ComposeJsonValue( + input: input, + assertScalerValue: () => input); + + [Test] + public void GetScalarValue_PlainDateTime_MatchesExpectedScalerValue() + => ComposeJsonValue( + input: "2022-12-31T18:59:59-05:00", + assertScalerValue: () => "2022-12-31T18:59:59-05:00"); + + [Test] + public void GetScalarValue_PlainInt_MatchesExpectedScalerValue() + => ComposeJsonValue( + input: "2022", + assertScalerValue: () => "2022"); + + [Test] + public void GetScalarValue_PlainDouble_MatchesExpectedScalerValue() + => ComposeJsonValue( + input: "2022.20", + assertScalerValue: () => "2022.2"); // extra zero dropped + + private void ComposeJsonValue( + string input, + Func? assertValueType = null, + Func? assertScalerValue = null) + { + YamlScalarNode node = new YamlScalarNode(input) + { + Style = YamlDotNet.Core.ScalarStyle.Plain, + }; + + JsonValue jValue = node.ToJsonValue(); + + jValue.GetScalarValue(); + + if (assertValueType != null) + { + Type valueType = assertValueType(); + MethodInfo genericGetValueMethod = GenericGetValueMethodInfo.MakeGenericMethod(valueType); + object? result = genericGetValueMethod.Invoke(jValue, null); + Assert.IsNotNull(result); + Assert.IsInstanceOf(valueType, result); + } + + if (assertScalerValue != null) + { + string expectedValue = assertScalerValue(); + string actualValue = jValue.GetScalarValue(); + Assert.AreEqual(expectedValue, actualValue); + } + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/StringExtensions.cs b/test/LEGO.AsyncAPI.Tests/StringExtensions.cs index 2f0fddb8..f9a74f6e 100644 --- a/test/LEGO.AsyncAPI.Tests/StringExtensions.cs +++ b/test/LEGO.AsyncAPI.Tests/StringExtensions.cs @@ -3,9 +3,22 @@ namespace LEGO.AsyncAPI.Tests { using System; + using System.IO; public static class StringExtensions { + public static Stream ToStream(this string input) + { + Stream stream = new MemoryStream(); + using (StreamWriter writer = new StreamWriter(stream, leaveOpen: true)) + { + writer.Write(input); + } + + stream.Position = 0; + return stream; + } + public static string MakeLineBreaksEnvironmentNeutral(this string input) { return input.Replace("\r\n", "\n") diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index 7cf8189e..dc52410b 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -62,11 +62,11 @@ public void GetYamlCompatibleString_FalseString_WrappedWithQuotes() [Test] public void GetYamlCompatibleString_DateTimeSlashString_WrappedWithQuotes() - => this.Compose("12/31/2022 23:59:59", "'12/31/2022 23:59:59'"); + => this.Compose("12/31/2022 23:59:59", "12/31/2022 23:59:59"); [Test] public void GetYamlCompatibleString_DateTimeDashString_WrappedWithQuotes() - => this.Compose("2022-12-31 23:59:59", "'2022-12-31 23:59:59'"); + => this.Compose("2022-12-31 23:59:59", "2022-12-31 23:59:59"); [Test] public void GetYamlCompatibleString_DateTimeISOString_NotWrappedWithQuotes()