Skip to content

Commit 9e42a5f

Browse files
committed
Added support for handling cycles
1 parent 05619b5 commit 9e42a5f

File tree

6 files changed

+324
-22
lines changed

6 files changed

+324
-22
lines changed

src/Microsoft.OpenApi/Models/OpenApiComponents.cs

Lines changed: 16 additions & 0 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.Any;
68
using Microsoft.OpenApi.Interfaces;
79
using Microsoft.OpenApi.Writers;
@@ -77,9 +79,23 @@ public void SerializeAsV3(IOpenApiWriter writer)
7779
}
7880

7981
// If references have been inlined we don't need the to render the components section
82+
// however if they have cycles, then we will need a component rendered
8083
if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences)
8184
{
85+
var loops = writer.GetSettings().LoopDetector.Loops;
8286
writer.WriteStartObject();
87+
if (loops.TryGetValue(typeof(OpenApiSchema), out List<object> schemas))
88+
{
89+
var openApiSchemas = schemas.Cast<OpenApiSchema>().Distinct().ToList()
90+
.ToDictionary<OpenApiSchema, string>(k => k.Reference.Id);
91+
92+
writer.WriteOptionalMap(
93+
OpenApiConstants.Schemas,
94+
Schemas,
95+
(w, key, component) => {
96+
component.SerializeAsV3WithoutReference(w);
97+
});
98+
}
8399
writer.WriteEndObject();
84100
return;
85101
}

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,50 @@ public void SerializeAsV2(IOpenApiWriter writer)
126126
// paths
127127
writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV2(w));
128128

129-
// Serialize each referenceable object as full object without reference if the reference in the object points to itself.
130-
// If the reference exists but points to other objects, the object is serialized to just that reference.
131-
132-
// definitions
133-
writer.WriteOptionalMap(
134-
OpenApiConstants.Definitions,
135-
Components?.Schemas,
136-
(w, key, component) =>
129+
// If references have been inlined we don't need the to render the components section
130+
// however if they have cycles, then we will need a component rendered
131+
if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences)
132+
{
133+
var loops = writer.GetSettings().LoopDetector.Loops;
134+
writer.WriteStartObject();
135+
if (loops.TryGetValue(typeof(OpenApiSchema), out List<object> schemas))
137136
{
138-
if (component.Reference != null &&
139-
component.Reference.Type == ReferenceType.Schema &&
140-
component.Reference.Id == key)
141-
{
142-
component.SerializeAsV2WithoutReference(w);
143-
}
144-
else
137+
var openApiSchemas = schemas.Cast<OpenApiSchema>().Distinct().ToList()
138+
.ToDictionary<OpenApiSchema, string>(k => k.Reference.Id);
139+
140+
writer.WriteOptionalMap(
141+
OpenApiConstants.Definitions,
142+
openApiSchemas,
143+
(w, key, component) =>
144+
{
145+
component.SerializeAsV2WithoutReference(w);
146+
});
147+
}
148+
writer.WriteEndObject();
149+
return;
150+
}
151+
else
152+
{
153+
// Serialize each referenceable object as full object without reference if the reference in the object points to itself.
154+
// If the reference exists but points to other objects, the object is serialized to just that reference.
155+
// definitions
156+
writer.WriteOptionalMap(
157+
OpenApiConstants.Definitions,
158+
Components?.Schemas,
159+
(w, key, component) =>
145160
{
146-
component.SerializeAsV2(w);
147-
}
148-
});
149-
161+
if (component.Reference != null &&
162+
component.Reference.Type == ReferenceType.Schema &&
163+
component.Reference.Id == key)
164+
{
165+
component.SerializeAsV2WithoutReference(w);
166+
}
167+
else
168+
{
169+
component.SerializeAsV2(w);
170+
}
171+
});
172+
}
150173
// parameters
151174
writer.WriteOptionalMap(
152175
OpenApiConstants.Parameters,

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,31 @@ public void SerializeAsV3(IOpenApiWriter writer)
251251
throw Error.ArgumentNull(nameof(writer));
252252
}
253253

254-
if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences)
254+
var settings = writer.GetSettings();
255+
256+
if (Reference != null)
255257
{
256-
Reference.SerializeAsV3(writer);
257-
return;
258+
if (settings.ReferenceInline != ReferenceInlineSetting.InlineLocalReferences)
259+
{
260+
Reference.SerializeAsV3(writer);
261+
return;
262+
}
263+
264+
// If Loop is detected then just Serialize as a reference.
265+
if (!settings.LoopDetector.PushLoop<OpenApiSchema>(this))
266+
{
267+
settings.LoopDetector.SaveLoop(this);
268+
Reference.SerializeAsV3(writer);
269+
return;
270+
}
258271
}
259272

260273
SerializeAsV3WithoutReference(writer);
274+
275+
if (Reference != null)
276+
{
277+
settings.LoopDetector.PopLoop<OpenApiSchema>();
278+
}
261279
}
262280

263281
/// <summary>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.OpenApi.Services
8+
{
9+
internal class LoopDetector
10+
{
11+
private readonly Dictionary<Type, Stack<object>> _loopStacks = new Dictionary<Type, Stack<object>>();
12+
13+
/// <summary>
14+
/// Maintain history of traversals to avoid stack overflows from cycles
15+
/// </summary>
16+
/// <param name="loopId">Any unique identifier for a stack.</param>
17+
/// <param name="key">Identifier used for current context.</param>
18+
/// <returns>If method returns false a loop was detected and the key is not added.</returns>
19+
public bool PushLoop<T>(T key)
20+
{
21+
Stack<object> stack;
22+
if (!_loopStacks.TryGetValue(typeof(T), out stack))
23+
{
24+
stack = new Stack<object>();
25+
_loopStacks.Add(typeof(T), stack);
26+
}
27+
28+
if (!stack.Contains(key))
29+
{
30+
stack.Push(key);
31+
return true;
32+
}
33+
else
34+
{
35+
return false; // Loop detected
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Exit from the context in cycle detection
41+
/// </summary>
42+
/// <param name="loopid">Identifier of loop</param>
43+
public void PopLoop<T>()
44+
{
45+
if (_loopStacks[typeof(T)].Count > 0)
46+
{
47+
_loopStacks[typeof(T)].Pop();
48+
}
49+
}
50+
51+
public void SaveLoop<T>(T loop)
52+
{
53+
if (!Loops.ContainsKey(typeof(T)))
54+
{
55+
Loops[typeof(T)] = new List<object>();
56+
}
57+
Loops[typeof(T)].Add(loop);
58+
}
59+
60+
/// <summary>
61+
/// List of Loops detected
62+
/// </summary>
63+
public Dictionary<Type, List<object>> Loops { get; } = new Dictionary<Type, List<object>>();
64+
65+
/// <summary>
66+
/// Reset loop tracking stack
67+
/// </summary>
68+
/// <param name="loopid">Identifier of loop to clear</param>
69+
internal void ClearLoop<T>()
70+
{
71+
_loopStacks[typeof(T)].Clear();
72+
}
73+
}
74+
}

src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
using Microsoft.OpenApi.Services;
3+
24
namespace Microsoft.OpenApi.Writers
35
{
46
/// <summary>
@@ -25,6 +27,7 @@ public enum ReferenceInlineSetting
2527
/// </summary>
2628
public class OpenApiWriterSettings
2729
{
30+
internal LoopDetector LoopDetector { get; } = new LoopDetector();
2831
/// <summary>
2932
/// Indicates how references in the source document should be handled.
3033
/// </summary>

0 commit comments

Comments
 (0)