Skip to content

Commit 93c468e

Browse files
committed
feat: deduplicates tags at the document level
Signed-off-by: Vincent Biret <[email protected]>
1 parent 49014cf commit 93c468e

File tree

16 files changed

+171
-21
lines changed

16 files changed

+171
-21
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,32 @@ 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+
if (value is HashSet<OpenApiTag> tags && tags.Comparer is OpenApiTagComparer)
96+
{
97+
_tags = tags;
98+
}
99+
else
100+
{
101+
_tags = new HashSet<OpenApiTag>(value, OpenApiTagComparer.Instance);
102+
}
103+
}
104+
}
83105

84106
/// <summary>
85107
/// Additional external documentation.
@@ -123,7 +145,7 @@ public OpenApiDocument(OpenApiDocument? document)
123145
Webhooks = document?.Webhooks != null ? new Dictionary<string, IOpenApiPathItem>(document.Webhooks) : null;
124146
Components = document?.Components != null ? new(document?.Components) : null;
125147
SecurityRequirements = document?.SecurityRequirements != null ? new List<OpenApiSecurityRequirement>(document.SecurityRequirements) : null;
126-
Tags = document?.Tags != null ? new List<OpenApiTag>(document.Tags) : null;
148+
Tags = document?.Tags != null ? new HashSet<OpenApiTag>(document.Tags, OpenApiTagComparer.Instance) : null;
127149
ExternalDocs = document?.ExternalDocs != null ? new(document?.ExternalDocs) : null;
128150
Extensions = document?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(document.Extensions) : null;
129151
Annotations = document?.Annotations != null ? new Dictionary<string, object>(document.Annotations) : 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;
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<OpenApiTag>
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(OpenApiTag? x, OpenApiTag? 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(OpenApiTag obj) => obj?.Name is null ? 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, _) => o.Tags = new HashSet<OpenApiTag>(n.CreateList(LoadTag, o), OpenApiTagComparer.Instance)},
108108
{"externalDocs", (o, n, _) => o.ExternalDocs = LoadExternalDocs(n, o)}
109109
};
110110

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, _) => o.Tags = new HashSet<OpenApiTag>(n.CreateList(LoadTag, o), 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/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, _) => o.Tags = new HashSet<OpenApiTag>(n.CreateList(LoadTag, o), 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/Services/OpenApiVisitorBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ public virtual void Visit(IOpenApiExample example)
309309
/// <summary>
310310
/// Visits list of <see cref="OpenApiTag"/>
311311
/// </summary>
312-
public virtual void Visit(IList<OpenApiTag> openApiTags)
312+
public virtual void Visit(ISet<OpenApiTag> openApiTags)
313313
{
314314
}
315315

src/Microsoft.OpenApi/Services/OpenApiWalker.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Text.Json.Nodes;
78
using Microsoft.OpenApi.Any;
89
using Microsoft.OpenApi.Extensions;
@@ -60,7 +61,7 @@ public void Walk(OpenApiDocument doc)
6061
/// <summary>
6162
/// Visits list of <see cref="OpenApiTag"/> and child objects
6263
/// </summary>
63-
internal void Walk(IList<OpenApiTag> tags)
64+
internal void Walk(ISet<OpenApiTag> tags)
6465
{
6566
if (tags == null)
6667
{
@@ -72,9 +73,10 @@ internal void Walk(IList<OpenApiTag> tags)
7273
// Visit tags
7374
if (tags != null)
7475
{
75-
for (var i = 0; i < tags.Count; i++)
76+
var tagsAsArray = tags.ToArray();
77+
for (var i = 0; i < tagsAsArray.Length; i++)
7678
{
77-
Walk(i.ToString(), () => Walk(tags[i]));
79+
Walk(i.ToString(), () => Walk(tagsAsArray[i]));
7880
}
7981
}
8082
}
@@ -1213,7 +1215,7 @@ internal void Walk(IOpenApiElement element)
12131215
case OpenApiServer e: Walk(e); break;
12141216
case OpenApiServerVariable e: Walk(e); break;
12151217
case OpenApiTag e: Walk(e); break;
1216-
case IList<OpenApiTag> e: Walk(e); break;
1218+
case ISet<OpenApiTag> e: Walk(e); break;
12171219
case IOpenApiExtensible e: Walk(e); break;
12181220
case IOpenApiExtension e: Walk(e); break;
12191221
}

test/Microsoft.OpenApi.Hidi.Tests/UtilityFiles/OpenApiDocumentMock.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ public static OpenApiDocument CreateOpenApiDocument()
629629
}
630630
}
631631
},
632-
Tags = new List<OpenApiTag>
632+
Tags = new HashSet<OpenApiTag>
633633
{
634634
new()
635635
{

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ public async Task ParseModifiedPetStoreDocumentWithTagAndSecurityShouldSucceed()
10001000
}
10011001
},
10021002
Components = components,
1003-
Tags = new List<OpenApiTag>
1003+
Tags = new HashSet<OpenApiTag>
10041004
{
10051005
new OpenApiTag
10061006
{

0 commit comments

Comments
 (0)