diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 1756f9dde..47ef07d53 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -245,6 +245,11 @@ public static class OpenApiConstants /// public const string Wrapped = "wrapped"; + /// + /// Field: NodeType + /// + public const string NodeType = "nodeType"; + /// /// Field: In /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiXml.cs b/src/Microsoft.OpenApi/Models/OpenApiXml.cs index 62c1b64de..82d7c2180 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiXml.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiXml.cs @@ -30,13 +30,40 @@ public class OpenApiXml : IOpenApiSerializable, IOpenApiExtensible /// Declares whether the property definition translates to an attribute instead of an element. /// Default value is false. /// - public bool Attribute { get; set; } + [Obsolete("Use NodeType property instead. This property will be removed in a future version.")] + internal bool Attribute + { + get + { + return NodeType == OpenApiXmlNodeType.Attribute; + } + set + { + NodeType = value ? OpenApiXmlNodeType.Attribute : OpenApiXmlNodeType.None; + } + } /// /// Signifies whether the array is wrapped. /// Default value is false. /// - public bool Wrapped { get; set; } + [Obsolete("Use NodeType property instead. This property will be removed in a future version.")] + internal bool Wrapped + { + get + { + return NodeType == OpenApiXmlNodeType.Element; + } + set + { + NodeType = value ? OpenApiXmlNodeType.Element : OpenApiXmlNodeType.None; + } + } + + /// + /// The node type of the XML representation. + /// + public OpenApiXmlNodeType? NodeType { get; set; } /// /// Specification Extensions. @@ -56,8 +83,7 @@ public OpenApiXml(OpenApiXml xml) Name = xml?.Name ?? Name; Namespace = xml?.Namespace ?? Namespace; Prefix = xml?.Prefix ?? Prefix; - Attribute = xml?.Attribute ?? Attribute; - Wrapped = xml?.Wrapped ?? Wrapped; + NodeType = xml?.NodeType ?? NodeType; Extensions = xml?.Extensions != null ? new Dictionary(xml.Extensions) : null; } @@ -108,11 +134,25 @@ private void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) // prefix writer.WriteProperty(OpenApiConstants.Prefix, Prefix); - // attribute - writer.WriteProperty(OpenApiConstants.Attribute, Attribute, false); - - // wrapped - writer.WriteProperty(OpenApiConstants.Wrapped, Wrapped, false); + // For OpenAPI 3.2.0 and above, serialize nodeType + if (specVersion >= OpenApiSpecVersion.OpenApi3_2) + { + if (NodeType.HasValue) + { + writer.WriteProperty(OpenApiConstants.NodeType, NodeType.Value.GetDisplayName()); + } + } + else + { + // For OpenAPI 3.1.0 and below, serialize attribute and wrapped + // Use backing fields if they were set via obsolete properties, + // otherwise derive from NodeType if set + var attribute = NodeType.HasValue && NodeType == OpenApiXmlNodeType.Attribute; + var wrapped = NodeType.HasValue && NodeType == OpenApiXmlNodeType.Element; + + writer.WriteProperty(OpenApiConstants.Attribute, attribute, false); + writer.WriteProperty(OpenApiConstants.Wrapped, wrapped, false); + } // extensions writer.WriteExtensions(Extensions, specVersion); diff --git a/src/Microsoft.OpenApi/Models/OpenApiXmlNodeType.cs b/src/Microsoft.OpenApi/Models/OpenApiXmlNodeType.cs new file mode 100644 index 000000000..11b8f85db --- /dev/null +++ b/src/Microsoft.OpenApi/Models/OpenApiXmlNodeType.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.OpenApi +{ + /// + /// The type of the XML node + /// + public enum OpenApiXmlNodeType + { + /// + /// Element node type + /// + [Display("element")] Element, + + /// + /// Attribute node type + /// + [Display("attribute")] Attribute, + + /// + /// Text node type + /// + [Display("text")] Text, + + /// + /// CDATA node type + /// + [Display("cdata")] Cdata, + + /// + /// None node type + /// + [Display("none")] None + } +} diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiXmlDeserializer.cs index 2a8c395fe..c5034017d 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiXmlDeserializer.cs @@ -42,7 +42,9 @@ internal static partial class OpenApiV2Deserializer var attribute = n.GetScalarValue(); if (attribute is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Attribute = bool.Parse(attribute); +#pragma warning restore CS0618 // Type or member is obsolete } } }, @@ -53,7 +55,9 @@ internal static partial class OpenApiV2Deserializer var wrapped = n.GetScalarValue(); if (wrapped is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Wrapped = bool.Parse(wrapped); +#pragma warning restore CS0618 // Type or member is obsolete } } }, diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiXmlDeserializer.cs index 2a02d52bb..677cbfc0f 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiXmlDeserializer.cs @@ -39,7 +39,9 @@ internal static partial class OpenApiV3Deserializer var attribute = n.GetScalarValue(); if (attribute is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Attribute = bool.Parse(attribute); +#pragma warning restore CS0618 // Type or member is obsolete } } }, @@ -50,7 +52,9 @@ internal static partial class OpenApiV3Deserializer var wrapped = n.GetScalarValue(); if (wrapped is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Wrapped = bool.Parse(wrapped); +#pragma warning restore CS0618 // Type or member is obsolete } } }, diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiXmlDeserializer.cs index 77d1c0163..f43018714 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiXmlDeserializer.cs @@ -41,7 +41,9 @@ internal static partial class OpenApiV31Deserializer var attribute = n.GetScalarValue(); if (attribute is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Attribute = bool.Parse(attribute); +#pragma warning restore CS0618 // Type or member is obsolete } } }, @@ -52,7 +54,9 @@ internal static partial class OpenApiV31Deserializer var wrapped = n.GetScalarValue(); if (wrapped is not null) { +#pragma warning disable CS0618 // Type or member is obsolete o.Wrapped = bool.Parse(wrapped); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiXmlDeserializer.cs index a0169629a..6f3de6145 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiXmlDeserializer.cs @@ -35,25 +35,14 @@ internal static partial class OpenApiV32Deserializer (o, n, _) => o.Prefix = n.GetScalarValue() }, { - "attribute", + "nodeType", (o, n, _) => { - var attribute = n.GetScalarValue(); - if (attribute is not null) + if (!n.GetScalarValue().TryGetEnumFromDisplayName(n.Context, out var nodeType)) { - o.Attribute = bool.Parse(attribute); - } - } - }, - { - "wrapped", - (o, n, _) => - { - var wrapped = n.GetScalarValue(); - if (wrapped is not null) - { - o.Wrapped = bool.Parse(wrapped); + return; } + o.NodeType = nodeType; } } }; diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs index 259400c90..0c9c78e6b 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs @@ -21,6 +21,7 @@ public async Task ParseBasicXmlShouldSucceed() var xml = await OpenApiModelFactory.LoadAsync(Resources.GetStream(Path.Combine(SampleFolderPath, "basicXml.yaml")), OpenApiSpecVersion.OpenApi3_0, new(), settings: SettingsFixture.ReaderSettings); // Assert +#pragma warning disable CS0618 // Type or member is obsolete Assert.Equivalent( new OpenApiXml { @@ -29,6 +30,7 @@ public async Task ParseBasicXmlShouldSucceed() Prefix = "samplePrefix", Wrapped = true }, xml); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs index 5424e1eb9..6426c0237 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Xunit; @@ -10,6 +11,7 @@ namespace Microsoft.OpenApi.Tests.Models [Collection("DefaultSettings")] public class OpenApiXmlTests { +#pragma warning disable CS0618 // Type or member is obsolete public static OpenApiXml AdvancedXml = new() { Name = "animal", @@ -22,9 +24,28 @@ public class OpenApiXmlTests {"x-xml-extension", new JsonNodeExtension(7)} } }; +#pragma warning restore CS0618 // Type or member is obsolete public static OpenApiXml BasicXml = new(); + public static OpenApiXml XmlWithNodeType = new() + { + Name = "pet", + Namespace = new("http://example.com/schema"), + Prefix = "ex", + NodeType = OpenApiXmlNodeType.Element, + Extensions = new Dictionary() + { + {"x-custom", new JsonNodeExtension("test")} + } + }; + + public static OpenApiXml XmlWithAttributeNodeType = new() + { + Name = "id", + NodeType = OpenApiXmlNodeType.Attribute + }; + [Theory] [InlineData(OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Json)] [InlineData(OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Json)] @@ -55,7 +76,6 @@ public async Task SerializeAdvancedXmlAsJsonWorks(OpenApiSpecVersion version) "namespace": "http://swagger.io/schema/sample", "prefix": "sample", "attribute": true, - "wrapped": true, "x-xml-extension": 7 } """; @@ -64,9 +84,7 @@ public async Task SerializeAdvancedXmlAsJsonWorks(OpenApiSpecVersion version) var actual = await AdvancedXml.SerializeAsJsonAsync(version); // Assert - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - Assert.Equal(expected, actual); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected))); } [Theory] @@ -81,7 +99,6 @@ public async Task SerializeAdvancedXmlAsYamlWorks(OpenApiSpecVersion version) namespace: http://swagger.io/schema/sample prefix: sample attribute: true - wrapped: true x-xml-extension: 7 """; @@ -93,5 +110,109 @@ public async Task SerializeAdvancedXmlAsYamlWorks(OpenApiSpecVersion version) expected = expected.MakeLineBreaksEnvironmentNeutral(); Assert.Equal(expected, actual); } + + [Fact] + public async Task SerializeXmlWithNodeTypeAsJsonV32Works() + { + // Arrange + var expected = + """ + { + "name": "pet", + "namespace": "http://example.com/schema", + "prefix": "ex", + "nodeType": "element", + "x-custom": "test" + } + """; + + // Act + var actual = await XmlWithNodeType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected))); + } + + [Fact] + public async Task SerializeXmlWithNodeTypeAsYamlV32Works() + { + // Arrange + var expected = + """ + name: pet + namespace: http://example.com/schema + prefix: ex + nodeType: element + x-custom: test + """; + + // Act + var actual = await XmlWithNodeType.SerializeAsYamlAsync(OpenApiSpecVersion.OpenApi3_2); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task SerializeXmlWithAttributeNodeTypeAsJsonV32Works() + { + // Arrange + var expected = + """ + { + "name": "id", + "nodeType": "attribute" + } + """; + + // Act + var actual = await XmlWithAttributeNodeType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected))); + } + + [Fact] + public async Task SerializeXmlWithNodeTypeAsJsonV31DoesNotSerializeNodeType() + { + // Arrange - In v3.1, nodeType should not be serialized, instead attribute/wrapped should be + var expected = + """ + { + "name": "pet", + "namespace": "http://example.com/schema", + "prefix": "ex", + "wrapped": true, + "x-custom": "test" + } + """; + + // Act + var actual = await XmlWithNodeType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected))); + } + + [Fact] + public async Task SerializeXmlWithAttributeNodeTypeAsJsonV31DoesNotSerializeNodeType() + { + // Arrange - In v3.1, nodeType should not be serialized, instead attribute/wrapped should be + var expected = + """ + { + "name": "id", + "attribute": true + } + """; + + // Act + var actual = await XmlWithAttributeNodeType.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1); + + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected))); + } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index fc6560475..d96b0d82f 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -508,6 +508,7 @@ namespace Microsoft.OpenApi public const string MultipleOf = "multipleOf"; public const string Name = "name"; public const string Namespace = "namespace"; + public const string NodeType = "nodeType"; public const string Not = "not"; public const string Null = "null"; public const string Nullable = "nullable"; @@ -1696,17 +1697,29 @@ namespace Microsoft.OpenApi { public OpenApiXml() { } public OpenApiXml(Microsoft.OpenApi.OpenApiXml xml) { } - public bool Attribute { get; set; } public System.Collections.Generic.IDictionary? Extensions { get; set; } public string? Name { get; set; } public System.Uri? Namespace { get; set; } + public Microsoft.OpenApi.OpenApiXmlNodeType? NodeType { get; set; } public string? Prefix { get; set; } - public bool Wrapped { get; set; } public virtual void SerializeAsV2(Microsoft.OpenApi.IOpenApiWriter writer) { } public virtual void SerializeAsV3(Microsoft.OpenApi.IOpenApiWriter writer) { } public virtual void SerializeAsV31(Microsoft.OpenApi.IOpenApiWriter writer) { } public virtual void SerializeAsV32(Microsoft.OpenApi.IOpenApiWriter writer) { } } + public enum OpenApiXmlNodeType + { + [Microsoft.OpenApi.Display("element")] + Element = 0, + [Microsoft.OpenApi.Display("attribute")] + Attribute = 1, + [Microsoft.OpenApi.Display("text")] + Text = 2, + [Microsoft.OpenApi.Display("cdata")] + Cdata = 3, + [Microsoft.OpenApi.Display("none")] + None = 4, + } public class OpenApiYamlWriter : Microsoft.OpenApi.OpenApiWriterBase { public OpenApiYamlWriter(System.IO.TextWriter textWriter) { }