From e791bf64da3d6fbb3bf2acc3a51ae8989c576236 Mon Sep 17 00:00:00 2001 From: costabello matthieu Date: Wed, 1 Oct 2025 10:25:58 -0400 Subject: [PATCH 01/12] add V3.2 properties on OpenApiTag --- .../Models/Interfaces/IOpenApiTag.cs | 15 ++ src/Microsoft.OpenApi/Models/OpenApiTag.cs | 77 +++++++--- .../Models/References/OpenApiTagReference.cs | 10 ++ .../Reader/V3/OpenApiTagDeserializer.cs | 27 +++- .../Reader/V31/OpenApiTagDeserializer.cs | 27 +++- .../Reader/V32/OpenApiTagDeserializer.cs | 25 +++- .../V31Tests/OpenApiTagDeserializerTests.cs | 83 +++++++++++ .../V32Tests/OpenApiTagDeserializerTests.cs | 127 +++++++++++++++++ .../V3Tests/OpenApiTagDeserializerTests.cs | 134 ++++++++++++++++++ .../PublicApi/PublicApi.approved.txt | 9 ++ 10 files changed, 511 insertions(+), 23 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiTagDeserializerTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagDeserializerTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiTag.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiTag.cs index 3f594c2e0..5391fc529 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiTag.cs @@ -15,4 +15,19 @@ public interface IOpenApiTag : IOpenApiReadOnlyExtensible, IOpenApiReadOnlyDescr /// Additional external documentation for this tag. /// public OpenApiExternalDocs? ExternalDocs { get; } + + /// + /// A short summary of the tag, used for display purposes. + /// + public string? Summary { get; } + + /// + /// The tag that this tag is nested under. + /// + public OpenApiTagReference? Parent { get; } + + /// + /// A machine-readable string to categorize what sort of tag it is. + /// + public string? Kind { get; } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index b74f68392..2c6e716d7 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -23,6 +23,15 @@ public class OpenApiTag : IOpenApiExtensible, IOpenApiTag, IOpenApiDescribedElem /// public IDictionary? Extensions { get; set; } + /// + public string? Summary { get; set; } + + /// + public OpenApiTagReference? Parent { get; set; } + + /// + public string? Kind { get; set; } + /// /// Parameterless constructor /// @@ -38,15 +47,30 @@ internal OpenApiTag(IOpenApiTag tag) Description = tag.Description ?? Description; ExternalDocs = tag.ExternalDocs != null ? new(tag.ExternalDocs) : null; Extensions = tag.Extensions != null ? new Dictionary(tag.Extensions) : null; + Summary = tag.Summary ?? Summary; + Parent = tag.Parent ?? Parent; + Kind = tag.Kind ?? Kind; } - + /// /// Serialize to Open Api v3.2 /// - public virtual void SerializeAsV32(IOpenApiWriter writer) + public virtual void SerializeAsV32(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_2, (writer, element) => element.SerializeAsV32(writer)); + + if (Summary != null) + writer.WriteProperty("summary", Summary); + if (Parent != null) + { + writer.WritePropertyName("parent"); + Parent.SerializeAsV32(writer); + } + if (Kind != null) + writer.WriteProperty("kind", Kind); + + writer.WriteEndObject(); } /// @@ -56,22 +80,48 @@ public virtual void SerializeAsV31(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer)); + + if (Summary != null) + writer.WriteProperty("x-oas-summary", Summary); + if (Parent != null) + { + writer.WritePropertyName("x-oas-parent"); + Parent.SerializeAsV31(writer); + } + if (Kind != null) + writer.WriteProperty("x-oas-kind", Kind); + + + writer.WriteEndObject(); } /// /// Serialize to Open Api v3.0 /// - public virtual void SerializeAsV3(IOpenApiWriter writer) + public virtual void SerializeAsV3(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); + + if (Summary != null) + writer.WriteProperty("x-oas-summary", Summary); + if (Parent != null) + { + writer.WritePropertyName("x-oas-parent"); + Parent.SerializeAsV31(writer); + } + if (Kind != null) + writer.WriteProperty("x-oas-kind", Kind); + + + writer.WriteEndObject(); } - internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, + internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, Action callback) { writer.WriteStartObject(); - + // name writer.WriteProperty(OpenApiConstants.Name, Name); @@ -83,8 +133,6 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio // extensions. writer.WriteExtensions(Extensions, version); - - writer.WriteEndObject(); } /// @@ -92,19 +140,8 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio /// public virtual void SerializeAsV2(IOpenApiWriter writer) { - writer.WriteStartObject(); - - // name - writer.WriteProperty(OpenApiConstants.Name, Name); - - // description - writer.WriteProperty(OpenApiConstants.Description, Description); - - // external docs - writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, e) => e.SerializeAsV2(w)); - - // extensions - writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); + SerializeInternal(writer, OpenApiSpecVersion.OpenApi2_0, + (writer, element) => element.SerializeAsV3(writer)); writer.WriteEndObject(); } diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index 92d1d1308..f1c4f5e60 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -58,6 +58,16 @@ public string? Description /// public string? Name { get => Target?.Name ?? Reference?.Id; } + + /// + public string? Summary => Target?.Summary; + + /// + public OpenApiTagReference? Parent => Target?.Parent; + + /// + public string? Kind => Target?.Kind; + /// public override IOpenApiTag CopyReferenceAsTargetElementWithOverrides(IOpenApiTag source) { diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiTagDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiTagDeserializer.cs index 3e6780c10..7e53533ed 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiTagDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiTagDeserializer.cs @@ -29,7 +29,32 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _tagPatternFields = new() { - {s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))} + { + s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), + (o, p, n, doc) => + { + if (p.Equals("x-oas-summary", StringComparison.OrdinalIgnoreCase)) + { + o.Summary = n.GetScalarValue(); + } + else if (p.Equals("x-oas-parent", StringComparison.OrdinalIgnoreCase)) + { + var tagName = n.GetScalarValue(); + if (tagName != null) + { + o.Parent = LoadTagByReference(tagName, doc); + } + } + else if (p.Equals("x-oas-kind", StringComparison.OrdinalIgnoreCase)) + { + o.Kind = n.GetScalarValue(); + } + else + { + o.AddExtension(p, LoadExtension(p, n)); + } + } + } }; public static OpenApiTag LoadTag(ParseNode n, OpenApiDocument hostDocument) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiTagDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiTagDeserializer.cs index 72ff86c3a..4504d4de8 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiTagDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiTagDeserializer.cs @@ -35,7 +35,32 @@ internal static partial class OpenApiV31Deserializer private static readonly PatternFieldMap _tagPatternFields = new() { - {s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))} + { + s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), + (o, p, n, doc) => + { + if (p.Equals("x-oas-summary", StringComparison.OrdinalIgnoreCase)) + { + o.Summary = n.GetScalarValue(); + } + else if (p.Equals("x-oas-parent", StringComparison.OrdinalIgnoreCase)) + { + var tagName = n.GetScalarValue(); + if (tagName != null) + { + o.Parent = LoadTagByReference(tagName, doc); + } + } + else if (p.Equals("x-oas-kind", StringComparison.OrdinalIgnoreCase)) + { + o.Kind = n.GetScalarValue(); + } + else + { + o.AddExtension(p, LoadExtension(p, n)); + } + } + } }; public static OpenApiTag LoadTag(ParseNode n, OpenApiDocument hostDocument) diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiTagDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiTagDeserializer.cs index 9a5468cc6..fab43129a 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiTagDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiTagDeserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -30,7 +30,30 @@ internal static partial class OpenApiV32Deserializer { o.ExternalDocs = LoadExternalDocs(n, t); } + }, + { + OpenApiConstants.Summary, (o, n, _) => + { + o.Summary = n.GetScalarValue(); + } + }, + { + "parent", (o, n, doc) => + { + var tagName = n.GetScalarValue(); + if (tagName != null) + { + o.Parent = LoadTagByReference(tagName, doc); + } + } + }, + { + "kind", (o, n, _) => + { + o.Kind = n.GetScalarValue(); + } } + }; private static readonly PatternFieldMap _tagPatternFields = new() diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiTagDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiTagDeserializerTests.cs new file mode 100644 index 000000000..1a4d432f3 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiTagDeserializerTests.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V31; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V31Tests; + +public class OpenApiTagDeserializerTests +{ + [Fact] + public void ShouldDeserializeTagWithNewV31Properties() + { + var json = + """ + { + "name": "store", + "description": "Store operations", + "x-oas-summary": "Operations related to the pet store", + "x-oas-parent": "pet", + "x-oas-kind": "operational", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com/" + }, + "x-custom-extension": "test-value" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "pet", + Description = "Parent tag for pets operations", + }); + + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV31Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("store", result.Name); + Assert.Equal("Store operations", result.Description); + Assert.Equal("Operations related to the pet store", result.Summary); + Assert.Equal("operational", result.Kind); + Assert.NotNull(result.Parent); + Assert.Equal("pet", result.Parent.Reference.Id); + Assert.NotNull(result.ExternalDocs); + Assert.Equal("Find more info here", result.ExternalDocs.Description); + Assert.Equal("https://example.com/", result.ExternalDocs.Url?.ToString()); + Assert.NotNull(result.Extensions); + Assert.Single(result.Extensions); + Assert.True(result.Extensions.ContainsKey("x-custom-extension")); + } + + [Fact] + public void ShouldDeserializeTagWithBasicProperties() + { + var json = + """ + { + "name": "pets", + "description": "Pet operations", + "x-oas-summary": "All operations for managing pets" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV31Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("pets", result.Name); + Assert.Equal("Pet operations", result.Description); + Assert.Equal("All operations for managing pets", result.Summary); + Assert.Null(result.Parent); + Assert.Null(result.Kind); + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagDeserializerTests.cs new file mode 100644 index 000000000..e3c8c4b8e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiTagDeserializerTests.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V32; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V32Tests; + +public class OpenApiTagDeserializerTests +{ + [Fact] + public void ShouldDeserializeTagWithNewV32Properties() + { + var json = + """ + { + "name": "store", + "description": "Store operations", + "summary": "Operations related to the pet store", + "parent": "pet", + "kind": "operational", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com/" + }, + "x-custom-extension": "test-value" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "pet", + Description = "Parent tag for pets operations", + }); + + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("store", result.Name); + Assert.Equal("Store operations", result.Description); + Assert.Equal("Operations related to the pet store", result.Summary); + Assert.Equal("operational", result.Kind); + Assert.NotNull(result.Parent); + Assert.Equal("pet", result.Parent.Reference.Id); + Assert.NotNull(result.ExternalDocs); + Assert.Equal("Find more info here", result.ExternalDocs.Description); + Assert.Equal("https://example.com/", result.ExternalDocs.Url?.ToString()); + Assert.NotNull(result.Extensions); + Assert.Single(result.Extensions); + Assert.True(result.Extensions.ContainsKey("x-custom-extension")); + } + + [Fact] + public void ShouldDeserializeTagWithBasicProperties() + { + var json = + """ + { + "name": "pets", + "description": "Pet operations", + "summary": "All operations for managing pets" + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("pets", result.Name); + Assert.Equal("Pet operations", result.Description); + Assert.Equal("All operations for managing pets", result.Summary); + Assert.Null(result.Parent); + Assert.Null(result.Kind); + } + + [Fact] + public void ShouldDeserializeAllNewPropertiesInV32Format() + { + var json = + """ + { + "name": "comprehensive", + "description": "A comprehensive tag example", + "summary": "Comprehensive tag for testing all V3.2 features", + "parent": "root", + "kind": "comprehensive-test", + "externalDocs": { + "description": "External documentation", + "url": "https://docs.example.com/" + } + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "root", + Description = "Root tag", + }); + + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV32Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("comprehensive", result.Name); + Assert.Equal("A comprehensive tag example", result.Description); + Assert.Equal("Comprehensive tag for testing all V3.2 features", result.Summary); + Assert.Equal("comprehensive-test", result.Kind); + Assert.NotNull(result.Parent); + Assert.Equal("root", result.Parent.Reference.Id); + Assert.False(result.Parent.UnresolvedReference); // Parent exists in document + Assert.NotNull(result.ExternalDocs); + Assert.Equal("External documentation", result.ExternalDocs.Description); + Assert.Equal("https://docs.example.com/", result.ExternalDocs.Url?.ToString()); + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs new file mode 100644 index 000000000..9f85f4ef6 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.Reader.V3; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V3Tests; + +public class OpenApiTagDeserializerTests +{ + [Fact] + public void ShouldDeserializeTagWithNewPropertiesAsExtensions() + { + var json = + """ + { + "name": "store", + "description": "Store operations", + "x-oas-summary": "Operations related to the pet store", + "x-oas-parent": "pet", + "x-oas-kind": "operational", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com/" + }, + "x-custom-extension": "test-value" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "pet", + Description = "Parent tag for pets operations", + }); + + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV3Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("store", result.Name); + Assert.Equal("Store operations", result.Description); + Assert.Equal("Operations related to the pet store", result.Summary); + Assert.Equal("operational", result.Kind); + Assert.NotNull(result.Parent); + Assert.Equal("pet", result.Parent.Reference.Id); + Assert.NotNull(result.ExternalDocs); + Assert.Equal("Find more info here", result.ExternalDocs.Description); + Assert.Equal("https://example.com/", result.ExternalDocs.Url?.ToString()); + Assert.NotNull(result.Extensions); + Assert.Single(result.Extensions); // Only the custom extension should remain + Assert.True(result.Extensions.ContainsKey("x-custom-extension")); + } + + [Fact] + public void ShouldIgnoreNativeV32PropertiesInV30() + { + var json = + """ + { + "name": "mixed", + "description": "Mixed format tag", + "summary": "Native summary should be ignored", + "parent": "should-be-ignored", + "kind": "should-be-ignored", + "x-oas-summary": "Extension summary should be used", + "x-oas-parent": "actual-parent", + "x-oas-kind": "actual-kind" + } + """; + + var hostDocument = new OpenApiDocument(); + hostDocument.Tags ??= new HashSet(); + hostDocument.Tags.Add(new OpenApiTag + { + Name = "actual-parent", + Description = "Actual parent tag", + }); + + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV3Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("mixed", result.Name); + Assert.Equal("Mixed format tag", result.Description); + // V3.0 should use extension values, not native properties + Assert.Equal("Extension summary should be used", result.Summary); + Assert.NotNull(result.Parent); + Assert.Equal("actual-parent", result.Parent.Reference.Id); + Assert.Equal("actual-kind", result.Kind); + // Native properties should appear as extensions since they're not recognized + if (result.Extensions != null) + { + Assert.True(result.Extensions.ContainsKey("summary")); + Assert.True(result.Extensions.ContainsKey("parent")); + Assert.True(result.Extensions.ContainsKey("kind")); + } + } + + [Fact] + public void ShouldDeserializeBasicTagWithoutNewProperties() + { + var json = + """ + { + "name": "basic", + "description": "Basic tag without new properties", + "externalDocs": { + "url": "https://example.com/" + } + } + """; + + var hostDocument = new OpenApiDocument(); + var jsonNode = JsonNode.Parse(json); + var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode); + + var result = OpenApiV3Deserializer.LoadTag(parseNode, hostDocument); + + Assert.NotNull(result); + Assert.Equal("basic", result.Name); + Assert.Equal("Basic tag without new properties", result.Description); + Assert.Null(result.Summary); + Assert.Null(result.Parent); + Assert.Null(result.Kind); + Assert.NotNull(result.ExternalDocs); + Assert.Equal("https://example.com/", result.ExternalDocs.Url?.ToString()); + } +} diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index a0deaacfb..06937c9e2 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -289,7 +289,10 @@ namespace Microsoft.OpenApi public interface IOpenApiTag : Microsoft.OpenApi.IOpenApiElement, Microsoft.OpenApi.IOpenApiReadOnlyDescribedElement, Microsoft.OpenApi.IOpenApiReadOnlyExtensible, Microsoft.OpenApi.IOpenApiReferenceable, Microsoft.OpenApi.IOpenApiSerializable, Microsoft.OpenApi.IShallowCopyable { Microsoft.OpenApi.OpenApiExternalDocs? ExternalDocs { get; } + string? Kind { get; } string? Name { get; } + Microsoft.OpenApi.OpenApiTagReference? Parent { get; } + string? Summary { get; } } public interface IOpenApiWriter { @@ -1397,7 +1400,10 @@ namespace Microsoft.OpenApi public string? Description { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; set; } public Microsoft.OpenApi.OpenApiExternalDocs? ExternalDocs { get; set; } + public string? Kind { get; set; } public string? Name { get; set; } + public Microsoft.OpenApi.OpenApiTagReference? Parent { get; set; } + public string? Summary { get; set; } public Microsoft.OpenApi.IOpenApiTag CreateShallowCopy() { } public virtual void SerializeAsV2(Microsoft.OpenApi.IOpenApiWriter writer) { } public virtual void SerializeAsV3(Microsoft.OpenApi.IOpenApiWriter writer) { } @@ -1410,7 +1416,10 @@ namespace Microsoft.OpenApi public string? Description { get; } public System.Collections.Generic.IDictionary? Extensions { get; } public Microsoft.OpenApi.OpenApiExternalDocs? ExternalDocs { get; } + public string? Kind { get; } public string? Name { get; } + public Microsoft.OpenApi.OpenApiTagReference? Parent { get; } + public string? Summary { get; } public override Microsoft.OpenApi.IOpenApiTag? Target { get; } protected override Microsoft.OpenApi.BaseOpenApiReference CopyReference(Microsoft.OpenApi.BaseOpenApiReference sourceReference) { } public override Microsoft.OpenApi.IOpenApiTag CopyReferenceAsTargetElementWithOverrides(Microsoft.OpenApi.IOpenApiTag source) { } From 77d2c23802e8deb8c2d217c2d12912fad043e07b Mon Sep 17 00:00:00 2001 From: costabello matthieu Date: Wed, 1 Oct 2025 10:31:35 -0400 Subject: [PATCH 02/12] fix serializeAsVX --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index 2c6e716d7..3b6465f03 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -108,7 +108,7 @@ public virtual void SerializeAsV3(IOpenApiWriter writer) if (Parent != null) { writer.WritePropertyName("x-oas-parent"); - Parent.SerializeAsV31(writer); + Parent.SerializeAsV3(writer); } if (Kind != null) writer.WriteProperty("x-oas-kind", Kind); @@ -141,7 +141,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio public virtual void SerializeAsV2(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi2_0, - (writer, element) => element.SerializeAsV3(writer)); + (writer, element) => element.SerializeAsV2(writer)); writer.WriteEndObject(); } From 4d780a6f55e93f086f00ecd8ce33b3d9eaf1d41f Mon Sep 17 00:00:00 2001 From: costabello matthieu Date: Wed, 1 Oct 2025 10:54:41 -0400 Subject: [PATCH 03/12] add models/openapitagtests --- ...sync_produceTerseOutput=False.verified.txt | 11 + ...Async_produceTerseOutput=True.verified.txt | 1 + ...sync_produceTerseOutput=False.verified.txt | 11 + ...Async_produceTerseOutput=True.verified.txt | 1 + .../Models/OpenApiTagTests.cs | 274 ++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=True.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..f6ba2c23b --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=False.verified.txt @@ -0,0 +1,11 @@ +{ + "name": "store", + "description": "Store operations", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com" + }, + "x-oas-summary": "Operations related to the pet store", + "x-oas-parent": "pet", + "x-oas-kind": "operational" +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..f2f4b736e --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV31JsonWorksAsync_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"name":"store","description":"Store operations","externalDocs":{"description":"Find more info here","url":"https://example.com"},"x-oas-summary":"Operations related to the pet store","x-oas-parent":"pet","x-oas-kind":"operational"} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..d4aec4618 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt @@ -0,0 +1,11 @@ +{ + "name": "store", + "description": "Store operations", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com" + }, + "summary": "Operations related to the pet store", + "parent": "pet", + "kind": "operational" +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..ffc36d5ac --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.SerializeTagWithV32PropertiesAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"name":"store","description":"Store operations","externalDocs":{"description":"Find more info here","url":"https://example.com"},"summary":"Operations related to the pet store","parent":"pet","kind":"operational"} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs index 2110e473b..5c96cc14a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs @@ -26,6 +26,16 @@ public class OpenApiTagTests } }; + public static readonly OpenApiTag TagWithV32Properties = new() + { + Name = "store", + Description = "Store operations", + Summary = "Operations related to the pet store", + Parent = new OpenApiTagReference("pet"), + Kind = "operational", + ExternalDocs = OpenApiExternalDocsTests.AdvanceExDocs + }; + public static IOpenApiTag ReferencedTag = new OpenApiTagReference("pet"); [Theory] @@ -310,5 +320,269 @@ public async Task SerializeReferencedTagAsV2YamlWorks() expected = expected.MakeLineBreaksEnvironmentNeutral(); Assert.Equal(expected, actual); } + + // Tests for V3.2 properties in V3.1 serialization (using extension format) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagWithV32PropertiesAsV31JsonWorksAsync(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = produceTerseOutput }); + + // Act + TagWithV32Properties.SerializeAsV31(writer); + await writer.FlushAsync(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + + [Fact] + public async Task SerializeTagWithV32PropertiesAsV31YamlWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputStringWriter); + var expected = @"name: store +description: Store operations +externalDocs: + description: Find more info here + url: https://example.com +x-oas-summary: Operations related to the pet store +x-oas-parent: pet +x-oas-kind: operational"; + + // Act + TagWithV32Properties.SerializeAsV31(writer); + await writer.FlushAsync(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + // Tests for V3.2 properties in V3.2 serialization (using native format) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeTagWithV32PropertiesAsV32JsonWorksAsync(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = produceTerseOutput }); + + // Act + TagWithV32Properties.SerializeAsV32(writer); + await writer.FlushAsync(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + + [Fact] + public async Task SerializeTagWithV32PropertiesAsV32YamlWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputStringWriter); + var expected = @"name: store +description: Store operations +externalDocs: + description: Find more info here + url: https://example.com +summary: Operations related to the pet store +parent: pet +kind: operational"; + + // Act + TagWithV32Properties.SerializeAsV32(writer); + await writer.FlushAsync(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + // Test for V3.0 serialization with V3.2 properties (should use extension format) + [Fact] + public async Task SerializeTagWithV32PropertiesAsV3YamlWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputStringWriter); + var expected = @"name: store +description: Store operations +externalDocs: + description: Find more info here + url: https://example.com +x-oas-summary: Operations related to the pet store +x-oas-parent: pet +x-oas-kind: operational"; + + // Act + TagWithV32Properties.SerializeAsV3(writer); + await writer.FlushAsync(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + // Tests for individual V3.2 properties + [Fact] + public void TagWithSummaryPropertyWorks() + { + // Arrange & Act + var tag = new OpenApiTag + { + Name = "test", + Summary = "Test summary" + }; + + // Assert + Assert.Equal("test", tag.Name); + Assert.Equal("Test summary", tag.Summary); + } + + [Fact] + public void TagWithParentPropertyWorks() + { + // Arrange & Act + var parentTag = new OpenApiTagReference("parent-tag"); + var tag = new OpenApiTag + { + Name = "child", + Parent = parentTag + }; + + // Assert + Assert.Equal("child", tag.Name); + Assert.NotNull(tag.Parent); + Assert.Equal(parentTag, tag.Parent); + } + + [Fact] + public void TagWithKindPropertyWorks() + { + // Arrange & Act + var tag = new OpenApiTag + { + Name = "test", + Kind = "category" + }; + + // Assert + Assert.Equal("test", tag.Name); + Assert.Equal("category", tag.Kind); + } + + [Fact] + public void TagWithAllV32PropertiesWorks() + { + // Arrange & Act + var parentTag = new OpenApiTagReference("parent-tag"); + var tag = new OpenApiTag + { + Name = "test", + Description = "Test description", + Summary = "Test summary", + Parent = parentTag, + Kind = "category" + }; + + // Assert + Assert.Equal("test", tag.Name); + Assert.Equal("Test description", tag.Description); + Assert.Equal("Test summary", tag.Summary); + Assert.Equal(parentTag, tag.Parent); + Assert.Equal("category", tag.Kind); + } + + [Fact] + public void CreateShallowCopyIncludesV32Properties() + { + // Arrange + var originalParent = new OpenApiTagReference("original-parent"); + var original = new OpenApiTag + { + Name = "original", + Description = "Original description", + Summary = "Original summary", + Parent = originalParent, + Kind = "original-kind" + }; + + // Act + var copy = original.CreateShallowCopy(); + + // Assert + Assert.Equal(original.Name, copy.Name); + Assert.Equal(original.Description, copy.Description); + Assert.Equal(original.Summary, copy.Summary); + Assert.Equal(original.Parent, copy.Parent); + Assert.Equal(original.Kind, copy.Kind); + } + + [Fact] + public void TagWithNullV32PropertiesWorks() + { + // Arrange & Act + var tag = new OpenApiTag + { + Name = "test", + Summary = null, + Parent = null, + Kind = null + }; + + // Assert + Assert.Equal("test", tag.Name); + Assert.Null(tag.Summary); + Assert.Null(tag.Parent); + Assert.Null(tag.Kind); + } + + [Theory] + [InlineData("summary")] + [InlineData("Summary")] + [InlineData("SUMMARY")] + public void TagSummaryPropertyIsCasePreserving(string summaryValue) + { + // Arrange & Act + var tag = new OpenApiTag + { + Name = "test", + Summary = summaryValue + }; + + // Assert + Assert.Equal(summaryValue, tag.Summary); + } + + [Theory] + [InlineData("category")] + [InlineData("operational")] + [InlineData("system")] + [InlineData("custom-kind")] + public void TagKindPropertyAcceptsVariousValues(string kindValue) + { + // Arrange & Act + var tag = new OpenApiTag + { + Name = "test", + Kind = kindValue + }; + + // Assert + Assert.Equal(kindValue, tag.Kind); + } } } From b7e91fe2652fb203c9b9ef7d9ec2ecef9c87924f Mon Sep 17 00:00:00 2001 From: costabello matthieu Date: Thu, 2 Oct 2025 16:05:37 -0400 Subject: [PATCH 04/12] retour PR --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 88 ++++++++++--------- .../Models/References/OpenApiTagReference.cs | 22 ++++- .../V3Tests/OpenApiTagDeserializerTests.cs | 8 -- .../Models/OpenApiTagTests.cs | 17 ---- 4 files changed, 69 insertions(+), 66 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index 3b6465f03..cb9159064 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -59,18 +59,6 @@ public virtual void SerializeAsV32(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_2, (writer, element) => element.SerializeAsV32(writer)); - - if (Summary != null) - writer.WriteProperty("summary", Summary); - if (Parent != null) - { - writer.WritePropertyName("parent"); - Parent.SerializeAsV32(writer); - } - if (Kind != null) - writer.WriteProperty("kind", Kind); - - writer.WriteEndObject(); } /// @@ -80,19 +68,6 @@ public virtual void SerializeAsV31(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_1, (writer, element) => element.SerializeAsV31(writer)); - - if (Summary != null) - writer.WriteProperty("x-oas-summary", Summary); - if (Parent != null) - { - writer.WritePropertyName("x-oas-parent"); - Parent.SerializeAsV31(writer); - } - if (Kind != null) - writer.WriteProperty("x-oas-kind", Kind); - - - writer.WriteEndObject(); } /// @@ -102,19 +77,6 @@ public virtual void SerializeAsV3(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer)); - - if (Summary != null) - writer.WriteProperty("x-oas-summary", Summary); - if (Parent != null) - { - writer.WritePropertyName("x-oas-parent"); - Parent.SerializeAsV3(writer); - } - if (Kind != null) - writer.WriteProperty("x-oas-kind", Kind); - - - writer.WriteEndObject(); } internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version, @@ -131,8 +93,56 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio // external docs writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback); + // summary - version specific handling + if (Summary != null) + { + if (version == OpenApiSpecVersion.OpenApi3_2) + { + writer.WriteProperty("summary", Summary); + } + else if (version == OpenApiSpecVersion.OpenApi3_1 || version == OpenApiSpecVersion.OpenApi3_0) + { + writer.WriteProperty("x-oas-summary", Summary); + } + } + + // parent - version specific handling + if (Parent != null) + { + if (version == OpenApiSpecVersion.OpenApi3_2) + { + writer.WritePropertyName("parent"); + Parent.SerializeAsV32(writer); + } + else if (version == OpenApiSpecVersion.OpenApi3_1) + { + writer.WritePropertyName("x-oas-parent"); + Parent.SerializeAsV31(writer); + } + else if (version == OpenApiSpecVersion.OpenApi3_0) + { + writer.WritePropertyName("x-oas-parent"); + Parent.SerializeAsV3(writer); + } + } + + // kind - version specific handling + if (Kind != null) + { + if (version == OpenApiSpecVersion.OpenApi3_2) + { + writer.WriteProperty("kind", Kind); + } + else if (version == OpenApiSpecVersion.OpenApi3_1 || version == OpenApiSpecVersion.OpenApi3_0) + { + writer.WriteProperty("x-oas-kind", Kind); + } + } + // extensions. writer.WriteExtensions(Extensions, version); + + writer.WriteEndObject(); } /// @@ -142,8 +152,6 @@ public virtual void SerializeAsV2(IOpenApiWriter writer) { SerializeInternal(writer, OpenApiSpecVersion.OpenApi2_0, (writer, element) => element.SerializeAsV2(writer)); - - writer.WriteEndObject(); } /// diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index f1c4f5e60..47907a9ff 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using System.Linq; @@ -63,7 +64,26 @@ public string? Description public string? Summary => Target?.Summary; /// - public OpenApiTagReference? Parent => Target?.Parent; + public OpenApiTagReference? Parent + { + get + { + // Prevent stack overflow by checking for self-reference + var targetParent = Target?.Parent; + if (targetParent == null) + return null; + + // Check if the target's parent reference is the same as this reference + if (ReferenceEquals(targetParent.Reference, this.Reference)) + return null; + + // Check if the target's parent name is the same as this tag's name + if (string.Equals(targetParent.Name, this.Name, StringComparison.OrdinalIgnoreCase)) + return null; + + return targetParent; + } + } /// public string? Kind => Target?.Kind; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs index 9f85f4ef6..bf36b25aa 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiTagDeserializerTests.cs @@ -88,18 +88,10 @@ public void ShouldIgnoreNativeV32PropertiesInV30() Assert.NotNull(result); Assert.Equal("mixed", result.Name); Assert.Equal("Mixed format tag", result.Description); - // V3.0 should use extension values, not native properties Assert.Equal("Extension summary should be used", result.Summary); Assert.NotNull(result.Parent); Assert.Equal("actual-parent", result.Parent.Reference.Id); Assert.Equal("actual-kind", result.Kind); - // Native properties should appear as extensions since they're not recognized - if (result.Extensions != null) - { - Assert.True(result.Extensions.ContainsKey("summary")); - Assert.True(result.Extensions.ContainsKey("parent")); - Assert.True(result.Extensions.ContainsKey("kind")); - } } [Fact] diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs index 5c96cc14a..bd424bcba 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs @@ -550,23 +550,6 @@ public void TagWithNullV32PropertiesWorks() Assert.Null(tag.Kind); } - [Theory] - [InlineData("summary")] - [InlineData("Summary")] - [InlineData("SUMMARY")] - public void TagSummaryPropertyIsCasePreserving(string summaryValue) - { - // Arrange & Act - var tag = new OpenApiTag - { - Name = "test", - Summary = summaryValue - }; - - // Assert - Assert.Equal(summaryValue, tag.Summary); - } - [Theory] [InlineData("category")] [InlineData("operational")] From 2837c4aac2d85fb73d5e9d0a56eed85bad78d6b2 Mon Sep 17 00:00:00 2001 From: kilifu Date: Thu, 2 Oct 2025 23:55:08 -0400 Subject: [PATCH 05/12] Update src/Microsoft.OpenApi/Models/OpenApiTag.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index cb9159064..cd40cafd1 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -96,7 +96,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio // summary - version specific handling if (Summary != null) { - if (version == OpenApiSpecVersion.OpenApi3_2) + if (version >= OpenApiSpecVersion.OpenApi3_2) { writer.WriteProperty("summary", Summary); } From 60e9b134d823906286b6fa7bd81c88f7d993639c Mon Sep 17 00:00:00 2001 From: kilifu Date: Thu, 2 Oct 2025 23:55:15 -0400 Subject: [PATCH 06/12] Update src/Microsoft.OpenApi/Models/OpenApiTag.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index cd40cafd1..0b45c0266 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -100,7 +100,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio { writer.WriteProperty("summary", Summary); } - else if (version == OpenApiSpecVersion.OpenApi3_1 || version == OpenApiSpecVersion.OpenApi3_0) + else if (version >= OpenApiSpecVersion.OpenApi3_0) { writer.WriteProperty("x-oas-summary", Summary); } From 6b99fc070a70e3cfe10e79814dd2d6cccb841ef7 Mon Sep 17 00:00:00 2001 From: kilifu Date: Thu, 2 Oct 2025 23:55:20 -0400 Subject: [PATCH 07/12] Update src/Microsoft.OpenApi/Models/OpenApiTag.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index 0b45c0266..16e60e0a6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -109,7 +109,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio // parent - version specific handling if (Parent != null) { - if (version == OpenApiSpecVersion.OpenApi3_2) + if (version >= OpenApiSpecVersion.OpenApi3_2) { writer.WritePropertyName("parent"); Parent.SerializeAsV32(writer); From c28e858998e6c0a32103de702121e6bf79299295 Mon Sep 17 00:00:00 2001 From: kilifu Date: Thu, 2 Oct 2025 23:55:28 -0400 Subject: [PATCH 08/12] Update src/Microsoft.OpenApi/Models/OpenApiTag.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index 16e60e0a6..1163d3507 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -129,7 +129,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio // kind - version specific handling if (Kind != null) { - if (version == OpenApiSpecVersion.OpenApi3_2) + if (version >= OpenApiSpecVersion.OpenApi3_2) { writer.WriteProperty("kind", Kind); } From 9ab88908548ee7be382a149b1d133be86e59624e Mon Sep 17 00:00:00 2001 From: kilifu Date: Thu, 2 Oct 2025 23:55:35 -0400 Subject: [PATCH 09/12] Update src/Microsoft.OpenApi/Models/OpenApiTag.cs Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index 1163d3507..a27ec135d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -133,7 +133,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio { writer.WriteProperty("kind", Kind); } - else if (version == OpenApiSpecVersion.OpenApi3_1 || version == OpenApiSpecVersion.OpenApi3_0) + else if (version >= OpenApiSpecVersion.OpenApi3_0) { writer.WriteProperty("x-oas-kind", Kind); } From b9976d489b69de1de40766f15834b7828cc4959a Mon Sep 17 00:00:00 2001 From: costabello matthieu Date: Fri, 3 Oct 2025 00:01:33 -0400 Subject: [PATCH 10/12] if the Target is a reference, and reference equals this, return null, otherwise, return the parent --- .../Models/References/OpenApiTagReference.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index 47907a9ff..519bc50c9 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -68,20 +68,13 @@ public OpenApiTagReference? Parent { get { - // Prevent stack overflow by checking for self-reference - var targetParent = Target?.Parent; - if (targetParent == null) + var target = Target; + if (target is OpenApiTagReference targetRef && ReferenceEquals(targetRef.Reference, this.Reference)) + { return null; - - // Check if the target's parent reference is the same as this reference - if (ReferenceEquals(targetParent.Reference, this.Reference)) - return null; - - // Check if the target's parent name is the same as this tag's name - if (string.Equals(targetParent.Name, this.Name, StringComparison.OrdinalIgnoreCase)) - return null; - - return targetParent; + } + + return target?.Parent; } } From f550dc8587c2272f6e8a63cf1540df7f5e348680 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 3 Oct 2025 08:16:47 -0400 Subject: [PATCH 11/12] chore: fixes reference comparison Signed-off-by: Vincent Biret --- .../Models/References/OpenApiTagReference.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs index 519bc50c9..78b92638c 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs @@ -68,13 +68,12 @@ public OpenApiTagReference? Parent { get { - var target = Target; - if (target is OpenApiTagReference targetRef && ReferenceEquals(targetRef.Reference, this.Reference)) + if (Target is OpenApiTagReference targetRef && (Reference.Id?.Equals(targetRef.Reference.Id, StringComparison.Ordinal) ?? false)) { return null; } - return target?.Parent; + return Target?.Parent; } } From 75b009dc7da74e4bd9ea942e982fa18e816d74b3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 3 Oct 2025 08:25:25 -0400 Subject: [PATCH 12/12] chore: refreshes benchmarks --- .../performance.Descriptions-report-github.md | 8 +-- .../performance.Descriptions-report.csv | 8 +-- .../performance.Descriptions-report.html | 10 ++-- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 60 +++++++++---------- .../performance.EmptyModels-report.csv | 58 +++++++++--------- .../performance.EmptyModels-report.html | 60 +++++++++---------- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 104 insertions(+), 104 deletions(-) diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index 0739f51c8..4f380baea 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -12,7 +12,7 @@ WarmupCount=3 ``` | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |------------- |---------------:|-------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 499.0 μs | 459.9 μs | 25.21 μs | 62.5000 | 11.7188 | - | 387.71 KB | -| PetStoreJson | 240.7 μs | 638.0 μs | 34.97 μs | 40.0391 | 8.7891 | - | 249.85 KB | -| GHESYaml | 1,055,965.3 μs | 311,428.8 μs | 17,070.46 μs | 66000.0000 | 22000.0000 | 4000.0000 | 384550.33 KB | -| GHESJson | 540,193.4 μs | 107,223.7 μs | 5,877.29 μs | 40000.0000 | 16000.0000 | 3000.0000 | 246021.04 KB | +| PetStoreYaml | 492.5 μs | 1,328.0 μs | 72.79 μs | 62.5000 | 11.7188 | - | 387.71 KB | +| PetStoreJson | 197.1 μs | 127.1 μs | 6.97 μs | 40.0391 | 8.7891 | - | 249.85 KB | +| GHESYaml | 1,134,963.6 μs | 681,713.6 μs | 37,367.02 μs | 66000.0000 | 22000.0000 | 4000.0000 | 384551.49 KB | +| GHESJson | 750,469.7 μs | 851,637.3 μs | 46,681.12 μs | 40000.0000 | 15000.0000 | 3000.0000 | 246021.99 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index c67569c3f..93d94a987 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,5 +1,5 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,499.0 μs,459.9 μs,25.21 μs,62.5000,11.7188,0.0000,387.71 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,240.7 μs,638.0 μs,34.97 μs,40.0391,8.7891,0.0000,249.85 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,055,965.3 μs","311,428.8 μs","17,070.46 μs",66000.0000,22000.0000,4000.0000,384550.33 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"540,193.4 μs","107,223.7 μs","5,877.29 μs",40000.0000,16000.0000,3000.0000,246021.04 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,492.5 μs,"1,328.0 μs",72.79 μs,62.5000,11.7188,0.0000,387.71 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,197.1 μs,127.1 μs,6.97 μs,40.0391,8.7891,0.0000,249.85 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,134,963.6 μs","681,713.6 μs","37,367.02 μs",66000.0000,22000.0000,4000.0000,384551.49 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"750,469.7 μs","851,637.3 μs","46,681.12 μs",40000.0000,15000.0000,3000.0000,246021.99 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 60e167966..2afbe8bad 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20251002-122727 +performance.Descriptions-20251003-081742