Skip to content

Commit

Permalink
Fixed the logic for convert Json -> Yaml for datetime
Browse files Browse the repository at this point in the history
Updated the JsonHelper `GetScalerValue` to use the built int logic for serialization rather then the System.ConvertMethod
  • Loading branch information
ByronMayne committed Mar 12, 2024
1 parent d11fdfa commit 6ad314c
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 37 additions & 3 deletions src/LEGO.AsyncAPI.Readers/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Contains helper methods for working with Json
/// </summary>
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,
};
}

/// <summary>
/// Takes a <see cref="JsonValue"/> and converts it into a string value.
/// </summary>
/// <param name="jsonValue">The node to convert.</param>
/// <returns>The string value.</returns>
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<object>(), 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)
Expand Down
2 changes: 1 addition & 1 deletion src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public string GetReferencePointer()
return null;
}

return refNode.GetScalarValue();
return refNode.AsValue().GetScalarValue();
}

public string GetScalarValue(ValueNode key)
Expand Down
2 changes: 1 addition & 1 deletion src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 26 additions & 7 deletions src/LEGO.AsyncAPI.Readers/YamlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(intValue);
}

if (double.TryParse(value, out double doubleValue))
{
return JsonValue.Create<double>(doubleValue);
}

if (DateTime.TryParse(value, out DateTime dateTimeValue))
{
return JsonValue.Create<DateTime>(dateTimeValue);
}

if (bool.TryParse(value, out bool boolValue))
{
return JsonValue.Create<bool>(boolValue);
}

return JsonValue.Create<string>(value);
case ScalarStyle.SingleQuoted:
case ScalarStyle.DoubleQuoted:
case ScalarStyle.Literal:
case ScalarStyle.Folded:
case ScalarStyle.Any:
return JsonValue.Create(yaml.Value);
return JsonValue.Create<string>(yaml.Value);
default:
throw new ArgumentOutOfRangeException();
}
Expand Down
3 changes: 2 additions & 1 deletion test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>11</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
120 changes: 120 additions & 0 deletions test/LEGO.AsyncAPI.Tests/Readers/YamlConverterTests.cs
Original file line number Diff line number Diff line change
@@ -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<Type>? assertValueType = null,
Func<string>? 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);
}
}
}
}
13 changes: 13 additions & 0 deletions test/LEGO.AsyncAPI.Tests/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 6ad314c

Please sign in to comment.