Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
54 changes: 51 additions & 3 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Exceptions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Services;
using Microsoft.OpenApi.Writers;

namespace Microsoft.OpenApi.Models
Expand Down Expand Up @@ -131,12 +133,17 @@ public void SerializeAsV2(IOpenApiWriter writer)
if (writer.GetSettings().ReferenceInline != ReferenceInlineSetting.DoNotInlineReferences)
{
var loops = writer.GetSettings().LoopDetector.Loops;
writer.WriteStartObject();

if (loops.TryGetValue(typeof(OpenApiSchema), out List<object> schemas))
{
var openApiSchemas = schemas.Cast<OpenApiSchema>().Distinct().ToList()
.ToDictionary<OpenApiSchema, string>(k => k.Reference.Id);

foreach (var schema in openApiSchemas.Values.ToList())
{
FindSchemaReferences.ResolveSchemas(Components, openApiSchemas);
}

writer.WriteOptionalMap(
OpenApiConstants.Definitions,
openApiSchemas,
Expand All @@ -145,8 +152,6 @@ public void SerializeAsV2(IOpenApiWriter writer)
component.SerializeAsV2WithoutReference(w);
});
}
writer.WriteEndObject();
return;
}
else
{
Expand Down Expand Up @@ -390,4 +395,47 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference)
}
}
}

internal class FindSchemaReferences : OpenApiVisitorBase
{
private Dictionary<string, OpenApiSchema> Schemas;

public static void ResolveSchemas(OpenApiComponents components, Dictionary<string, OpenApiSchema> schemas )
{
var visitor = new FindSchemaReferences();
visitor.Schemas = schemas;
var walker = new OpenApiWalker(visitor);
walker.Walk(components);
}

public override void Visit(IOpenApiReferenceable referenceable)
{
switch (referenceable)
{
case OpenApiSchema schema:
if (!Schemas.ContainsKey(schema.Reference.Id))
{
Schemas.Add(schema.Reference.Id, schema);
}
break;

default:
break;
}
base.Visit(referenceable);
}

public override void Visit(OpenApiSchema schema)
{
// This is needed to handle schemas used in Responses in components
if (schema.Reference != null)
{
if (!Schemas.ContainsKey(schema.Reference.Id))
{
Schemas.Add(schema.Reference.Id, schema);
}
}
base.Visit(schema);
}
}
}
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ internal void SerializeAsV2(
if (!settings.LoopDetector.PushLoop<OpenApiSchema>(this))
{
settings.LoopDetector.SaveLoop(this);
Reference.SerializeAsV3(writer);
Reference.SerializeAsV2(writer);
return;
}
}
Expand Down
205 changes: 166 additions & 39 deletions test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,77 @@ public void WriteDateTimeAsJsonShouldMatchExpected(DateTimeOffset dateTimeOffset
[Fact]

public void WriteInlineSchema()
{
// Arrange
var doc = CreateDocWithSimpleSchemaToInline();

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 WriteInlineSchemaV2()
{
var doc = CreateDocWithSimpleSchemaToInline();

var expected =
@"swagger: '2.0'
info:
title: Demo
version: 1.0.0
paths:
/:
get:
produces:
- application/json
responses:
'200':
description: OK
schema:
type: object";

var outputString = new StringWriter(CultureInfo.InvariantCulture);
var writer = new OpenApiYamlWriter(outputString, new OpenApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineLocalReferences });

// Act
doc.SerializeAsV2(writer);
var actual = outputString.GetStringBuilder().ToString();

// Assert
actual = actual.MakeLineBreaksEnvironmentNeutral();
expected = expected.MakeLineBreaksEnvironmentNeutral();
Assert.Equal(expected, actual);
}

private static OpenApiDocument CreateDocWithSimpleSchemaToInline()
{
// Arrange
var thingSchema = new OpenApiSchema()
Expand Down Expand Up @@ -397,6 +468,15 @@ public void WriteInlineSchema()
["thing"] = thingSchema}
}
};
return doc;
}

[Fact]

public void WriteInlineRecursiveSchema()
{
// Arrange
var doc = CreateDocWithRecursiveSchemaReference();

var expected =
@"openapi: 3.0.1
Expand All @@ -413,10 +493,29 @@ public void WriteInlineSchema()
application/json:
schema:
type: object
components: { }";
properties:
children:
$ref: '#/components/schemas/thing'
related:
type: integer
components:
schemas:
thing:
type: object
properties:
children:
type: object
properties:
children:
$ref: '#/components/schemas/thing'
related:
type: integer
related:
type: integer";
// 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});
var writer = new OpenApiYamlWriter(outputString, new OpenApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineLocalReferences });

// Act
doc.SerializeAsV3(writer);
Expand All @@ -428,27 +527,44 @@ public void WriteInlineSchema()
Assert.Equal(expected, actual);
}


[Fact]

public void WriteInlineRecursiveSchema()
private static OpenApiDocument CreateDocWithRecursiveSchemaReference()
{
// Arrange
var thingSchema = new OpenApiSchema() {
var thingSchema = new OpenApiSchema()
{
Type = "object",
UnresolvedReference = false,
Reference = new OpenApiReference {
Id = "thing",
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 {

var relatedSchema = new OpenApiSchema()
{
Type = "integer",
UnresolvedReference = false,
Reference = new OpenApiReference
{
Id = "related",
Type = ReferenceType.Schema
}
};

thingSchema.Properties["related"] = relatedSchema;

var doc = new OpenApiDocument()
{
Info = new OpenApiInfo()
{
Title = "Demo",
Version = "1.0.0"
},
Paths = new OpenApiPaths()
{
["/"] = new OpenApiPathItem
{
Operations = {
[OperationType.Get] = new OpenApiOperation() {
Responses = {
Expand All @@ -462,50 +578,61 @@ public void WriteInlineRecursiveSchema()
}
}
}
}
}
}
},
Components = new OpenApiComponents {
Components = new OpenApiComponents
{
Schemas = {
["thing"] = thingSchema}
["thing"] = thingSchema}
}
};

return doc;
}

[Fact]
public void WriteInlineRecursiveSchemav2()
{
// Arrange
var doc = CreateDocWithRecursiveSchemaReference();

var expected =
@"openapi: 3.0.1
@"swagger: '2.0'
info:
title: Demo
version: 1.0.0
paths:
/:
get:
produces:
- application/json
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'";
schema:
type: object
properties:
children:
$ref: '#/definitions/thing'
related:
type: integer
definitions:
thing:
type: object
properties:
children:
$ref: '#/definitions/thing'
related:
$ref: '#/definitions/related'
related:
type: integer";
// 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);
doc.SerializeAsV2(writer);
var actual = outputString.GetStringBuilder().ToString();

// Assert
Expand Down