Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,19 @@ public interface IOpenApiTag : IOpenApiReadOnlyExtensible, IOpenApiReadOnlyDescr
/// Additional external documentation for this tag.
/// </summary>
public OpenApiExternalDocs? ExternalDocs { get; }

/// <summary>
/// A short summary of the tag, used for display purposes.
/// </summary>
public string? Summary { get; }

/// <summary>
/// The tag that this tag is nested under.
/// </summary>
public OpenApiTagReference? Parent { get; }

/// <summary>
/// A machine-readable string to categorize what sort of tag it is.
/// </summary>
public string? Kind { get; }
}
77 changes: 57 additions & 20 deletions src/Microsoft.OpenApi/Models/OpenApiTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ public class OpenApiTag : IOpenApiExtensible, IOpenApiTag, IOpenApiDescribedElem
/// <inheritdoc/>
public IDictionary<string, IOpenApiExtension>? Extensions { get; set; }

/// <inheritdoc/>
public string? Summary { get; set; }

/// <inheritdoc/>
public OpenApiTagReference? Parent { get; set; }

/// <inheritdoc/>
public string? Kind { get; set; }

/// <summary>
/// Parameterless constructor
/// </summary>
Expand All @@ -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<string, IOpenApiExtension>(tag.Extensions) : null;
Summary = tag.Summary ?? Summary;
Parent = tag.Parent ?? Parent;
Kind = tag.Kind ?? Kind;
}

/// <summary>
/// Serialize <see cref="OpenApiTag"/> to Open Api v3.2
/// </summary>
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();
}

/// <summary>
Expand All @@ -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();
}

/// <summary>
/// Serialize <see cref="OpenApiTag"/> to Open Api v3.0
/// </summary>
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.SerializeAsV3(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<IOpenApiWriter, IOpenApiSerializable> callback)
{
writer.WriteStartObject();

// name
writer.WriteProperty(OpenApiConstants.Name, Name);

Expand All @@ -83,28 +133,15 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio

// extensions.
writer.WriteExtensions(Extensions, version);

writer.WriteEndObject();
}

/// <summary>
/// Serialize <see cref="OpenApiTag"/> to Open Api v2.0
/// </summary>
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.SerializeAsV2(writer));

writer.WriteEndObject();
}
Expand Down
10 changes: 10 additions & 0 deletions src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public string? Description

/// <inheritdoc/>
public string? Name { get => Target?.Name ?? Reference?.Id; }

/// <inheritdoc/>
public string? Summary => Target?.Summary;

/// <inheritdoc/>
public OpenApiTagReference? Parent => Target?.Parent;

/// <inheritdoc/>
public string? Kind => Target?.Kind;

/// <inheritdoc/>
public override IOpenApiTag CopyReferenceAsTargetElementWithOverrides(IOpenApiTag source)
{
Expand Down
27 changes: 26 additions & 1 deletion src/Microsoft.OpenApi/Reader/V3/OpenApiTagDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,32 @@ internal static partial class OpenApiV3Deserializer

private static readonly PatternFieldMap<OpenApiTag> _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)
Expand Down
27 changes: 26 additions & 1 deletion src/Microsoft.OpenApi/Reader/V31/OpenApiTagDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,32 @@ internal static partial class OpenApiV31Deserializer

private static readonly PatternFieldMap<OpenApiTag> _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)
Expand Down
25 changes: 24 additions & 1 deletion src/Microsoft.OpenApi/Reader/V32/OpenApiTagDeserializer.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<OpenApiTag> _tagPatternFields = new()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OpenApiTag>();
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);
}
}
Loading
Loading