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 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);
}
}
}

/// <summary>
/// Additional external documentation.
Expand Down Expand Up @@ -123,7 +145,7 @@
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 Expand Up @@ -222,7 +244,7 @@
/// <summary>
/// Serialize <see cref="OpenApiDocument"/> to OpenAPI object V2.0.
/// </summary>
public void SerializeAsV2(IOpenApiWriter writer)

Check warning on line 247 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 30 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
Utils.CheckArgumentNull(writer);

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 @@
}
},
{"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 Expand Up @@ -202,7 +202,7 @@
#endif
{
var pieces = host.Split(':');
host = pieces.First();

Check warning on line 205 in src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Indexing at 0 should be used instead of the "Enumerable" extension method "First" (https://rules.sonarsource.com/csharp/RSPEC-6608)
port = int.Parse(pieces.Last(), CultureInfo.InvariantCulture);
}

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 @@
/// <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 @@ -70,11 +71,12 @@
_visitor.Visit(tags);

// Visit tags
if (tags != null)

Check warning on line 74 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
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 @@ -130,7 +132,7 @@
/// <summary>
/// Visits <see cref="OpenApiComponents"/> and child objects
/// </summary>
internal void Walk(OpenApiComponents? components)

Check warning on line 135 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 52 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (components == null)
{
Expand Down Expand Up @@ -266,7 +268,7 @@
_visitor.Visit(paths);

// Visit Paths
if (paths != null)

Check warning on line 271 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
foreach (var pathItem in paths)
{
Expand All @@ -291,7 +293,7 @@
_visitor.Visit(webhooks);

// Visit Webhooks
if (webhooks != null)

Check warning on line 296 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
foreach (var pathItem in webhooks)
{
Expand All @@ -299,7 +301,7 @@
Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for /
_visitor.CurrentKeys.Path = null;
}
};

Check warning on line 304 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this empty statement. (https://rules.sonarsource.com/csharp/RSPEC-1116)
}

/// <summary>
Expand Down Expand Up @@ -423,7 +425,7 @@

_visitor.Visit(callback);

if (callback != null)

Check warning on line 428 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
foreach (var item in callback.PathItems)
{
Expand Down Expand Up @@ -460,7 +462,7 @@
return;
}

if (tag is IOpenApiReferenceHolder openApiReferenceHolder)

Check warning on line 465 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
Walk(openApiReferenceHolder);
}
Expand Down Expand Up @@ -493,7 +495,7 @@

_visitor.Visit(serverVariables);

if (serverVariables != null)

Check warning on line 498 in src/Microsoft.OpenApi/Services/OpenApiWalker.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
foreach (var variable in serverVariables)
{
Expand Down Expand Up @@ -1213,7 +1215,7 @@
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