Skip to content

Commit 717deb0

Browse files
authored
Merge pull request #2032 from microsoft/fix/tag-references
fix: side effects in tag references
2 parents d087d24 + 23c8fdf commit 717deb0

File tree

50 files changed

+342
-356
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+342
-356
lines changed

src/Microsoft.OpenApi.Readers/OpenApiYamlReader.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings,
8686
/// <inheritdoc/>
8787
public T ReadFragment<T>(MemoryStream input,
8888
OpenApiSpecVersion version,
89+
OpenApiDocument openApiDocument,
8990
out OpenApiDiagnostic diagnostic,
9091
OpenApiReaderSettings settings = null) where T : IOpenApiElement
9192
{
@@ -105,13 +106,13 @@ public T ReadFragment<T>(MemoryStream input,
105106
return default;
106107
}
107108

108-
return ReadFragment<T>(jsonNode, version, out diagnostic, settings);
109+
return ReadFragment<T>(jsonNode, version, openApiDocument, out diagnostic, settings);
109110
}
110111

111112
/// <inheritdoc/>
112-
public static T ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
113+
public static T ReadFragment<T>(JsonNode input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
113114
{
114-
return _jsonReader.ReadFragment<T>(input, version, out diagnostic, settings);
115+
return _jsonReader.ReadFragment<T>(input, version, openApiDocument, out diagnostic, settings);
115116
}
116117

117118
/// <summary>

src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text.Json.Nodes;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Microsoft.OpenApi.Models;
89
using Microsoft.OpenApi.Reader;
910

1011
namespace Microsoft.OpenApi.Interfaces
@@ -36,9 +37,10 @@ public interface IOpenApiReader
3637
/// </summary>
3738
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
3839
/// <param name="version">Version of the OpenAPI specification that the fragment conforms to.</param>
40+
/// <param name="openApiDocument">The OpenApiDocument object to which the fragment belongs, used to lookup references.</param>
3941
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing.</param>
4042
/// <param name="settings">The OpenApiReader settings.</param>
4143
/// <returns>Instance of newly created IOpenApiElement.</returns>
42-
T ReadFragment<T>(MemoryStream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement;
44+
T ReadFragment<T>(MemoryStream input, OpenApiSpecVersion version, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement;
4345
}
4446
}

src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal interface IOpenApiVersionService
2828
/// <param name="node">document fragment node</param>
2929
/// <param name="doc">A host document instance.</param>
3030
/// <returns>Instance of OpenAPIElement</returns>
31-
T LoadElement<T>(ParseNode node, OpenApiDocument doc = null) where T : IOpenApiElement;
31+
T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement;
3232

3333
/// <summary>
3434
/// Converts a generic RootNode instance into a strongly typed OpenApiDocument

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -501,21 +501,6 @@ private static string ConvertByteArrayToString(byte[] hash)
501501
throw new ArgumentException(Properties.SRResource.LocalReferenceRequiresType);
502502
}
503503

504-
// Special case for Tag
505-
if (reference.Type == ReferenceType.Tag)
506-
{
507-
foreach (var tag in this.Tags ?? Enumerable.Empty<OpenApiTag>())
508-
{
509-
if (tag.Name == reference.Id)
510-
{
511-
tag.Reference = reference;
512-
return tag;
513-
}
514-
}
515-
516-
return null;
517-
}
518-
519504
string uriLocation;
520505
if (reference.Id.Contains("/")) // this means its a URL reference
521506
{

src/Microsoft.OpenApi/Models/OpenApiOperation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class OpenApiOperation : IOpenApiSerializable, IOpenApiExtensible, IOpenA
2626
/// A list of tags for API documentation control.
2727
/// Tags can be used for logical grouping of operations by resources or any other qualifier.
2828
/// </summary>
29-
public IList<OpenApiTag>? Tags { get; set; } = new List<OpenApiTag>();
29+
public IList<OpenApiTagReference>? Tags { get; set; } = [];
3030

3131
/// <summary>
3232
/// A short summary of what the operation does.
@@ -121,7 +121,7 @@ public OpenApiOperation() { }
121121
/// </summary>
122122
public OpenApiOperation(OpenApiOperation? operation)
123123
{
124-
Tags = operation?.Tags != null ? new List<OpenApiTag>(operation.Tags) : null;
124+
Tags = operation?.Tags != null ? new List<OpenApiTagReference>(operation.Tags) : null;
125125
Summary = operation?.Summary ?? Summary;
126126
Description = operation?.Description ?? Description;
127127
ExternalDocs = operation?.ExternalDocs != null ? new(operation?.ExternalDocs) : null;

src/Microsoft.OpenApi/Models/OpenApiTag.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.OpenApi.Models
1111
/// <summary>
1212
/// Tag Object.
1313
/// </summary>
14-
public class OpenApiTag : IOpenApiReferenceable, IOpenApiExtensible
14+
public class OpenApiTag : IOpenApiSerializable, IOpenApiExtensible
1515
{
1616
/// <summary>
1717
/// The name of the tag.
@@ -38,11 +38,6 @@ public class OpenApiTag : IOpenApiReferenceable, IOpenApiExtensible
3838
/// </summary>
3939
public bool UnresolvedReference { get; set; }
4040

41-
/// <summary>
42-
/// Reference.
43-
/// </summary>
44-
public OpenApiReference Reference { get; set; }
45-
4641
/// <summary>
4742
/// Parameterless constructor
4843
/// </summary>
@@ -58,7 +53,6 @@ public OpenApiTag(OpenApiTag tag)
5853
ExternalDocs = tag?.ExternalDocs != null ? new(tag.ExternalDocs) : null;
5954
Extensions = tag?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(tag.Extensions) : null;
6055
UnresolvedReference = tag?.UnresolvedReference ?? UnresolvedReference;
61-
Reference = tag?.Reference != null ? new(tag.Reference) : null;
6256
}
6357

6458
/// <summary>

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

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.Linq;
57
using Microsoft.OpenApi.Interfaces;
68
using Microsoft.OpenApi.Writers;
79

@@ -10,21 +12,24 @@ namespace Microsoft.OpenApi.Models.References
1012
/// <summary>
1113
/// Tag Object Reference
1214
/// </summary>
13-
public class OpenApiTagReference : OpenApiTag
15+
public class OpenApiTagReference : OpenApiTag, IOpenApiReferenceable
1416
{
1517
internal OpenApiTag _target;
16-
private readonly OpenApiReference _reference;
17-
private string _description;
1818

19-
private OpenApiTag Target
19+
/// <summary>
20+
/// Reference.
21+
/// </summary>
22+
public OpenApiReference Reference { get; set; }
23+
24+
/// <summary>
25+
/// Resolved target of the reference.
26+
/// </summary>
27+
public OpenApiTag Target
2028
{
2129
get
2230
{
23-
_target ??= Reference.HostDocument?.ResolveReferenceTo<OpenApiTag>(_reference);
24-
_target ??= new OpenApiTag() { Name = _reference.Id };
25-
OpenApiTag resolved = new OpenApiTag(_target);
26-
if (!string.IsNullOrEmpty(_description)) resolved.Description = _description;
27-
return resolved;
31+
_target ??= Reference.HostDocument?.Tags.FirstOrDefault(t => StringComparer.Ordinal.Equals(t.Name, Reference.Id));
32+
return _target;
2833
}
2934
}
3035

@@ -37,49 +42,43 @@ public OpenApiTagReference(string referenceId, OpenApiDocument hostDocument)
3742
{
3843
Utils.CheckArgumentNullOrEmpty(referenceId);
3944

40-
_reference = new OpenApiReference()
45+
Reference = new OpenApiReference()
4146
{
4247
Id = referenceId,
4348
HostDocument = hostDocument,
4449
Type = ReferenceType.Tag
4550
};
46-
47-
Reference = _reference;
4851
}
4952

50-
internal OpenApiTagReference(OpenApiTag target, string referenceId)
53+
/// <summary>
54+
/// Copy Constructor
55+
/// </summary>
56+
/// <param name="source">The source to copy information from.</param>
57+
public OpenApiTagReference(OpenApiTagReference source):base()
5158
{
52-
_target = target;
53-
54-
_reference = new OpenApiReference()
55-
{
56-
Id = referenceId,
57-
Type = ReferenceType.Tag,
58-
};
59+
Reference = source?.Reference != null ? new(source.Reference) : null;
60+
_target = source?._target;
5961
}
6062

63+
private const string ReferenceErrorMessage = "Setting the value from the reference is not supported, use the target property instead.";
6164
/// <inheritdoc/>
62-
public override string Description
63-
{
64-
get => string.IsNullOrEmpty(_description) ? Target?.Description : _description;
65-
set => _description = value;
66-
}
65+
public override string Description { get => Target.Description; set => throw new InvalidOperationException(ReferenceErrorMessage); }
6766

6867
/// <inheritdoc/>
69-
public override OpenApiExternalDocs ExternalDocs { get => Target?.ExternalDocs; set => Target.ExternalDocs = value; }
68+
public override OpenApiExternalDocs ExternalDocs { get => Target.ExternalDocs; set => throw new InvalidOperationException(ReferenceErrorMessage); }
7069

7170
/// <inheritdoc/>
72-
public override IDictionary<string, IOpenApiExtension> Extensions { get => Target?.Extensions; set => Target.Extensions = value; }
71+
public override IDictionary<string, IOpenApiExtension> Extensions { get => Target.Extensions; set => throw new InvalidOperationException(ReferenceErrorMessage); }
7372

7473
/// <inheritdoc/>
75-
public override string Name { get => Target?.Name; set => Target.Name = value; }
74+
public override string Name { get => Target.Name; set => throw new InvalidOperationException(ReferenceErrorMessage); }
7675

7776
/// <inheritdoc/>
7877
public override void SerializeAsV3(IOpenApiWriter writer)
7978
{
80-
if (!writer.GetSettings().ShouldInlineReference(_reference))
79+
if (!writer.GetSettings().ShouldInlineReference(Reference))
8180
{
82-
_reference.SerializeAsV3(writer);
81+
Reference.SerializeAsV3(writer);
8382
}
8483
else
8584
{
@@ -90,9 +89,9 @@ public override void SerializeAsV3(IOpenApiWriter writer)
9089
/// <inheritdoc/>
9190
public override void SerializeAsV31(IOpenApiWriter writer)
9291
{
93-
if (!writer.GetSettings().ShouldInlineReference(_reference))
92+
if (!writer.GetSettings().ShouldInlineReference(Reference))
9493
{
95-
_reference.SerializeAsV31(writer);
94+
Reference.SerializeAsV31(writer);
9695
}
9796
else
9897
{
@@ -103,9 +102,9 @@ public override void SerializeAsV31(IOpenApiWriter writer)
103102
/// <inheritdoc/>
104103
public override void SerializeAsV2(IOpenApiWriter writer)
105104
{
106-
if (!writer.GetSettings().ShouldInlineReference(_reference))
105+
if (!writer.GetSettings().ShouldInlineReference(Reference))
107106
{
108-
_reference.SerializeAsV2(writer);
107+
Reference.SerializeAsV2(writer);
109108
}
110109
else
111110
{

src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,12 @@ public async Task<ReadResult> ReadAsync(Stream input,
148148
/// <inheritdoc/>
149149
public T ReadFragment<T>(MemoryStream input,
150150
OpenApiSpecVersion version,
151+
OpenApiDocument openApiDocument,
151152
out OpenApiDiagnostic diagnostic,
152153
OpenApiReaderSettings settings = null) where T : IOpenApiElement
153154
{
154-
if (input is null) throw new ArgumentNullException(nameof(input));
155+
Utils.CheckArgumentNull(input);
156+
Utils.CheckArgumentNull(openApiDocument);
155157

156158
JsonNode jsonNode;
157159

@@ -167,12 +169,13 @@ public T ReadFragment<T>(MemoryStream input,
167169
return default;
168170
}
169171

170-
return ReadFragment<T>(jsonNode, version, out diagnostic);
172+
return ReadFragment<T>(jsonNode, version, openApiDocument, out diagnostic);
171173
}
172174

173175
/// <inheritdoc/>
174176
public T ReadFragment<T>(JsonNode input,
175177
OpenApiSpecVersion version,
178+
OpenApiDocument openApiDocument,
176179
out OpenApiDiagnostic diagnostic,
177180
OpenApiReaderSettings settings = null) where T : IOpenApiElement
178181
{
@@ -187,7 +190,7 @@ public T ReadFragment<T>(JsonNode input,
187190
try
188191
{
189192
// Parse the OpenAPI element
190-
element = context.ParseFragment<T>(input, version);
193+
element = context.ParseFragment<T>(input, version, openApiDocument);
191194
}
192195
catch (OpenApiException ex)
193196
{

src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ public static ReadResult Load(MemoryStream stream,
6161
/// <param name="input">Stream containing OpenAPI description to parse.</param>
6262
/// <param name="version">Version of the OpenAPI specification that the fragment conforms to.</param>
6363
/// <param name="format"></param>
64+
/// <param name="openApiDocument">The OpenApiDocument object to which the fragment belongs, used to lookup references.</param>
6465
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing.</param>
6566
/// <param name="settings">The OpenApiReader settings.</param>
6667
/// <returns>Instance of newly created IOpenApiElement.</returns>
6768
/// <returns>The OpenAPI element.</returns>
68-
public static T Load<T>(MemoryStream input, OpenApiSpecVersion version, string format, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
69+
public static T Load<T>(MemoryStream input, OpenApiSpecVersion version, string format, OpenApiDocument openApiDocument, out OpenApiDiagnostic diagnostic, OpenApiReaderSettings settings = null) where T : IOpenApiElement
6970
{
7071
format ??= InspectStreamFormat(input);
71-
return OpenApiReaderRegistry.GetReader(format).ReadFragment<T>(input, version, out diagnostic, settings);
72+
return OpenApiReaderRegistry.GetReader(format).ReadFragment<T>(input, version, openApiDocument, out diagnostic, settings);
7273
}
7374

7475
/// <summary>
@@ -91,13 +92,14 @@ public static async Task<ReadResult> LoadAsync(string url, OpenApiReaderSettings
9192
/// <param name="url">The path to the OpenAPI file</param>
9293
/// <param name="version">Version of the OpenAPI specification that the fragment conforms to.</param>
9394
/// <param name="settings">The OpenApiReader settings.</param>
95+
/// <param name="openApiDocument">The OpenApiDocument object to which the fragment belongs, used to lookup references.</param>
9496
/// <param name="token"></param>
9597
/// <returns>Instance of newly created IOpenApiElement.</returns>
9698
/// <returns>The OpenAPI element.</returns>
97-
public static async Task<T> LoadAsync<T>(string url, OpenApiSpecVersion version, OpenApiReaderSettings settings = null, CancellationToken token = default) where T : IOpenApiElement
99+
public static async Task<T> LoadAsync<T>(string url, OpenApiSpecVersion version, OpenApiDocument openApiDocument, OpenApiReaderSettings settings = null, CancellationToken token = default) where T : IOpenApiElement
98100
{
99101
var (stream, format) = await RetrieveStreamAndFormatAsync(url, token).ConfigureAwait(false);
100-
return await LoadAsync<T>(stream, version, format, settings, token);
102+
return await LoadAsync<T>(stream, version, openApiDocument, format, settings, token);
101103
}
102104

103105
/// <summary>
@@ -145,27 +147,30 @@ public static async Task<ReadResult> LoadAsync(Stream input, string format = nul
145147
/// <typeparam name="T"></typeparam>
146148
/// <param name="input"></param>
147149
/// <param name="version"></param>
150+
/// <param name="openApiDocument">The document used to lookup tag or schema references.</param>
148151
/// <param name="format"></param>
149152
/// <param name="settings"></param>
150153
/// <param name="token"></param>
151154
/// <returns></returns>
152155
public static async Task<T> LoadAsync<T>(Stream input,
153156
OpenApiSpecVersion version,
157+
OpenApiDocument openApiDocument,
154158
string format = null,
155159
OpenApiReaderSettings settings = null,
156160
CancellationToken token = default) where T : IOpenApiElement
157161
{
162+
Utils.CheckArgumentNull(openApiDocument);
158163
if (input is null) throw new ArgumentNullException(nameof(input));
159164
if (input is MemoryStream memoryStream)
160165
{
161-
return Load<T>(memoryStream, version, format, out var _, settings);
166+
return Load<T>(memoryStream, version, format, openApiDocument, out var _, settings);
162167
}
163168
else
164169
{
165170
memoryStream = new MemoryStream();
166171
await input.CopyToAsync(memoryStream, 81920, token).ConfigureAwait(false);
167172
memoryStream.Position = 0;
168-
return Load<T>(memoryStream, version, format, out var _, settings);
173+
return Load<T>(memoryStream, version, format, openApiDocument, out var _, settings);
169174
}
170175
}
171176

@@ -195,12 +200,14 @@ public static ReadResult Parse(string input,
195200
/// </summary>
196201
/// <param name="input">The input string.</param>
197202
/// <param name="version"></param>
203+
/// <param name="openApiDocument">The OpenApiDocument object to which the fragment belongs, used to lookup references.</param>
198204
/// <param name="diagnostic">The diagnostic entity containing information from the reading process.</param>
199205
/// <param name="format">The Open API format</param>
200206
/// <param name="settings">The OpenApi reader settings.</param>
201207
/// <returns>An OpenAPI document instance.</returns>
202208
public static T Parse<T>(string input,
203209
OpenApiSpecVersion version,
210+
OpenApiDocument openApiDocument,
204211
out OpenApiDiagnostic diagnostic,
205212
string format = null,
206213
OpenApiReaderSettings settings = null) where T : IOpenApiElement
@@ -209,7 +216,7 @@ public static T Parse<T>(string input,
209216
format ??= InspectInputFormat(input);
210217
settings ??= new OpenApiReaderSettings();
211218
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
212-
return Load<T>(stream, version, format, out diagnostic, settings);
219+
return Load<T>(stream, version, format, openApiDocument, out diagnostic, settings);
213220
}
214221

215222
private static readonly OpenApiReaderSettings DefaultReaderSettings = new();

0 commit comments

Comments
 (0)