diff --git a/src/Microsoft.OpenApi.Readers/OpenApiDocumentExtensions.cs b/src/Microsoft.OpenApi.Readers/OpenApiDocumentExtensions.cs new file mode 100644 index 000000000..eb2b8fcdf --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/OpenApiDocumentExtensions.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; + +namespace Microsoft.OpenApi +{ + /// + /// Loads an OpenApiDocument instance through Load/LoadAsync/Parse pattern + /// + public static class OpenApiDocumentExtensions + { + /// + /// Loads an OpenApiDocument from a file stream + /// + /// + /// + /// + /// + /// + public static OpenApiDocument Load(this OpenApiDocument document, + Stream stream, + out OpenApiDiagnostic diagnostic, + OpenApiReaderSettings settings = null) + { + return new OpenApiStreamReader(settings).Read(stream, out diagnostic); + } + + /// + /// Loads an OpenApiDocument from a TextReader + /// + /// + /// + /// + /// + /// + public static OpenApiDocument Load(this OpenApiDocument document, + TextReader reader, + out OpenApiDiagnostic diagnostic, + OpenApiReaderSettings settings = null) + { + return new OpenApiTextReaderReader(settings).Read(reader, out diagnostic); + } + + /// + /// Loads an OpenApiDocument from a string input + /// + /// + /// + /// + /// + /// + public static OpenApiDocument Load(this OpenApiDocument document, + string input, + out OpenApiDiagnostic diagnostic, + OpenApiReaderSettings settings = null) + { + return new OpenApiStringReader(settings).Read(input, out diagnostic); + } + + /// + /// Loads an OpenApiDocument from a string input + /// + /// + /// + /// + /// + /// + public static OpenApiDocument Parse(this OpenApiDocument document, + string input, + out OpenApiDiagnostic diagnostic, + OpenApiReaderSettings settings = null) + { + return new OpenApiStringReader(settings).Read(input, out diagnostic); + } + + /// + /// Loads an OpenApiDocument asynchronously from a TextReader + /// + /// + /// + /// + /// + /// + public static Task LoadAsync(this OpenApiDocument document, + TextReader reader, + OpenApiReaderSettings settings = null, + CancellationToken cancellationToken = default) + { + return new OpenApiTextReaderReader(settings).ReadAsync(reader, cancellationToken); + } + + /// + /// Loads an OpenApiDocument from a file stream + /// + /// + /// + /// + /// + /// + public static Task LoadAsync(this OpenApiDocument document, + Stream stream, + OpenApiReaderSettings settings = null, + CancellationToken cancellationToken = default) + { + return new OpenApiStreamReader(settings).ReadAsync(stream, cancellationToken); + } + } +} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index 95482bfa6..e0f727a2a 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; @@ -17,7 +15,6 @@ using Microsoft.OpenApi.Readers.Services; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; -using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers { @@ -37,6 +34,7 @@ public OpenApiYamlDocumentReader(OpenApiReaderSettings settings = null) _settings = settings ?? new OpenApiReaderSettings(); } + /// /// Reads the stream input and parses it into an Open API document. /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index e79a6539d..e3b2d8f3a 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -18,12 +18,12 @@ public class OpenApiWorkspaceStreamTests public async Task LoadingDocumentWithResolveAllReferencesShouldLoadDocumentIntoWorkspace() { // Create a reader that will resolve all references - var reader = new OpenApiStreamReader(new OpenApiReaderSettings() + var settings = new OpenApiReaderSettings() { LoadExternalRefs = true, CustomExternalLoader = new MockLoader(), BaseUrl = new Uri("file://c:\\") - }); + }; // Todo: this should be ReadAsync var stream = new MemoryStream(); @@ -37,7 +37,7 @@ public async Task LoadingDocumentWithResolveAllReferencesShouldLoadDocumentIntoW wr.Flush(); stream.Position = 0; - var result = await reader.ReadAsync(stream); + var result = await new OpenApiDocument().LoadAsync(stream, settings: settings); Assert.NotNull(result.OpenApiDocument.Workspace); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs index 984c4cdcd..39731ccb0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs @@ -8,7 +8,6 @@ using FluentAssertions; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Exceptions; -using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Xunit; @@ -41,8 +40,7 @@ public void ShouldThrowWhenReferenceTypeIsInvalid() $ref: '#/defi888nition/does/notexist' "; - var reader = new OpenApiStringReader(); - var doc = reader.Read(input, out var diagnostic); + var doc = new OpenApiDocument().Parse(input, out var diagnostic); diagnostic.Errors.Should().BeEquivalentTo(new List { new OpenApiError( new OpenApiException("Unknown reference type 'defi888nition'")) }); @@ -68,9 +66,7 @@ public void ShouldThrowWhenReferenceDoesNotExist() $ref: '#/definitions/doesnotexist' "; - var reader = new OpenApiStringReader(); - - var doc = reader.Read(input, out var diagnostic); + var doc = new OpenApiDocument().Load(input, out var diagnostic); diagnostic.Errors.Should().BeEquivalentTo(new List { new OpenApiError( new OpenApiException("Invalid Reference identifier 'doesnotexist'.")) }); @@ -88,7 +84,7 @@ public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); - var openApiDoc = new OpenApiStringReader().Read( + var openApiDoc = new OpenApiDocument().Load( @" swagger: 2.0 info: @@ -166,112 +162,110 @@ public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) [Fact] public void ShouldParseProducesInAnyOrder() { - using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "twoResponses.json"))) - { - var reader = new OpenApiStreamReader(); - var doc = reader.Read(stream, out var diagnostic); + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "twoResponses.json")); + var doc = new OpenApiDocument().Load(stream, out var diagnostic); - var successSchema = new OpenApiSchema() + var successSchema = new OpenApiSchema() + { + Type = "array", + Reference = new OpenApiReference { - Type = "array", - Reference = new OpenApiReference + Type = ReferenceType.Schema, + Id = "Item", + HostDocument = doc + }, + Items = new OpenApiSchema() + { + Reference = new OpenApiReference() { Type = ReferenceType.Schema, Id = "Item", HostDocument = doc - }, - Items = new OpenApiSchema() - { - Reference = new OpenApiReference() + } + } + }; + + var okSchema = new OpenApiSchema() + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "Item", + HostDocument = doc + }, + Properties = new Dictionary() + { + { "id", new OpenApiSchema() { - Type = ReferenceType.Schema, - Id = "Item", - HostDocument = doc + Type = "string", + Description = "Item identifier." } } - }; + } + }; - var okSchema = new OpenApiSchema() + var errorSchema = new OpenApiSchema() + { + Reference = new OpenApiReference { - Reference = new OpenApiReference - { - Type = ReferenceType.Schema, - Id = "Item", - HostDocument = doc - }, - Properties = new Dictionary() - { - { "id", new OpenApiSchema() - { - Type = "string", - Description = "Item identifier." - } - } - } - }; - - var errorSchema = new OpenApiSchema() + Type = ReferenceType.Schema, + Id = "Error", + HostDocument = doc + }, + Properties = new Dictionary() { - Reference = new OpenApiReference - { - Type = ReferenceType.Schema, - Id = "Error", - HostDocument = doc + { "code", new OpenApiSchema() + { + Type = "integer", + Format = "int32" + } }, - Properties = new Dictionary() - { - { "code", new OpenApiSchema() - { - Type = "integer", - Format = "int32" - } - }, - { "message", new OpenApiSchema() - { - Type = "string" - } - }, - { "fields", new OpenApiSchema() - { - Type = "string" - } - } - } - }; - - var okMediaType = new OpenApiMediaType - { - Schema = new OpenApiSchema - { - Type = "array", - Items = okSchema + { "message", new OpenApiSchema() + { + Type = "string" + } + }, + { "fields", new OpenApiSchema() + { + Type = "string" + } } - }; + } + }; - var errorMediaType = new OpenApiMediaType + var okMediaType = new OpenApiMediaType + { + Schema = new OpenApiSchema { - Schema = errorSchema - }; + Type = "array", + Items = okSchema + } + }; - doc.Should().BeEquivalentTo(new OpenApiDocument + var errorMediaType = new OpenApiMediaType + { + Schema = errorSchema + }; + + doc.Should().BeEquivalentTo(new OpenApiDocument + { + Info = new OpenApiInfo { - Info = new OpenApiInfo - { - Title = "Two responses", - Version = "1.0.0" - }, - Servers = + Title = "Two responses", + Version = "1.0.0" + }, + Servers = { new OpenApiServer { Url = "https://" } }, - Paths = new OpenApiPaths + Paths = new OpenApiPaths + { + ["/items"] = new OpenApiPathItem { - ["/items"] = new OpenApiPathItem - { - Operations = + Operations = { [OperationType.Get] = new OpenApiOperation { @@ -344,29 +338,26 @@ public void ShouldParseProducesInAnyOrder() } } } - } - }, - Components = new OpenApiComponents - { - Schemas = + } + }, + Components = new OpenApiComponents + { + Schemas = { ["Item"] = okSchema, ["Error"] = errorSchema } - } - }); - } + } + }); } [Fact] public void ShouldAssignSchemaToAllResponses() { OpenApiDocument document; - OpenApiDiagnostic diagnostic; - using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "multipleProduces.json"))) - { - document = new OpenApiStreamReader().Read(stream, out diagnostic); - } + + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "multipleProduces.json")); + document = new OpenApiDocument().Load(stream, out var diagnostic); Assert.Equal(OpenApiSpecVersion.OpenApi2_0, diagnostic.SpecificationVersion); @@ -437,18 +428,15 @@ public void ShouldAssignSchemaToAllResponses() [Fact] public void ShouldAllowComponentsThatJustContainAReference() { - using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "ComponentRootReference.json"))) + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "ComponentRootReference.json")); + OpenApiDocument doc = new OpenApiDocument().Load(stream, out OpenApiDiagnostic diags); + OpenApiSchema schema1 = doc.Components.Schemas["AllPets"]; + Assert.False(schema1.UnresolvedReference); + OpenApiSchema schema2 = doc.ResolveReferenceTo(schema1.Reference); + if (schema2.UnresolvedReference && schema1.Reference.Id == schema2.Reference.Id) { - OpenApiStreamReader reader = new OpenApiStreamReader(); - OpenApiDocument doc = reader.Read(stream, out OpenApiDiagnostic diags); - OpenApiSchema schema1 = doc.Components.Schemas["AllPets"]; - Assert.False(schema1.UnresolvedReference); - OpenApiSchema schema2 = doc.ResolveReferenceTo(schema1.Reference); - if (schema2.UnresolvedReference && schema1.Reference.Id == schema2.Reference.Id) - { - // detected a cycle - this code gets triggered - Assert.True(false, "A cycle should not be detected"); - } + // detected a cycle - this code gets triggered + Assert.True(false, "A cycle should not be detected"); } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 254a37ef9..a26e677ba 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -81,7 +79,7 @@ public OpenApiDocumentTests(ITestOutputHelper output) [Fact] public void ParseDocumentFromInlineStringShouldSucceed() { - var openApiDoc = new OpenApiStringReader().Read( + var openApiDoc = new OpenApiDocument().Parse( @" openapi : 3.0.0 info: @@ -123,7 +121,7 @@ public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); - var openApiDoc = new OpenApiStringReader().Read( + var openApiDoc = new OpenApiDocument().Load( @" openapi : 3.0.0 info: @@ -196,7 +194,7 @@ public void ParseBasicDocumentWithMultipleServersShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicDocumentWithMultipleServers.yaml"))) { - var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + var openApiDoc = new OpenApiDocument().Load(stream, out var diagnostic); diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() @@ -238,7 +236,7 @@ public void ParseBasicDocumentWithMultipleServersShouldSucceed() public void ParseBrokenMinimalDocumentShouldYieldExpectedDiagnostic() { using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "brokenMinimalDocument.yaml")); - var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + var openApiDoc = new OpenApiDocument().Load(stream, out var diagnostic); openApiDoc.Should().BeEquivalentTo( new OpenApiDocument @@ -267,7 +265,7 @@ public void ParseMinimalDocumentShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "minimalDocument.yaml"))) { - var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + var openApiDoc = new OpenApiDocument().Load(stream, out var diagnostic); openApiDoc.Should().BeEquivalentTo( new OpenApiDocument @@ -298,7 +296,7 @@ public void ParseStandardPetStoreDocumentShouldSucceed() OpenApiDiagnostic context; using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStore.yaml"))) { - var actual = new OpenApiStreamReader().Read(stream, out context); + var actual = new OpenApiDocument().Load(stream, out context); var components = new OpenApiComponents { @@ -728,7 +726,7 @@ public void ParseModifiedPetStoreDocumentWithTagAndSecurityShouldSucceed() OpenApiDiagnostic context; using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStoreWithTagAndSecurity.yaml"))) { - var actual = new OpenApiStreamReader().Read(stream, out context); + var actual = new OpenApiDocument().Load(stream, out context); var components = new OpenApiComponents { @@ -1262,7 +1260,7 @@ public void ParsePetStoreExpandedShouldSucceed() using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "petStoreExpanded.yaml"))) { - var actual = new OpenApiStreamReader().Read(stream, out context); + var actual = new OpenApiDocument().Load(stream, out context); // TODO: Create the object in memory and compare with the one read from YAML file. } @@ -1276,7 +1274,7 @@ public void GlobalSecurityRequirementShouldReferenceSecurityScheme() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "securedApi.yaml"))) { - var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + var openApiDoc = new OpenApiDocument().Load(stream, out var diagnostic); var securityRequirement = openApiDoc.SecurityRequirements.First(); @@ -1289,7 +1287,7 @@ public void HeaderParameterShouldAllowExample() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "apiWithFullHeaderComponent.yaml"))) { - var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + var openApiDoc = new OpenApiDocument().Load(stream, out var diagnostic); var exampleHeader = openApiDoc.Components?.Headers?["example-header"]; Assert.NotNull(exampleHeader); @@ -1365,10 +1363,9 @@ public void DoesNotChangeExternalReferences() using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "documentWithExternalRefs.yaml")); // Act - var doc = new OpenApiStreamReader( - new OpenApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences }) - .Read(stream, out var diagnostic); - + var settings = new OpenApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences }; + var doc = new OpenApiDocument().Load(stream, out var diagnostic, settings); + var externalRef = doc.Components.Schemas["Nested"].Properties["AnyOf"].AnyOf.First().Reference.ReferenceV3; var externalRef2 = doc.Components.Schemas["Nested"].Properties["AnyOf"].AnyOf.Last().Reference.ReferenceV3; @@ -1384,10 +1381,12 @@ public void ParseDocumentWithReferencedSecuritySchemeWorks() using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "docWithSecuritySchemeReference.yaml")); // Act - var doc = new OpenApiStreamReader(new OpenApiReaderSettings + var settings = new OpenApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences - }).Read(stream, out var diagnostic); + }; + + var doc = new OpenApiDocument().Load(stream, out var diagnostic, settings); var securityScheme = doc.Components.SecuritySchemes["OAuth2"]; @@ -1401,7 +1400,7 @@ public void ParseDocumentWithWebhooksShouldSucceed() { // Arrange and Act using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "documentWithWebhooks.yaml")); - var actual = new OpenApiStreamReader().Read(stream, out var diagnostic); + var actual = new OpenApiDocument().Load(stream, out var diagnostic); var components = new OpenApiComponents { @@ -1609,7 +1608,7 @@ public void ParseDocumentsWithReusablePathItemInWebhooksSucceeds() { // Arrange && Act using var stream = Resources.GetStream("V3Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml"); - var actual = new OpenApiStreamReader().Read(stream, out var context); + var actual = new OpenApiDocument().Load(stream, out var context); var components = new OpenApiComponents { @@ -1829,7 +1828,7 @@ public void ParseDocumentWithDescriptionInDollarRefsShouldSucceed() using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "documentWithSummaryAndDescriptionInReference.yaml")); // Act - var actual = new OpenApiStreamReader().Read(stream, out var diagnostic); + var actual = new OpenApiDocument().Load(stream, out var diagnostic); var schema = actual.Paths["/pets"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; var header = actual.Components.Responses["Test"].Headers["X-Test"];