From 5419a1ece4b3ea16163a8a869d461b33c4ec8908 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Sun, 10 Mar 2024 14:14:38 -0400 Subject: [PATCH 1/9] Converted the project to .netstandard This allows it to be used by any project type rather then just .netframework --- AsyncAPI.sln | 3 ++- Common.Build.props | 13 ++++++++++++ .../BindingsCollection.cs | 11 ++++++++-- .../LEGO.AsyncAPI.Bindings.csproj | 21 ++++++------------- .../LEGO.AsyncAPI.Readers.csproj | 17 +++++---------- src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj | 19 ++++++----------- 6 files changed, 41 insertions(+), 43 deletions(-) create mode 100644 Common.Build.props diff --git a/AsyncAPI.sln b/AsyncAPI.sln index f972fa3d..db79f153 100644 --- a/AsyncAPI.sln +++ b/AsyncAPI.sln @@ -12,9 +12,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE167614-5BCB-4046-BD4C-ABB70E9F3462}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + Common.Build.props = Common.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LEGO.AsyncAPI.Bindings", "src\LEGO.AsyncAPI.Bindings\LEGO.AsyncAPI.Bindings.csproj", "{33CA31F4-ECFE-4227-BFE9-F49783DD29A0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LEGO.AsyncAPI.Bindings", "src\LEGO.AsyncAPI.Bindings\LEGO.AsyncAPI.Bindings.csproj", "{33CA31F4-ECFE-4227-BFE9-F49783DD29A0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Common.Build.props b/Common.Build.props new file mode 100644 index 00000000..f4f9797f --- /dev/null +++ b/Common.Build.props @@ -0,0 +1,13 @@ + + + + 10 + netstandard2.0 + disable + The LEGO Group + https://github.com/LEGO/AsyncAPI.NET + README.md + https://github.com/LEGO/AsyncAPI.NET + asyncapi .net openapi documentation + + \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs index e52392b6..ccfad8a4 100644 --- a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs @@ -19,8 +19,15 @@ public static TCollection Add( IEnumerable source) where TCollection : ICollection { - ArgumentNullException.ThrowIfNull(destination); - ArgumentNullException.ThrowIfNull(source); + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } if (destination is List list) { diff --git a/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj index e05aacab..751ea5c6 100644 --- a/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj +++ b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj @@ -1,20 +1,11 @@ - - + + - net6.0 - disable - The LEGO Group - https://github.com/LEGO/AsyncAPI.NET - README.md - AsyncAPI.NET Bindings - asyncapi .net openapi documentation - AsyncAPI.NET.Bindings - LEGO.AsyncAPI.Bindings - LEGO.AsyncAPI.Bindings - https://github.com/LEGO/AsyncAPI.NET + AsyncAPI.NET Bindings + AsyncAPI.NET.Bindings + LEGO.AsyncAPI.Bindings + LEGO.AsyncAPI.Bindings - - diff --git a/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj index 6fd9d2e7..b0164afd 100644 --- a/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj +++ b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj @@ -1,18 +1,11 @@  - + - net6.0 - disable disable - The LEGO Group - https://github.com/LEGO/AsyncAPI.NET - README.md - AsyncAPI.NET Readers for JSON and YAML documents - asyncapi .net openapi documentation - AsyncAPI.NET.Readers - LEGO.AsyncAPI.Readers - LEGO.AsyncAPI.Readers - https://github.com/LEGO/AsyncAPI.NET + AsyncAPI.NET Readers for JSON and YAML documents + AsyncAPI.NET.Readers + LEGO.AsyncAPI.Readers + LEGO.AsyncAPI.Readers diff --git a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj index 8d4b9501..c2dd1598 100644 --- a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj +++ b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj @@ -1,19 +1,11 @@  - + - net6.0 - disable - The LEGO Group - https://github.com/LEGO/AsyncAPI.NET - README.md - AsyncAPI.NET models - asyncapi .net openapi documentation - AsyncAPI.NET - LEGO.AsyncAPI - LEGO.AsyncAPI - https://github.com/LEGO/AsyncAPI.NET + AsyncAPI.NET models + AsyncAPI.NET + LEGO.AsyncAPI + LEGO.AsyncAPI - @@ -27,6 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + <_Parameter1>$(MSBuildProjectName).Tests From ca7b075865f428eb15dc1a63f2711470aaad5561 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Sun, 10 Mar 2024 15:13:49 -0400 Subject: [PATCH 2/9] Fixed unit tests being broken for serialization Added new unit test to validate the special characters string extensions, which was failing tests --- .../SpecialCharacterStringExtensions.cs | 36 +- .../AsyncApiDocumentV2Tests.cs | 407 +++++++++--------- .../LEGO.AsyncAPI.Tests.csproj | 98 ++--- .../SpecialCharacterStringExtensionsTests.cs | 73 ++++ 4 files changed, 359 insertions(+), 255 deletions(-) create mode 100644 test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index 1a091066..60f4fd8e 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -100,10 +100,15 @@ public static class SpecialCharacterStringExtensions /// Escapes all special characters and put the string in quotes if necessary to /// get a YAML-compatible string. /// - internal static string GetYamlCompatibleString(this string input) + internal static string GetYamlCompatibleString(this string? input) { + if (input == null) + { + return "null"; + } + // If string is an empty string, wrap it in quote to ensure it is not recognized as null. - if (input == "") + if (input.Length == 0) { return "''"; } @@ -163,7 +168,7 @@ internal static string GetYamlCompatibleString(this string input) input = input.Replace("\x1e", "\\x1e"); input = input.Replace("\x1f", "\\x1f"); - return $"\"{input}\""; + return $"'{input}'"; } // If string @@ -183,11 +188,26 @@ internal static string GetYamlCompatibleString(this string input) return $"'{input}'"; } - // If string can be mistaken as a number, a boolean, or a timestamp, - // wrap it in quote to indicate that this is indeed a string, not a number, a boolean, or a timestamp - if (decimal.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var _) || - bool.TryParse(input, out var _) || - DateTime.TryParse(input, out var _)) + // Handle lexemes that can be intperated as as string + // https://yaml.org/spec/1.2-old/spec.html#id2761292 + switch (input.ToLower()) + { + // Example 2.20. Floating Point + case "-.inf": + case ".inf": + case ".nan": + // Example 2.21. Miscellaneous + case "null": + + // Booleans + case "true": + case "false": + return $"'{input}'"; + } + + // Handle numbers + char first = input[0]; + if (char.IsDigit(first) || first == '-' || first == '+') { return $"'{input}'"; } diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index a598ac18..d78ea65e 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -22,8 +22,19 @@ public class ExtensionClass public string Key { get; set; } public long OtherKey { get; set; } } + public class AsyncApiDocumentV2Tests { + [Test] + public void AsyncApiDocumentYaml_DefalutVersionNumber_IsWrappedInSingleQuotes() + { + string actual = new AsyncApiDocumentBuilder() + .Build() + .SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + Assert.AreEqual("asyncapi: '2.6.0'\ninfo: { }", actual); + } + [Test] public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() { @@ -32,7 +43,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() @"asyncapi: '2.6.0' info: title: Streetlights Kafka API - version: 1.0.0 + version: '1.0.0' description: The Smartylighting Streetlights API allows you to remotely manage the city lights. license: name: Apache 2.0 @@ -200,25 +211,25 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() maximum: 100 minimum: 0"; - var asyncApiDocument = new AsyncApiDocumentBuilder() - .WithInfo(new AsyncApiInfo - { - Title = "Streetlights Kafka API", - Version = "1.0.0", - Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", - License = new AsyncApiLicense + var asyncApiDocument = new AsyncApiDocumentBuilder() + .WithInfo(new AsyncApiInfo { - Name = "Apache 2.0", - Url = new Uri("https://www.apache.org/licenses/LICENSE-2.0"), - }, - }) - .WithServer("scram-connections", new AsyncApiServer - { - Url = "test.mykafkacluster.org:18092", - Protocol = "kafka-secure", - Description = "Test broker secured with scramSha256", - Security = new List + Title = "Streetlights Kafka API", + Version = "1.0.0", + Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + License = new AsyncApiLicense + { + Name = "Apache 2.0", + Url = new Uri("https://www.apache.org/licenses/LICENSE-2.0"), + }, + }) + .WithServer("scram-connections", new AsyncApiServer { + Url = "test.mykafkacluster.org:18092", + Protocol = "kafka-secure", + Description = "Test broker secured with scramSha256", + Security = new List + { new AsyncApiSecurityRequirement { { @@ -232,9 +243,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, new List() }, }, - }, - Tags = new List - { + }, + Tags = new List + { new AsyncApiTag { Name = "env:test-scram", @@ -250,15 +261,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Name = "visibility:private", Description = "This resource is private and only available to certain users", }, - }, - }) - .WithServer("mtls-connections", new AsyncApiServer - { - Url = "test.mykafkacluster.org:28092", - Protocol = "kafka-secure", - Description = "Test broker secured with X509", - Security = new List + }, + }) + .WithServer("mtls-connections", new AsyncApiServer { + Url = "test.mykafkacluster.org:28092", + Protocol = "kafka-secure", + Description = "Test broker secured with X509", + Security = new List + { new AsyncApiSecurityRequirement { { @@ -272,9 +283,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, new List() }, }, - }, - Tags = new List - { + }, + Tags = new List + { new AsyncApiTag { Name = "env:test-mtls", @@ -290,16 +301,16 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Name = "visibility:private", Description = "This resource is private and only available to certain users", }, - }, - }) - .WithDefaultContentType() - .WithChannel( - "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", - new AsyncApiChannel() - { - Description = "The topic on which measured values may be produced and consumed.", - Parameters = new Dictionary + }, + }) + .WithDefaultContentType() + .WithChannel( + "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + new AsyncApiChannel() { + Description = "The topic on which measured values may be produced and consumed.", + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -310,13 +321,13 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Publish = new AsyncApiOperation() - { - Summary = "Inform about environmental lighting conditions of a particular streetlight.", - OperationId = "receiveLightMeasurement", - Traits = new List + }, + Publish = new AsyncApiOperation() { + Summary = "Inform about environmental lighting conditions of a particular streetlight.", + OperationId = "receiveLightMeasurement", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -325,9 +336,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -336,15 +347,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -355,12 +366,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "turnOn", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "turnOn", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -369,9 +380,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -380,15 +391,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -399,12 +410,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "turnOff", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "turnOff", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -413,9 +424,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -424,15 +435,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.dim", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.dim", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -443,12 +454,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "dimLight", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "dimLight", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -457,9 +468,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -468,17 +479,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithComponent("lightMeasured", new AsyncApiMessage() - { - Name = "lightMeasured", - Title = "Light measured", - Summary = "Inform about environmental lighting conditions of a particular streetlight.", - ContentType = "application/json", - Traits = new List() + }) + .WithComponent("lightMeasured", new AsyncApiMessage() { + Name = "lightMeasured", + Title = "Light measured", + Summary = "Inform about environmental lighting conditions of a particular streetlight.", + ContentType = "application/json", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -487,23 +498,23 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "lightMeasuredPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "lightMeasuredPayload", + }, }, - }, - }) - .WithComponent("turnOnOff", new AsyncApiMessage() - { - Name = "turnOnOff", - Title = "Turn on/off", - Summary = "Command a particular streetlight to turn the lights on or off.", - Traits = new List() + }) + .WithComponent("turnOnOff", new AsyncApiMessage() { + Name = "turnOnOff", + Title = "Turn on/off", + Summary = "Command a particular streetlight to turn the lights on or off.", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -512,23 +523,23 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "turnOnOffPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "turnOnOffPayload", + }, }, - }, - }) - .WithComponent("dimLight", new AsyncApiMessage() - { - Name = "dimLight", - Title = "Dim light", - Summary = "Command a particular streetlight to dim the lights.", - Traits = new List() + }) + .WithComponent("dimLight", new AsyncApiMessage() { + Name = "dimLight", + Title = "Dim light", + Summary = "Command a particular streetlight to dim the lights.", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -537,21 +548,21 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "dimLightPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "dimLightPayload", + }, }, - }, - }) - .WithComponent("lightMeasuredPayload", new AsyncApiSchema() - { - Type = SchemaType.Object, - Properties = new Dictionary() + }) + .WithComponent("lightMeasuredPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "lumens", new AsyncApiSchema() { @@ -570,13 +581,13 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("turnOnOffPayload", new AsyncApiSchema() - { - Type = SchemaType.Object, - Properties = new Dictionary() + }, + }) + .WithComponent("turnOnOffPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "command", new AsyncApiSchema() { @@ -599,13 +610,13 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("dimLightPayload", new AsyncApiSchema() - { - Type = SchemaType.Object, - Properties = new Dictionary() + }, + }) + .WithComponent("dimLightPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "percentage", new AsyncApiSchema() { @@ -625,40 +636,40 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("sentAt", new AsyncApiSchema() - { - Type = SchemaType.String, - Format = "date-time", - Description = "Date and time when the message was sent.", - - }) - .WithComponent("saslScram", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.ScramSha256, - Description = "Provide your username and password for SASL/SCRAM authentication", - }) - .WithComponent("certs", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.X509, - Description = "Download the certificate files from service provider", - }) - .WithComponent("streetlightId", new AsyncApiParameter() - { - Description = "The ID of the streetlight.", - Schema = new AsyncApiSchema() + }, + }) + .WithComponent("sentAt", new AsyncApiSchema() { Type = SchemaType.String, - }, - }) - .WithComponent("commonHeaders", new AsyncApiMessageTrait() - { - Headers = new AsyncApiSchema() + Format = "date-time", + Description = "Date and time when the message was sent.", + + }) + .WithComponent("saslScram", new AsyncApiSecurityScheme { - Type = SchemaType.Object, - Properties = new Dictionary() + Type = SecuritySchemeType.ScramSha256, + Description = "Provide your username and password for SASL/SCRAM authentication", + }) + .WithComponent("certs", new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.X509, + Description = "Download the certificate files from service provider", + }) + .WithComponent("streetlightId", new AsyncApiParameter() + { + Description = "The ID of the streetlight.", + Schema = new AsyncApiSchema() { + Type = SchemaType.String, + }, + }) + .WithComponent("commonHeaders", new AsyncApiMessageTrait() + { + Headers = new AsyncApiSchema() + { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "my-app-header", new AsyncApiSchema() { @@ -667,13 +678,13 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Maximum = 100, } }, + }, }, - }, - }) - .WithComponent("kafka", new AsyncApiOperationTrait() - { - Bindings = new AsyncApiBindings() + }) + .WithComponent("kafka", new AsyncApiOperationTrait() { + Bindings = new AsyncApiBindings() + { { "kafka", new KafkaOperationBinding() { @@ -687,9 +698,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .Build(); + }, + }) + .Build(); // Act var actual = asyncApiDocument.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); @@ -868,7 +879,7 @@ public void SerializeV2_WithFullSpec_Serializes() string authorizationUrl = "https://example.com/authorization"; string requirementString = "requirementItem"; - + var document = new AsyncApiDocument() { Id = documentId, @@ -1171,7 +1182,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() Id = "bindings", }, }, - } + } } }, ServerBindings = new Dictionary>() @@ -1193,7 +1204,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() { new PulsarChannelBinding() { - Namespace = "users", + Namespace = "users", Persistence = AsyncAPI.Models.Bindings.Pulsar.Persistence.Persistent, } } @@ -1216,7 +1227,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() var reader = new AsyncApiStringReader(settings); var deserialized = reader.Read(actual, out var diagnostic); } - + [Test] public void Serializev2_WithBindings_Serializes() { diff --git a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj index 1be4a264..f10a92bc 100644 --- a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj +++ b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj @@ -1,55 +1,55 @@  - - net6.0 - disable - enable + + net6.0 + disable + enable + false + $(NoWarn);SA1600 + - false - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs new file mode 100644 index 00000000..a71b678e --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Writers +{ + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + using System; + + internal class SpecialCharacterStringExtensionsTests + { + [Test] + public void GetYamlCompatibleString_NullValue_ReturnsNull() + => this.Compose(null, "null"); + + [Test] + public void GetYamlCompatibleString_EmptyValue_ReturnsNull() + => this.Compose(string.Empty, "''"); + + [Test] + public void GetYamlCompatibleString_NullWordString_ReturnsWrappedValue() + => this.Compose("null", "'null'"); + + [Test] + public void GetYamlCompatibleString_TildaWordString_ReturnsWrappedValue() + => this.Compose("~", "'~'"); + + [Test] + [TestCase("\0", "\\0")] + [TestCase("\x01", "\\x01")] + [TestCase("\x02", "\\x02")] + [TestCase("\x03", "\\x03")] + [TestCase("\x04", "\\x04")] + [TestCase("\x05", "\\x05")] + [TestCase("\x06", "\\x06")] + [TestCase("\a", "\\a")] + [TestCase("\b", "\\b")] + [TestCase("\t", "\\t")] + [TestCase("\n", "\\n")] + [TestCase("\v", "\\v")] + [TestCase("\f", "\\f")] + [TestCase("\r", "\\r")] + [TestCase("\x0e", "\\x0e")] + [TestCase("\x0f", "\\x0f")] + [TestCase("\x10", "\\x10")] + [TestCase("\x11", "\\x11")] + [TestCase("\x12", "\\x12")] + [TestCase("\x13", "\\x13")] + [TestCase("\x14", "\\x14")] + [TestCase("\x15", "\\x15")] + [TestCase("\x16", "\\x16")] + [TestCase("\x17", "\\x17")] + [TestCase("\x18", "\\x18")] + [TestCase("\x19", "\\x19")] + [TestCase("\x1a", "\\x1a")] + [TestCase("\x1b", "\\x1b")] + [TestCase("\x1c", "\\x1c")] + [TestCase("\x1d", "\\x1d")] + [TestCase("\x1e", "\\x1e")] + [TestCase("\x1f", "\\x1f")] + public void GetYamlCompatibleString_ControlCharacters_AreEscaped(string input, string expected) + => this.Compose($"value {input}", $"'value {expected}'"); + + private void Compose( + string? input, + string expected) + { + string actual = SpecialCharacterStringExtensions.GetYamlCompatibleString(input); + Console.WriteLine($"Expected: <{expected}>"); + Console.WriteLine($"Actual: <{actual}>"); + Assert.AreEqual(expected, actual); + } + } +} From 010e2dc0b0d70a485d8f28ff6bfd9780b24f02e1 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Sun, 10 Mar 2024 15:32:17 -0400 Subject: [PATCH 3/9] Fixed all the unit tests and added more tests --- .../SpecialCharacterStringExtensions.cs | 6 ++-- .../AsyncApiDocumentV2Tests.cs | 12 +++---- .../Models/AsyncApiSchema_Should.cs | 4 +-- .../SpecialCharacterStringExtensionsTests.cs | 36 +++++++++++++++++++ 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index 60f4fd8e..bb515ae3 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -5,9 +5,12 @@ namespace LEGO.AsyncAPI.Writers using System; using System.Globalization; using System.Linq; + using System.Text.RegularExpressions; public static class SpecialCharacterStringExtensions { + private static readonly Regex numberRegex = new Regex("^[+-]?[0-9]*\\.?[0-9]*$", RegexOptions.Compiled); + // Plain style strings cannot start with indicators. // http://www.yaml.org/spec/1.2/spec.html#indicator// private static readonly char[] yamlIndicators = @@ -206,8 +209,7 @@ internal static string GetYamlCompatibleString(this string? input) } // Handle numbers - char first = input[0]; - if (char.IsDigit(first) || first == '-' || first == '+') + if (numberRegex.IsMatch(input)) { return $"'{input}'"; } diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index d78ea65e..fb2ab526 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -26,13 +26,13 @@ public class ExtensionClass public class AsyncApiDocumentV2Tests { [Test] - public void AsyncApiDocumentYaml_DefalutVersionNumber_IsWrappedInSingleQuotes() + public void AsyncApiDocumentYaml_DefalutVersionNumber_IsNotWrappedInSingleQuotes() { string actual = new AsyncApiDocumentBuilder() .Build() .SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - Assert.AreEqual("asyncapi: '2.6.0'\ninfo: { }", actual); + Assert.AreEqual("asyncapi: 2.6.0\ninfo: { }", actual); } [Test] @@ -40,10 +40,10 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() { // Arrange var expected = -@"asyncapi: '2.6.0' +@"asyncapi: 2.6.0 info: title: Streetlights Kafka API - version: '1.0.0' + version: 1.0.0 description: The Smartylighting Streetlights API allows you to remotely manage the city lights. license: name: Apache 2.0 @@ -715,7 +715,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() public void SerializeV2_WithFullSpec_Serializes() { var expected = - @"asyncapi: '2.6.0' + @"asyncapi: 2.6.0 info: title: apiTitle version: apiVersion @@ -1231,7 +1231,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() [Test] public void Serializev2_WithBindings_Serializes() { - var expected = @"asyncapi: '2.6.0' + var expected = @"asyncapi: 2.6.0 info: description: test description servers: diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs index 686ed9b6..cce55a71 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -284,7 +284,7 @@ public class AsyncApiSchema_Should }; private string NoInlinedReferences => - @"asyncapi: '2.6.0' + @"asyncapi: 2.6.0 info: title: Streetlights Kafka API version: 1.0.0 @@ -320,7 +320,7 @@ public class AsyncApiSchema_Should description: test"; private string InlinedReferences => - @"asyncapi: '2.6.0' + @"asyncapi: 2.6.0 info: title: Streetlights Kafka API version: 1.0.0 diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index a71b678e..f58e1b99 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -24,6 +24,42 @@ public void GetYamlCompatibleString_NullWordString_ReturnsWrappedValue() public void GetYamlCompatibleString_TildaWordString_ReturnsWrappedValue() => this.Compose("~", "'~'"); + [Test] + public void GetYamlCompatibleString_IntegerWithTwoPeriods_RendersPlainStyle() + => this.Compose("1.2.3", "1.2.3"); + + [Test] + public void GetYamlCompatibleString_Float_WrappedWithQuotes() + => this.Compose("1.2", "'1.2'"); + + [Test] + public void GetYamlCompatibleString_PositiveFloat_WrappedWithQuotes() + => this.Compose("+1.2", "'+1.2'"); + + [Test] + public void GetYamlCompatibleString_NegativeFloat_WrappedWithQuotes() + => this.Compose("-1.2", "'-1.2'"); + + [Test] + public void GetYamlCompatibleString_PositiveInfinityFloat_WrappedWithQuotes() + => this.Compose(".inf", "'.inf'"); + + [Test] + public void GetYamlCompatibleString_NegativeInfinityFloat_WrappedWithQuotes() + => this.Compose("-.inf", "'-.inf'"); + + [Test] + public void GetYamlCompatibleString_NanFloat_WrappedWithQuotes() + => this.Compose(".nan", "'.nan'"); + + [Test] + public void GetYamlCompatibleString_TrueString_WrappedWithQuotes() + => this.Compose("true", "'true'"); + + [Test] + public void GetYamlCompatibleString_FalseString_WrappedWithQuotes() + => this.Compose("false", "'flase'"); + [Test] [TestCase("\0", "\\0")] [TestCase("\x01", "\\x01")] From 9f4c2f2e83dfc7224d9f7e6ae040c7928a3d3d52 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 11 Mar 2024 08:29:41 +0100 Subject: [PATCH 4/9] Update src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs --- src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index bb515ae3..b7873266 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -9,7 +9,7 @@ namespace LEGO.AsyncAPI.Writers public static class SpecialCharacterStringExtensions { - private static readonly Regex numberRegex = new Regex("^[+-]?[0-9]*\\.?[0-9]*$", RegexOptions.Compiled); + private static readonly Regex numberRegex = new Regex("^[+-]?[0-9]*\\.?[0-9]*$", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); // Plain style strings cannot start with indicators. // http://www.yaml.org/spec/1.2/spec.html#indicator// From 30559ef3b5656b08ef08840eda46f6ec7614309d Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 11 Mar 2024 08:33:47 +0100 Subject: [PATCH 5/9] Update test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs --- .../Writers/SpecialCharacterStringExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index f58e1b99..4e46d469 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -58,7 +58,7 @@ public void GetYamlCompatibleString_TrueString_WrappedWithQuotes() [Test] public void GetYamlCompatibleString_FalseString_WrappedWithQuotes() - => this.Compose("false", "'flase'"); + => this.Compose("false", "'false'"); [Test] [TestCase("\0", "\\0")] From aff12b79caa3aeab010dd7e28942aa089f26139f Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 11 Mar 2024 09:09:25 +0100 Subject: [PATCH 6/9] Update test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs --- .../Writers/SpecialCharacterStringExtensionsTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index 4e46d469..08578840 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -61,6 +61,17 @@ public void GetYamlCompatibleString_FalseString_WrappedWithQuotes() => this.Compose("false", "'false'"); [Test] + public void GetYamlCompatibleString_DateTimeSlashString_WrappedWithQuotes() + => 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'"); + + [Test] + public void GetYamlCompatibleString_DateTimeISOString_NotWrappedWithQuotes() + => this.Compose("2022-12-31T23:59:59Z", "2022-12-31T23:59:59Z"); + [Test] [TestCase("\0", "\\0")] [TestCase("\x01", "\\x01")] [TestCase("\x02", "\\x02")] From 76d7c22430ff46c79112da4af0e87944a3924b2f Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 11 Mar 2024 09:09:41 +0100 Subject: [PATCH 7/9] Update test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs --- .../Writers/SpecialCharacterStringExtensionsTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index 08578840..c98fab68 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -71,6 +71,7 @@ public void GetYamlCompatibleString_DateTimeDashString_WrappedWithQuotes() [Test] public void GetYamlCompatibleString_DateTimeISOString_NotWrappedWithQuotes() => this.Compose("2022-12-31T23:59:59Z", "2022-12-31T23:59:59Z"); + [Test] [TestCase("\0", "\\0")] [TestCase("\x01", "\\x01")] From d11fdfaf7525a9d501108f9c57395ef8e40e6de4 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Mon, 11 Mar 2024 20:48:12 -0400 Subject: [PATCH 8/9] Added more tests for date time formats --- .../SpecialCharacterStringExtensionsTests.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs index c98fab68..7cf8189e 100644 --- a/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Writers/SpecialCharacterStringExtensionsTests.cs @@ -63,15 +63,27 @@ 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'"); - + [Test] public void GetYamlCompatibleString_DateTimeDashString_WrappedWithQuotes() => this.Compose("2022-12-31 23:59:59", "'2022-12-31 23:59:59'"); - + [Test] public void GetYamlCompatibleString_DateTimeISOString_NotWrappedWithQuotes() => this.Compose("2022-12-31T23:59:59Z", "2022-12-31T23:59:59Z"); + [Test] + public void GetYamlCompatibleString_DateTimeCanonicalString_NotWrappedWithQuotes() + => this.Compose("2001-12-15T02:59:43.1Z", "2001-12-15T02:59:43.1Z"); + + [Test] + public void GetYamlCompatibleString_DateTimeSpacedString_NotWrappedWithQuotes() + => this.Compose("2001-12-14 21:59:43.10 -5", "2001-12-14 21:59:43.10 -5"); + + [Test] + public void GetYamlCompatibleString_DateString_NotWrappedWithQuotes() + => this.Compose("2002-12-14", "2002-12-14"); + [Test] [TestCase("\0", "\\0")] [TestCase("\x01", "\\x01")] From 6ad314cb53570b966f90c967a0cc83f6a8830e1f Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Mon, 11 Mar 2024 22:36:14 -0400 Subject: [PATCH 9/9] Fixed the logic for convert Json -> Yaml for datetime Updated the JsonHelper `GetScalerValue` to use the built int logic for serialization rather then the System.ConvertMethod --- .editorconfig | 3 + src/LEGO.AsyncAPI.Readers/JsonHelper.cs | 40 +++++- .../ParseNodes/MapNode.cs | 2 +- .../ParseNodes/ValueNode.cs | 2 +- src/LEGO.AsyncAPI.Readers/YamlConverter.cs | 33 ++++- .../LEGO.AsyncAPI.Tests.csproj | 3 +- .../Readers/YamlConverterTests.cs | 120 ++++++++++++++++++ test/LEGO.AsyncAPI.Tests/StringExtensions.cs | 13 ++ .../SpecialCharacterStringExtensionsTests.cs | 4 +- 9 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 test/LEGO.AsyncAPI.Tests/Readers/YamlConverterTests.cs 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()