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
26 changes: 24 additions & 2 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,32 @@ public void RegisterComponents()
public IList<OpenApiSecurityRequirement>? SecurityRequirements { get; set; } =
new List<OpenApiSecurityRequirement>();

private HashSet<OpenApiTag>? _tags;
/// <summary>
/// A list of tags used by the specification with additional metadata.
/// </summary>
public IList<OpenApiTag>? Tags { get; set; } = new List<OpenApiTag>();
public ISet<OpenApiTag>? Tags
{
get
{
return _tags;
}
set
{
if (value is null)
{
return;
}
if (value is HashSet<OpenApiTag> tags && tags.Comparer is OpenApiTagComparer)
{
_tags = tags;
}
else
{
_tags = new HashSet<OpenApiTag>(value, OpenApiTagComparer.Instance);
}
Comment thread Fixed
}
}

/// <summary>
/// Additional external documentation.
Expand Down Expand Up @@ -123,7 +145,7 @@ public OpenApiDocument(OpenApiDocument? document)
Webhooks = document?.Webhooks != null ? new Dictionary<string, IOpenApiPathItem>(document.Webhooks) : null;
Components = document?.Components != null ? new(document?.Components) : null;
SecurityRequirements = document?.SecurityRequirements != null ? new List<OpenApiSecurityRequirement>(document.SecurityRequirements) : null;
Tags = document?.Tags != null ? new List<OpenApiTag>(document.Tags) : null;
Tags = document?.Tags != null ? new HashSet<OpenApiTag>(document.Tags, OpenApiTagComparer.Instance) : null;
ExternalDocs = document?.ExternalDocs != null ? new(document?.ExternalDocs) : null;
Extensions = document?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(document.Extensions) : null;
Annotations = document?.Annotations != null ? new Dictionary<string, object>(document.Annotations) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public override OpenApiTag Target
{
get
{
return Reference.HostDocument?.Tags.FirstOrDefault(t => StringComparer.Ordinal.Equals(t.Name, Reference.Id));
return Reference.HostDocument?.Tags.FirstOrDefault(t => OpenApiTagComparer.StringComparer.Equals(t.Name, Reference.Id));
}
}

Expand Down
47 changes: 47 additions & 0 deletions src/Microsoft.OpenApi/OpenApiTagComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Models;

namespace Microsoft.OpenApi;

#nullable enable
/// <summary>
/// This comparer is used to maintain a globally unique list of tags encountered
/// in a particular OpenAPI document.
/// </summary>
internal sealed class OpenApiTagComparer : IEqualityComparer<OpenApiTag>
{
private static readonly Lazy<OpenApiTagComparer> _lazyInstance = new(() => new OpenApiTagComparer());
/// <summary>
/// Default instance for the comparer.
/// </summary>
internal static OpenApiTagComparer Instance { get => _lazyInstance.Value; }

/// <inheritdoc/>
public bool Equals(OpenApiTag? x, OpenApiTag? y)
{
if (x is null && y is null)
{
return true;
}
if (x is null || y is null)
{
return false;
}
if (ReferenceEquals(x, y))
{
return true;
}
return StringComparer.Equals(x.Name, y.Name);
}

// Tag comparisons are case-sensitive by default. Although the OpenAPI specification
// only outlines case sensitivity for property names, we extend this principle to
// property values for tag names as well.
// See https://spec.openapis.org/oas/v3.1.0#format.
internal static readonly StringComparer StringComparer = StringComparer.Ordinal;

/// <inheritdoc/>
public int GetHashCode(OpenApiTag obj) => obj?.Name is null ? 0 : StringComparer.GetHashCode(obj.Name);
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ internal static partial class OpenApiV2Deserializer
}
},
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)},
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o)},
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader.ParseNodes;
Expand All @@ -26,7 +27,7 @@ internal static partial class OpenApiV3Deserializer
{"servers", (o, n, _) => o.Servers = n.CreateList(LoadServer, o)},
{"paths", (o, n, _) => o.Paths = LoadPaths(n, o)},
{"components", (o, n, _) => o.Components = LoadComponents(n, o)},
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o) },
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)},
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader.ParseNodes;
Expand All @@ -24,7 +25,7 @@ internal static partial class OpenApiV31Deserializer
{"paths", (o, n, _) => o.Paths = LoadPaths(n, o)},
{"webhooks", (o, n, _) => o.Webhooks = n.CreateMap(LoadPathItem, o)},
{"components", (o, n, _) => o.Components = LoadComponents(n, o)},
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o) },
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)},
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)}
};
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ public virtual void Visit(IOpenApiExample example)
/// <summary>
/// Visits list of <see cref="OpenApiTag"/>
/// </summary>
public virtual void Visit(IList<OpenApiTag> openApiTags)
public virtual void Visit(ISet<OpenApiTag> openApiTags)
{
}

Expand Down
10 changes: 6 additions & 4 deletions src/Microsoft.OpenApi/Services/OpenApiWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
Expand Down Expand Up @@ -60,7 +61,7 @@ public void Walk(OpenApiDocument doc)
/// <summary>
/// Visits list of <see cref="OpenApiTag"/> and child objects
/// </summary>
internal void Walk(IList<OpenApiTag> tags)
internal void Walk(ISet<OpenApiTag> tags)
{
if (tags == null)
{
Expand All @@ -72,9 +73,10 @@ internal void Walk(IList<OpenApiTag> tags)
// Visit tags
if (tags != null)
{
for (var i = 0; i < tags.Count; i++)
var tagsAsArray = tags.ToArray();
for (var i = 0; i < tagsAsArray.Length; i++)
{
Walk(i.ToString(), () => Walk(tags[i]));
Walk(i.ToString(), () => Walk(tagsAsArray[i]));
}
}
}
Expand Down Expand Up @@ -1213,7 +1215,7 @@ internal void Walk(IOpenApiElement element)
case OpenApiServer e: Walk(e); break;
case OpenApiServerVariable e: Walk(e); break;
case OpenApiTag e: Walk(e); break;
case IList<OpenApiTag> e: Walk(e); break;
case ISet<OpenApiTag> e: Walk(e); break;
case IOpenApiExtensible e: Walk(e); break;
case IOpenApiExtension e: Walk(e); break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ public static OpenApiDocument CreateOpenApiDocument()
}
}
},
Tags = new List<OpenApiTag>
Tags = new HashSet<OpenApiTag>
{
new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ public async Task ParseModifiedPetStoreDocumentWithTagAndSecurityShouldSucceed()
}
},
Components = components,
Tags = new List<OpenApiTag>
Tags = new HashSet<OpenApiTag>
{
new OpenApiTag
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand Down Expand Up @@ -35,7 +36,7 @@ public async Task ParseOperationWithParameterWithNoLocationShouldSucceed()
{
var openApiDocument = new OpenApiDocument
{
Tags = { new OpenApiTag() { Name = "user" } }
Tags = new HashSet<OpenApiTag> { new() { Name = "user" } }
};
// Act
var operation = await OpenApiModelFactory.LoadAsync<OpenApiOperation>(Path.Combine(SampleFolderPath, "operationWithParameterWithNoLocation.json"), OpenApiSpecVersion.OpenApi3_0, openApiDocument);
Expand Down
39 changes: 38 additions & 1 deletion test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2078,7 +2078,7 @@ public async Task SerializeDocumentTagsWithMultipleExtensionsWorks()
Version = "1.0.0"
},
Paths = new OpenApiPaths(),
Tags = new List<OpenApiTag>
Tags = new HashSet<OpenApiTag>
{
new OpenApiTag
{
Expand All @@ -2102,5 +2102,42 @@ public async Task SerializeDocumentTagsWithMultipleExtensionsWorks()
var actual = await doc.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
}
[Fact]
public void DeduplicatesTags()
{
var document = new OpenApiDocument
{
Tags = new HashSet<OpenApiTag>
{
new OpenApiTag
{
Name = "tag1",
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-tag1"] = new OpenApiAny("tag1")
}
},
new OpenApiTag
{
Name = "tag2",
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-tag2"] = new OpenApiAny("tag2")
}
},
new OpenApiTag
{
Name = "tag1",
Extensions = new Dictionary<string, IOpenApiExtension>
{
["x-tag1"] = new OpenApiAny("tag1")
}
}
}
};
Assert.Equal(2, document.Tags.Count);
Assert.Contains(document.Tags, t => t.Name == "tag1");
Assert.Contains(document.Tags, t => t.Name == "tag2");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public class OpenApiOperationTests
{
Tags = new List<OpenApiTagReference>
{
new OpenApiTagReference("tagId1", new OpenApiDocument{ Tags = new List<OpenApiTag>() { new OpenApiTag{Name = "tagId1"}} })
new OpenApiTagReference("tagId1", new OpenApiDocument{ Tags = new HashSet<OpenApiTag>() { new OpenApiTag{Name = "tagId1"}} })
},
Summary = "summary1",
Description = "operationDescription",
Expand Down
40 changes: 40 additions & 0 deletions test/Microsoft.OpenApi.Tests/OpenApiTagComparerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.OpenApi.Models;
using Xunit;

namespace Microsoft.OpenApi.Tests;

public class OpenApiTagComparerTests
{
private readonly OpenApiTagComparer _comparer = OpenApiTagComparer.Instance;
[Fact]
public void Defensive()
{
Assert.NotNull(_comparer);

Assert.True(_comparer.Equals(null, null));
Assert.False(_comparer.Equals(null, new OpenApiTag()));
Assert.Equal(0, _comparer.GetHashCode(null));
Assert.Equal(0, _comparer.GetHashCode(new OpenApiTag()));
}
[Fact]
public void SameNamesAreEqual()
{
var openApiTag1 = new OpenApiTag { Name = "tag" };
var openApiTag2 = new OpenApiTag { Name = "tag" };
Assert.True(_comparer.Equals(openApiTag1, openApiTag2));
}
[Fact]
public void SameInstanceAreEqual()
{
var openApiTag = new OpenApiTag { Name = "tag" };
Assert.True(_comparer.Equals(openApiTag, openApiTag));
}

[Fact]
public void DifferentCasingAreNotEquals()
{
var openApiTag1 = new OpenApiTag { Name = "tag" };
var openApiTag2 = new OpenApiTag { Name = "TAG" };
Assert.False(_comparer.Equals(openApiTag1, openApiTag2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ namespace Microsoft.OpenApi.Models
public Microsoft.OpenApi.Models.OpenApiPaths Paths { get; set; }
public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiSecurityRequirement>? SecurityRequirements { get; set; }
public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiServer>? Servers { get; set; }
public System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiTag>? Tags { get; set; }
public System.Collections.Generic.ISet<Microsoft.OpenApi.Models.OpenApiTag>? Tags { get; set; }
public System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.Interfaces.IOpenApiPathItem>? Webhooks { get; set; }
public Microsoft.OpenApi.Services.OpenApiWorkspace? Workspace { get; set; }
public bool AddComponent<T>(string id, T componentToRegister) { }
Expand Down Expand Up @@ -1664,8 +1664,8 @@ namespace Microsoft.OpenApi.Services
public virtual void Visit(System.Collections.Generic.IList<Microsoft.OpenApi.Models.Interfaces.IOpenApiParameter> parameters) { }
public virtual void Visit(System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiSecurityRequirement> openApiSecurityRequirements) { }
public virtual void Visit(System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiServer> servers) { }
public virtual void Visit(System.Collections.Generic.IList<Microsoft.OpenApi.Models.OpenApiTag> openApiTags) { }
public virtual void Visit(System.Collections.Generic.IList<Microsoft.OpenApi.Models.References.OpenApiTagReference> openApiTags) { }
public virtual void Visit(System.Collections.Generic.ISet<Microsoft.OpenApi.Models.OpenApiTag> openApiTags) { }
public virtual void Visit(System.Text.Json.Nodes.JsonNode node) { }
}
public class OpenApiWalker
Expand Down
4 changes: 2 additions & 2 deletions test/Microsoft.OpenApi.Tests/Visitors/InheritanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void ExpectedVirtualsInvolved()
visitor.Visit(default(OpenApiSecurityRequirement));
visitor.Visit(default(IOpenApiSecurityScheme));
visitor.Visit(default(IOpenApiExample));
visitor.Visit(default(IList<OpenApiTag>));
visitor.Visit(default(ISet<OpenApiTag>));
visitor.Visit(default(IList<OpenApiSecurityRequirement>));
visitor.Visit(default(IOpenApiExtensible));
visitor.Visit(default(IOpenApiExtension));
Expand Down Expand Up @@ -292,7 +292,7 @@ public override void Visit(IOpenApiExample example)
base.Visit(example);
}

public override void Visit(IList<OpenApiTag> openApiTags)
public override void Visit(ISet<OpenApiTag> openApiTags)
{
EncodeCall();
base.Visit(openApiTags);
Expand Down
Loading