From f00c27cc0e104dd67ae26f85323950a7ae44e5d4 Mon Sep 17 00:00:00 2001 From: darrelmiller Date: Tue, 7 Jan 2020 12:36:17 -0500 Subject: [PATCH 1/4] Added support for inlining references when writing out API descriptions --- .../OpenApiReaderSettings.cs | 5 ++- src/Microsoft.OpenApi.Workbench/MainModel.cs | 23 ++++++++++++- .../MainWindow.xaml | 1 + .../Extensions/OpenAPIWriterExtensions.cs | 26 +++++++++++++++ .../OpenApiSerializableExtensions.cs | 9 +++-- .../Models/OpenApiCallback.cs | 2 +- .../Models/OpenApiComponents.cs | 6 ++++ .../Models/OpenApiExample.cs | 2 +- src/Microsoft.OpenApi/Models/OpenApiLink.cs | 2 +- .../Models/OpenApiParameter.cs | 4 +-- .../Models/OpenApiResponse.cs | 4 +-- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 5 +-- .../Writers/OpenApiJsonWriter.cs | 2 +- .../Writers/OpenApiWriterBase.cs | 19 +++++++++++ .../Writers/OpenApiWriterSettings.cs | 33 +++++++++++++++++++ .../Writers/OpenApiYamlWriter.cs | 5 +-- 16 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.OpenApi/Extensions/OpenAPIWriterExtensions.cs create mode 100644 src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs index 1b1c2f367..acac59051 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs @@ -1,4 +1,7 @@ -using Microsoft.OpenApi.Any; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Readers.ParseNodes; using Microsoft.OpenApi.Validations; diff --git a/src/Microsoft.OpenApi.Workbench/MainModel.cs b/src/Microsoft.OpenApi.Workbench/MainModel.cs index 087340f14..50aaeda17 100644 --- a/src/Microsoft.OpenApi.Workbench/MainModel.cs +++ b/src/Microsoft.OpenApi.Workbench/MainModel.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Text; using Microsoft.OpenApi.Extensions; @@ -11,6 +12,7 @@ using Microsoft.OpenApi.Readers; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; +using Microsoft.OpenApi.Writers; namespace Microsoft.OpenApi.Workbench { @@ -31,6 +33,11 @@ public class MainModel : INotifyPropertyChanged private string _renderTime; + /// + /// Default format. + /// + private bool _Inline = false; + /// /// Default format. /// @@ -112,6 +119,16 @@ public OpenApiFormat Format } } + public bool Inline + { + get => _Inline; + set + { + _Inline = value; + OnPropertyChanged(nameof(Inline)); + } + } + public OpenApiSpecVersion Version { get => _version; @@ -232,10 +249,14 @@ internal void ParseDocument() private string WriteContents(OpenApiDocument document) { var outputStream = new MemoryStream(); + document.Serialize( outputStream, Version, - Format); + Format, + new OpenApiWriterSettings() { + ReferenceInline = this.Inline == true ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences + }); outputStream.Position = 0; diff --git a/src/Microsoft.OpenApi.Workbench/MainWindow.xaml b/src/Microsoft.OpenApi.Workbench/MainWindow.xaml index 21aba30e4..daf8a2209 100644 --- a/src/Microsoft.OpenApi.Workbench/MainWindow.xaml +++ b/src/Microsoft.OpenApi.Workbench/MainWindow.xaml @@ -42,6 +42,7 @@ + diff --git a/src/Microsoft.OpenApi/Extensions/OpenAPIWriterExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenAPIWriterExtensions.cs new file mode 100644 index 000000000..a32807ab6 --- /dev/null +++ b/src/Microsoft.OpenApi/Extensions/OpenAPIWriterExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.OpenApi.Writers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OpenApi +{ + internal static class OpenAPIWriterExtensions + { + /// + /// Temporary extension method until we add Settings property to IOpenApiWriter in next major version + /// + /// + /// + internal static OpenApiWriterSettings GetSettings(this IOpenApiWriter openApiWriter) + { + if (openApiWriter is OpenApiWriterBase) + { + return ((OpenApiWriterBase)openApiWriter).Settings; + } + return new OpenApiWriterSettings(); + } + } +} diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs index 2f943148f..876ef7cce 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs @@ -54,7 +54,8 @@ public static void Serialize( this T element, Stream stream, OpenApiSpecVersion specVersion, - OpenApiFormat format) + OpenApiFormat format, + OpenApiWriterSettings settings = null) where T : IOpenApiSerializable { if (stream == null) @@ -67,10 +68,10 @@ public static void Serialize( switch (format) { case OpenApiFormat.Json: - writer = new OpenApiJsonWriter(streamWriter); + writer = new OpenApiJsonWriter(streamWriter,settings); break; case OpenApiFormat.Yaml: - writer = new OpenApiYamlWriter(streamWriter); + writer = new OpenApiYamlWriter(streamWriter, settings); break; default: throw new OpenApiException(string.Format(SRResource.OpenApiFormatNotSupported, format)); @@ -86,6 +87,7 @@ public static void Serialize( /// The Open API element. /// The output writer. /// Version of the specification the output should conform to + public static void Serialize(this T element, IOpenApiWriter writer, OpenApiSpecVersion specVersion) where T : IOpenApiSerializable { @@ -116,6 +118,7 @@ public static void Serialize(this T element, IOpenApiWriter writer, OpenApiSp writer.Flush(); } + /// /// Serializes the to the Open API document as a string in JSON format. /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiCallback.cs b/src/Microsoft.OpenApi/Models/OpenApiCallback.cs index fc6db76dc..25258f97e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiCallback.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiCallback.cs @@ -70,7 +70,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; diff --git a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs index 6adab4b13..08769f7bb 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs @@ -76,6 +76,12 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } + // If references have been inlined we don't need the to render the components section + if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences) + { + return; + } + writer.WriteStartObject(); // Serialize each referenceable object as full object without reference if the reference in the object points to itself. diff --git a/src/Microsoft.OpenApi/Models/OpenApiExample.cs b/src/Microsoft.OpenApi/Models/OpenApiExample.cs index 0ebce156b..1cde685e6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiExample.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiExample.cs @@ -64,7 +64,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; diff --git a/src/Microsoft.OpenApi/Models/OpenApiLink.cs b/src/Microsoft.OpenApi/Models/OpenApiLink.cs index 4bd7ef4bf..a5503af4d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiLink.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiLink.cs @@ -71,7 +71,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs index a34116723..f3e07cdd2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs @@ -139,7 +139,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; @@ -210,7 +210,7 @@ public void SerializeAsV2(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV2(writer); return; diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index a802f79af..1ed1d9c76 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -61,7 +61,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; @@ -105,7 +105,7 @@ public void SerializeAsV2(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV2(writer); return; diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 008f4ae21..12da00a5c 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -251,7 +251,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; @@ -424,7 +424,8 @@ internal void SerializeAsV2( throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV2(writer); return; diff --git a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs index 19e5b16ab..70ac529d9 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs @@ -14,7 +14,7 @@ public class OpenApiJsonWriter : OpenApiWriterBase /// Initializes a new instance of the class. /// /// The text writer. - public OpenApiJsonWriter(TextWriter textWriter) : base(textWriter) + public OpenApiJsonWriter(TextWriter textWriter, OpenApiWriterSettings settings = null) : base(textWriter, settings) { } diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs index 172e3fa2a..318593a04 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs @@ -14,6 +14,11 @@ namespace Microsoft.OpenApi.Writers /// public abstract class OpenApiWriterBase : IOpenApiWriter { + /// + /// Settings for controlling how the OpenAPI document will be written out. + /// + public OpenApiWriterSettings Settings { get; set; } + /// /// The indentation string to prepand to each line for each indentation level. /// @@ -41,6 +46,20 @@ public OpenApiWriterBase(TextWriter textWriter) Scopes = new Stack(); } + /// + /// + /// + /// + /// + public OpenApiWriterBase(TextWriter textWriter, OpenApiWriterSettings settings = null) : this(textWriter) + { + if (settings == null) + { + settings = new OpenApiWriterSettings(); + } + Settings = settings; + } + /// /// Base Indentation Level. /// This denotes how many indentations are needed for the property in the base object. diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs new file mode 100644 index 000000000..00bb9dede --- /dev/null +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs @@ -0,0 +1,33 @@ + +namespace Microsoft.OpenApi.Writers +{ + /// + /// Indicates if and when the reader should convert references into complete object renderings + /// + public enum ReferenceInlineSetting + { + /// + /// Create placeholder objects with an OpenApiReference instance and UnresolvedReference set to true. + /// + DoNotInlineReferences, + /// + /// Convert local references to references of valid domain objects. + /// + InlineLocalReferences, + /// + /// Convert all references to references of valid domain objects. + /// + InlineAllReferences + } + + /// + /// Configuration settings to control how OpenAPI documents are written + /// + public class OpenApiWriterSettings + { + /// + /// Indicates how references in the source document should be handled. + /// + public ReferenceInlineSetting ReferenceInline { get; set; } = ReferenceInlineSetting.DoNotInlineReferences; + } +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs index d213e6154..5d4560101 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs @@ -14,11 +14,12 @@ public class OpenApiYamlWriter : OpenApiWriterBase /// Initializes a new instance of the class. /// /// The text writer. - public OpenApiYamlWriter(TextWriter textWriter) : base(textWriter) + /// + public OpenApiYamlWriter(TextWriter textWriter, OpenApiWriterSettings settings = null) : base(textWriter, settings) { + } - /// /// Base Indentation Level. /// This denotes how many indentations are needed for the property in the base object. From 08bb98f8b85f385cc70307cfe248dd1ec3eaf188 Mon Sep 17 00:00:00 2001 From: darrelmiller Date: Tue, 7 Jan 2020 13:27:07 -0500 Subject: [PATCH 2/4] Added empty object to components. --- src/Microsoft.OpenApi/Models/OpenApiComponents.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs index 08769f7bb..a041c66c1 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs @@ -79,6 +79,8 @@ public void SerializeAsV3(IOpenApiWriter writer) // If references have been inlined we don't need the to render the components section if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences) { + writer.WriteStartObject(); + writer.WriteEndObject(); return; } From 456cec5adc64aa7136b9cb8503a9fdc8a2039395 Mon Sep 17 00:00:00 2001 From: darrelmiller Date: Tue, 7 Jan 2020 14:55:02 -0500 Subject: [PATCH 3/4] Added support for inlining header refefences --- src/Microsoft.OpenApi/Models/OpenApiHeader.cs | 4 ++-- src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs index c5fa288ae..8aab78dbb 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs @@ -96,7 +96,7 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV3(writer); return; @@ -161,7 +161,7 @@ public void SerializeAsV2(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null) + if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) { Reference.SerializeAsV2(writer); return; diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs index 318593a04..5ef51e73b 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Properties; namespace Microsoft.OpenApi.Writers @@ -14,6 +15,7 @@ namespace Microsoft.OpenApi.Writers /// public abstract class OpenApiWriterBase : IOpenApiWriter { + /// /// Settings for controlling how the OpenAPI document will be written out. /// From 9e42a5f0589cb2451d19b008fbdaf798f002962d Mon Sep 17 00:00:00 2001 From: darrelmiller Date: Sun, 12 Jan 2020 21:23:24 -0500 Subject: [PATCH 4/4] Added support for handling cycles --- .../Models/OpenApiComponents.cs | 16 ++ .../Models/OpenApiDocument.cs | 61 +++++-- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 24 ++- .../Services/LoopDetector.cs | 74 ++++++++ .../Writers/OpenApiWriterSettings.cs | 3 + .../Writers/OpenApiYamlWriterTests.cs | 168 ++++++++++++++++++ 6 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.OpenApi/Services/LoopDetector.cs diff --git a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs index 14e2ff049..08b8bd020 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using System.Linq; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Writers; @@ -77,9 +79,23 @@ public void SerializeAsV3(IOpenApiWriter writer) } // If references have been inlined we don't need the to render the components section + // however if they have cycles, then we will need a component rendered if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences) { + var loops = writer.GetSettings().LoopDetector.Loops; writer.WriteStartObject(); + if (loops.TryGetValue(typeof(OpenApiSchema), out List schemas)) + { + var openApiSchemas = schemas.Cast().Distinct().ToList() + .ToDictionary(k => k.Reference.Id); + + writer.WriteOptionalMap( + OpenApiConstants.Schemas, + Schemas, + (w, key, component) => { + component.SerializeAsV3WithoutReference(w); + }); + } writer.WriteEndObject(); return; } diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 7d3b49198..8ad081af5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -126,27 +126,50 @@ public void SerializeAsV2(IOpenApiWriter writer) // paths writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV2(w)); - // Serialize each referenceable object as full object without reference if the reference in the object points to itself. - // If the reference exists but points to other objects, the object is serialized to just that reference. - - // definitions - writer.WriteOptionalMap( - OpenApiConstants.Definitions, - Components?.Schemas, - (w, key, component) => + // If references have been inlined we don't need the to render the components section + // however if they have cycles, then we will need a component rendered + if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences) + { + var loops = writer.GetSettings().LoopDetector.Loops; + writer.WriteStartObject(); + if (loops.TryGetValue(typeof(OpenApiSchema), out List schemas)) { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Schema && - component.Reference.Id == key) - { - component.SerializeAsV2WithoutReference(w); - } - else + var openApiSchemas = schemas.Cast().Distinct().ToList() + .ToDictionary(k => k.Reference.Id); + + writer.WriteOptionalMap( + OpenApiConstants.Definitions, + openApiSchemas, + (w, key, component) => + { + component.SerializeAsV2WithoutReference(w); + }); + } + writer.WriteEndObject(); + return; + } + else + { + // Serialize each referenceable object as full object without reference if the reference in the object points to itself. + // If the reference exists but points to other objects, the object is serialized to just that reference. + // definitions + writer.WriteOptionalMap( + OpenApiConstants.Definitions, + Components?.Schemas, + (w, key, component) => { - component.SerializeAsV2(w); - } - }); - + if (component.Reference != null && + component.Reference.Type == ReferenceType.Schema && + component.Reference.Id == key) + { + component.SerializeAsV2WithoutReference(w); + } + else + { + component.SerializeAsV2(w); + } + }); + } // parameters writer.WriteOptionalMap( OpenApiConstants.Parameters, diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 5cb1c10d8..0a47dfae3 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -251,13 +251,31 @@ public void SerializeAsV3(IOpenApiWriter writer) throw Error.ArgumentNull(nameof(writer)); } - if (Reference != null && writer.GetSettings().ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) + var settings = writer.GetSettings(); + + if (Reference != null) { - Reference.SerializeAsV3(writer); - return; + if (settings.ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) + { + Reference.SerializeAsV3(writer); + return; + } + + // If Loop is detected then just Serialize as a reference. + if (!settings.LoopDetector.PushLoop(this)) + { + settings.LoopDetector.SaveLoop(this); + Reference.SerializeAsV3(writer); + return; + } } SerializeAsV3WithoutReference(writer); + + if (Reference != null) + { + settings.LoopDetector.PopLoop(); + } } /// diff --git a/src/Microsoft.OpenApi/Services/LoopDetector.cs b/src/Microsoft.OpenApi/Services/LoopDetector.cs new file mode 100644 index 000000000..64001b22b --- /dev/null +++ b/src/Microsoft.OpenApi/Services/LoopDetector.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OpenApi.Services +{ + internal class LoopDetector + { + private readonly Dictionary> _loopStacks = new Dictionary>(); + + /// + /// Maintain history of traversals to avoid stack overflows from cycles + /// + /// Any unique identifier for a stack. + /// Identifier used for current context. + /// If method returns false a loop was detected and the key is not added. + public bool PushLoop(T key) + { + Stack stack; + if (!_loopStacks.TryGetValue(typeof(T), out stack)) + { + stack = new Stack(); + _loopStacks.Add(typeof(T), stack); + } + + if (!stack.Contains(key)) + { + stack.Push(key); + return true; + } + else + { + return false; // Loop detected + } + } + + /// + /// Exit from the context in cycle detection + /// + /// Identifier of loop + public void PopLoop() + { + if (_loopStacks[typeof(T)].Count > 0) + { + _loopStacks[typeof(T)].Pop(); + } + } + + public void SaveLoop(T loop) + { + if (!Loops.ContainsKey(typeof(T))) + { + Loops[typeof(T)] = new List(); + } + Loops[typeof(T)].Add(loop); + } + + /// + /// List of Loops detected + /// + public Dictionary> Loops { get; } = new Dictionary>(); + + /// + /// Reset loop tracking stack + /// + /// Identifier of loop to clear + internal void ClearLoop() + { + _loopStacks[typeof(T)].Clear(); + } + } +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs index 00bb9dede..45eedc831 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs @@ -1,4 +1,6 @@  +using Microsoft.OpenApi.Services; + namespace Microsoft.OpenApi.Writers { /// @@ -25,6 +27,7 @@ public enum ReferenceInlineSetting /// public class OpenApiWriterSettings { + internal LoopDetector LoopDetector { get; } = new LoopDetector(); /// /// Indicates how references in the source document should be handled. /// diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs index 4b0df7624..dd846030b 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using FluentAssertions; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; using Xunit; using Xunit.Abstractions; @@ -346,5 +347,172 @@ public void WriteDateTimeAsJsonShouldMatchExpected(DateTimeOffset dateTimeOffset // Assert writtenString.Should().Be(expectedString); } + + [Fact] + + public void WriteInlineSchema() + { + // Arrange + var thingSchema = new OpenApiSchema() + { + Type = "object", + UnresolvedReference = false, + Reference = new OpenApiReference + { + Id = "thing", + Type = ReferenceType.Schema + } + }; + + var doc = new OpenApiDocument() + { + Info = new OpenApiInfo() + { + Title = "Demo", + Version = "1.0.0" + }, + Paths = new OpenApiPaths() + { + ["/"] = new OpenApiPathItem + { + Operations = { + [OperationType.Get] = new OpenApiOperation() { + Responses = { + ["200"] = new OpenApiResponse { + Description = "OK", + Content = { + ["application/json"] = new OpenApiMediaType() { + Schema = thingSchema + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = { + ["thing"] = thingSchema} + } + }; + + var expected = +@"openapi: 3.0.1 +info: + title: Demo + version: 1.0.0 +paths: + /: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +components: { }"; + + var outputString = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputString, new OpenApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineLocalReferences}); + + // Act + doc.SerializeAsV3(writer); + var actual = outputString.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + + [Fact] + + public void WriteInlineRecursiveSchema() + { + // Arrange + var thingSchema = new OpenApiSchema() { + Type = "object", + UnresolvedReference = false, + Reference = new OpenApiReference { + Id = "thing", + Type = ReferenceType.Schema + } + }; + thingSchema.Properties["children"] = thingSchema; + + var doc = new OpenApiDocument() { + Info = new OpenApiInfo() { Title = "Demo", + Version = "1.0.0" }, + Paths = new OpenApiPaths() { + ["/"] = new OpenApiPathItem { + Operations = { + [OperationType.Get] = new OpenApiOperation() { + Responses = { + ["200"] = new OpenApiResponse { + Description = "OK", + Content = { + ["application/json"] = new OpenApiMediaType() { + Schema = thingSchema + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents { + Schemas = { + ["thing"] = thingSchema} + } + }; + + var expected = +@"openapi: 3.0.1 +info: + title: Demo + version: 1.0.0 +paths: + /: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + children: + $ref: '#/components/schemas/thing' +components: + schemas: + thing: + type: object + properties: + children: + type: object + properties: + children: + $ref: '#/components/schemas/thing'"; + // Component schemas that are there due to cycles are still inlined because the items they reference may not exist in the components because they don't have cycles. + + var outputString = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputString, new OpenApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineLocalReferences }); + + // Act + doc.SerializeAsV3(writer); + var actual = outputString.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + } }