diff --git a/.github/workflows/release-please-gha.yml b/.github/workflows/release-please-gha.yml index 5750600d8..9c76a0d64 100644 --- a/.github/workflows/release-please-gha.yml +++ b/.github/workflows/release-please-gha.yml @@ -17,6 +17,9 @@ on: - support/v1 - support/v2 +permissions: + contents: read + jobs: release: runs-on: ubuntu-latest diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2d6304e18..4d2b02b5e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.7" + ".": "2.3.8" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fd74b4886..b8d1854c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [2.3.8](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.7...v2.3.8) (2025-10-27) + + +### Bug Fixes + +* an issue where numeric property names would be missing quotes in yaml conversion ([da43c98](https://github.com/microsoft/OpenAPI.NET/commit/da43c98cfd7938a2354bfe57f431aa4bd0407b66)) +* an issue where numeric property names would be missing quotes in yaml conversion ([234504c](https://github.com/microsoft/OpenAPI.NET/commit/234504c6a1a53be8630b1f2eda8640f04a92327d)) +* quote property names in yaml that match boolean values ([39a9f41](https://github.com/microsoft/OpenAPI.NET/commit/39a9f4112a123b9207504d4a840a9be553703555)) +* yaml blocks and line returns ([b053848](https://github.com/microsoft/OpenAPI.NET/commit/b05384872e9364aedf8d8fc24b36bab9824594c5)) +* yaml multi-line literals maintain their lines ([558a1ce](https://github.com/microsoft/OpenAPI.NET/commit/558a1ceafc22e6075470a8799582575c8c1e125d)) + ## [2.3.7](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.6...v2.3.7) (2025-10-24) diff --git a/Directory.Build.props b/Directory.Build.props index 17a280e02..ea4943006 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ https://github.com/Microsoft/OpenAPI.NET © Microsoft Corporation. All rights reserved. OpenAPI .NET - 2.3.7 + 2.3.8 diff --git a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj index c836548aa..1f2f5fb36 100644 --- a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj +++ b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj @@ -33,7 +33,7 @@ all - + diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index 260e4d971..5acf0c007 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -87,7 +87,10 @@ public static JsonObject ToJsonObject(this YamlMappingNode yaml) private static YamlMappingNode ToYamlMapping(this JsonObject obj) { - return new YamlMappingNode(obj.ToDictionary(x => (YamlNode)new YamlScalarNode(x.Key), x => x.Value!.ToYamlNode())); + return new YamlMappingNode(obj.ToDictionary(x => (YamlNode)new YamlScalarNode(x.Key) + { + Style = NeedsQuoting(x.Key) ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain + }, x => x.Value!.ToYamlNode())); } /// @@ -132,6 +135,11 @@ ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => (JsonValu }; } + private static bool NeedsQuoting(string value) => + decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out _) || + bool.TryParse(value, out _) || + YamlNullRepresentations.Contains(value); + private static YamlScalarNode ToYamlScalar(this JsonValue val) { // Try to get the underlying value based on its actual type @@ -142,13 +150,20 @@ private static YamlScalarNode ToYamlScalar(this JsonValue val) // For string values, we need to determine if they should be quoted in YAML // Strings that look like numbers, booleans, or null need to be quoted // to preserve their string type when round-tripping - var needsQuoting = decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out _) || - bool.TryParse(stringValue, out _) || - YamlNullRepresentations.Contains(stringValue); + var needsQuoting = NeedsQuoting(stringValue); + + var containsNewLine = stringValue.Contains('\n'); + + var style = (needsQuoting, containsNewLine) switch + { + (true, _) => ScalarStyle.DoubleQuoted, + (false, true) => ScalarStyle.Literal, + (false, false) => ScalarStyle.Plain + }; return new YamlScalarNode(stringValue) { - Style = needsQuoting ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain + Style = style }; } diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index a40febbbf..70073bfaa 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -1,4 +1,5 @@ -using Microsoft.OpenApi.YamlReader; +using Microsoft.OpenApi.Tests; +using Microsoft.OpenApi.YamlReader; using SharpYaml; using SharpYaml.Serialization; using System.IO; @@ -208,10 +209,7 @@ public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks() var yamlOutput = ConvertYamlNodeToString(yamlNode); // Convert back to JSON to verify round-tripping - var yamlStream = new YamlStream(); - using var sr = new StringReader(yamlOutput); - yamlStream.Load(sr); - var jsonBack = yamlStream.Documents[0].ToJsonNode(); + var jsonBack = ConvertYamlStringToJsonNode(yamlOutput); // Assert - line breaks should be preserved during round-trip var originalMultiline = json["multiline"]?.GetValue(); @@ -225,12 +223,105 @@ public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks() Assert.Contains("\n", roundTripDescription); } + [Fact] + public void NumericPropertyNamesShouldRemainStringsFromJson() + { + // Given + var yamlInput = + """ + "123": value1 + "456": value2 + """; + + // Given + var jsonNode = Assert.IsType(JsonNode.Parse(@"{ + ""123"": ""value1"", + ""456"": ""value2"" + }")); + + // When + var convertedBack = jsonNode.ToYamlNode(); + var convertedBackOutput = ConvertYamlNodeToString(convertedBack); + + // Then + Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void NumericPropertyNamesShouldRemainStringsFromYaml() + { + // Given + var yamlInput = + """ + "123": value1 + "456": value2 + """; + + var jsonNode = ConvertYamlStringToJsonNode(yamlInput); + + var convertedBack = jsonNode.ToYamlNode(); + var convertedBackOutput = ConvertYamlNodeToString(convertedBack); + // Then + Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void BooleanPropertyNamesShouldRemainStringsFromYaml() + { + // Given + var yamlInput = + """ + "true": value1 + "false": value2 + """; + + var jsonNode = ConvertYamlStringToJsonNode(yamlInput); + + var convertedBack = jsonNode.ToYamlNode(); + var convertedBackOutput = ConvertYamlNodeToString(convertedBack); + // Then + Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral()); + } + + [Fact] + public void LineBreaksShouldRoundTrip() + { + var yamlInput = + """ + python: |- + from openai import OpenAI + + client = OpenAI( + api_key="My API Key", + ) + page = client.beta.assistants.list() + page = page.data[0] + print(page.id) + """; + // When + var jsonNode = ConvertYamlStringToJsonNode(yamlInput); + var convertedBack = jsonNode.ToYamlNode(); + var convertedBackOutput = ConvertYamlNodeToString(convertedBack); + + // Then + Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral()); + } + + private static JsonNode ConvertYamlStringToJsonNode(string yamlInput) + { + var yamlDocument = new YamlStream(); + using var sr = new StringReader(yamlInput); + yamlDocument.Load(sr); + var yamlRoot = yamlDocument.Documents[0].RootNode; + return yamlRoot.ToJsonNode(); + } + private static string ConvertYamlNodeToString(YamlNode yamlNode) { using var ms = new MemoryStream(); var yamlStream = new YamlStream(new YamlDocument(yamlNode)); var writer = new StreamWriter(ms); - yamlStream.Save(writer); + yamlStream.Save(writer, isLastDocumentEndImplicit: true); writer.Flush(); ms.Seek(0, SeekOrigin.Begin); var reader = new StreamReader(ms);