Skip to content

Commit 1be8edc

Browse files
authored
Merge pull request #2167 from microsoft/fix/unique-tags
fix/unique tags
2 parents 6a31f30 + 8997383 commit 1be8edc

23 files changed

+324
-77
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,27 @@ public void RegisterComponents()
7676
public IList<OpenApiSecurityRequirement>? SecurityRequirements { get; set; } =
7777
new List<OpenApiSecurityRequirement>();
7878

79+
private HashSet<OpenApiTag>? _tags;
7980
/// <summary>
8081
/// A list of tags used by the specification with additional metadata.
8182
/// </summary>
82-
public IList<OpenApiTag>? Tags { get; set; } = new List<OpenApiTag>();
83+
public ISet<OpenApiTag>? Tags
84+
{
85+
get
86+
{
87+
return _tags;
88+
}
89+
set
90+
{
91+
if (value is null)
92+
{
93+
return;
94+
}
95+
_tags = value is HashSet<OpenApiTag> tags && tags.Comparer is OpenApiTagComparer ?
96+
tags :
97+
new HashSet<OpenApiTag>(value, OpenApiTagComparer.Instance);
98+
}
99+
}
83100

84101
/// <summary>
85102
/// Additional external documentation.
@@ -123,7 +140,7 @@ public OpenApiDocument(OpenApiDocument? document)
123140
Webhooks = document?.Webhooks != null ? new Dictionary<string, IOpenApiPathItem>(document.Webhooks) : null;
124141
Components = document?.Components != null ? new(document?.Components) : null;
125142
SecurityRequirements = document?.SecurityRequirements != null ? new List<OpenApiSecurityRequirement>(document.SecurityRequirements) : null;
126-
Tags = document?.Tags != null ? new List<OpenApiTag>(document.Tags) : null;
143+
Tags = document?.Tags != null ? new HashSet<OpenApiTag>(document.Tags, OpenApiTagComparer.Instance) : null;
127144
ExternalDocs = document?.ExternalDocs != null ? new(document?.ExternalDocs) : null;
128145
Extensions = document?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(document.Extensions) : null;
129146
Annotations = document?.Annotations != null ? new Dictionary<string, object>(document.Annotations) : null;

src/Microsoft.OpenApi/Models/OpenApiOperation.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,28 @@ public class OpenApiOperation : IOpenApiSerializable, IOpenApiExtensible, IOpenA
2323
/// </summary>
2424
public const bool DeprecatedDefault = false;
2525

26+
private HashSet<OpenApiTagReference>? _tags;
2627
/// <summary>
2728
/// A list of tags for API documentation control.
2829
/// Tags can be used for logical grouping of operations by resources or any other qualifier.
2930
/// </summary>
30-
public IList<OpenApiTagReference>? Tags { get; set; } = [];
31+
public ISet<OpenApiTagReference>? Tags
32+
{
33+
get
34+
{
35+
return _tags;
36+
}
37+
set
38+
{
39+
if (value is null)
40+
{
41+
return;
42+
}
43+
_tags = value is HashSet<OpenApiTagReference> tags && tags.Comparer is OpenApiTagComparer ?
44+
tags :
45+
new HashSet<OpenApiTagReference>(value, OpenApiTagComparer.Instance);
46+
}
47+
}
3148

3249
/// <summary>
3350
/// A short summary of what the operation does.
@@ -123,7 +140,7 @@ public OpenApiOperation() { }
123140
public OpenApiOperation(OpenApiOperation operation)
124141
{
125142
Utils.CheckArgumentNull(operation);
126-
Tags = operation.Tags != null ? new List<OpenApiTagReference>(operation.Tags) : null;
143+
Tags = operation.Tags != null ? new HashSet<OpenApiTagReference>(operation.Tags) : null;
127144
Summary = operation.Summary ?? Summary;
128145
Description = operation.Description ?? Description;
129146
ExternalDocs = operation.ExternalDocs != null ? new(operation.ExternalDocs) : null;

src/Microsoft.OpenApi/Models/References/OpenApiTagReference.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public override OpenApiTag Target
2121
{
2222
get
2323
{
24-
return Reference.HostDocument?.Tags.FirstOrDefault(t => StringComparer.Ordinal.Equals(t.Name, Reference.Id));
24+
return Reference.HostDocument?.Tags?.FirstOrDefault(t => OpenApiTagComparer.StringComparer.Equals(t.Name, Reference.Id));
2525
}
2626
}
2727

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.OpenApi.Models.Interfaces;
4+
5+
namespace Microsoft.OpenApi;
6+
7+
#nullable enable
8+
/// <summary>
9+
/// This comparer is used to maintain a globally unique list of tags encountered
10+
/// in a particular OpenAPI document.
11+
/// </summary>
12+
internal sealed class OpenApiTagComparer : IEqualityComparer<IOpenApiTag>
13+
{
14+
private static readonly Lazy<OpenApiTagComparer> _lazyInstance = new(() => new OpenApiTagComparer());
15+
/// <summary>
16+
/// Default instance for the comparer.
17+
/// </summary>
18+
internal static OpenApiTagComparer Instance { get => _lazyInstance.Value; }
19+
20+
/// <inheritdoc/>
21+
public bool Equals(IOpenApiTag? x, IOpenApiTag? y)
22+
{
23+
if (x is null && y is null)
24+
{
25+
return true;
26+
}
27+
if (x is null || y is null)
28+
{
29+
return false;
30+
}
31+
if (ReferenceEquals(x, y))
32+
{
33+
return true;
34+
}
35+
return StringComparer.Equals(x.Name, y.Name);
36+
}
37+
38+
// Tag comparisons are case-sensitive by default. Although the OpenAPI specification
39+
// only outlines case sensitivity for property names, we extend this principle to
40+
// property values for tag names as well.
41+
// See https://spec.openapis.org/oas/v3.1.0#format.
42+
internal static readonly StringComparer StringComparer = StringComparer.Ordinal;
43+
44+
/// <inheritdoc/>
45+
public int GetHashCode(IOpenApiTag obj) => string.IsNullOrEmpty(obj?.Name) ? 0 : StringComparer.GetHashCode(obj!.Name);
46+
}
47+
#nullable restore

src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ internal static partial class OpenApiV2Deserializer
104104
}
105105
},
106106
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)},
107-
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o)},
107+
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
108108
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)}
109109
};
110110

src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ internal static partial class OpenApiV2Deserializer
2323
new()
2424
{
2525
{
26-
"tags", (o, n, doc) => o.Tags = n.CreateSimpleList(
27-
(valueNode, doc) =>
28-
LoadTagByReference(
29-
valueNode.GetScalarValue(), doc), doc)
26+
"tags", (o, n, doc) => {
27+
if (n.CreateSimpleList((valueNode, doc) => LoadTagByReference(valueNode.GetScalarValue(), doc), doc) is {Count: > 0} tags)
28+
{
29+
o.Tags = new HashSet<OpenApiTagReference>(tags, OpenApiTagComparer.Instance);
30+
}
31+
}
3032
},
3133
{
3234
"summary",

src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
56
using Microsoft.OpenApi.Extensions;
67
using Microsoft.OpenApi.Models;
78
using Microsoft.OpenApi.Reader.ParseNodes;
@@ -26,7 +27,7 @@ internal static partial class OpenApiV3Deserializer
2627
{"servers", (o, n, _) => o.Servers = n.CreateList(LoadServer, o)},
2728
{"paths", (o, n, _) => o.Paths = LoadPaths(n, o)},
2829
{"components", (o, n, _) => o.Components = LoadComponents(n, o)},
29-
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o) },
30+
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
3031
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)},
3132
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)}
3233
};

src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
56
using Microsoft.OpenApi.Extensions;
67
using Microsoft.OpenApi.Models;
78
using Microsoft.OpenApi.Models.References;
@@ -19,10 +20,12 @@ internal static partial class OpenApiV3Deserializer
1920
new()
2021
{
2122
{
22-
"tags", (o, n, doc) => o.Tags = n.CreateSimpleList(
23-
(valueNode, doc) =>
24-
LoadTagByReference(
25-
valueNode.GetScalarValue(), doc), doc)
23+
"tags", (o, n, doc) => {
24+
if (n.CreateSimpleList((valueNode, doc) => LoadTagByReference(valueNode.GetScalarValue(), doc), doc) is {Count: > 0} tags)
25+
{
26+
o.Tags = new HashSet<OpenApiTagReference>(tags, OpenApiTagComparer.Instance);
27+
}
28+
}
2629
},
2730
{
2831
"summary",

src/Microsoft.OpenApi/Reader/V31/OpenApiDocumentDeserializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Microsoft.OpenApi.Extensions;
34
using Microsoft.OpenApi.Models;
45
using Microsoft.OpenApi.Reader.ParseNodes;
@@ -24,7 +25,7 @@ internal static partial class OpenApiV31Deserializer
2425
{"paths", (o, n, _) => o.Paths = LoadPaths(n, o)},
2526
{"webhooks", (o, n, _) => o.Webhooks = n.CreateMap(LoadPathItem, o)},
2627
{"components", (o, n, _) => o.Components = LoadComponents(n, o)},
27-
{"tags", (o, n, _) => o.Tags = n.CreateList(LoadTag, o) },
28+
{"tags", (o, n, _) => { if (n.CreateList(LoadTag, o) is {Count:> 0} tags) {o.Tags = new HashSet<OpenApiTag>(tags, OpenApiTagComparer.Instance); } } },
2829
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)},
2930
{"security", (o, n, _) => o.SecurityRequirements = n.CreateList(LoadSecurityRequirement, o)}
3031
};

src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Microsoft.OpenApi.Extensions;
34
using Microsoft.OpenApi.Models;
45
using Microsoft.OpenApi.Models.References;
@@ -16,9 +17,12 @@ internal static partial class OpenApiV31Deserializer
1617
new()
1718
{
1819
{
19-
"tags", (o, n, doc) => o.Tags = n.CreateSimpleList(
20-
(valueNode, doc) =>
21-
LoadTagByReference(valueNode.GetScalarValue(), doc), doc)
20+
"tags", (o, n, doc) => {
21+
if (n.CreateSimpleList((valueNode, doc) => LoadTagByReference(valueNode.GetScalarValue(), doc), doc) is {Count: > 0} tags)
22+
{
23+
o.Tags = new HashSet<OpenApiTagReference>(tags, OpenApiTagComparer.Instance);
24+
}
25+
}
2226
},
2327
{
2428
"summary", (o, n, _) =>

0 commit comments

Comments
 (0)